diff --git a/.gitignore b/.gitignore index 3e523f5..643981d 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,6 @@ dist package-lock.json pnpm-lock.yaml yarn.lock + +/index.cjs +/index.js diff --git a/CHANGELOG.md b/CHANGELOG.md index e582e8a..b5114eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 0.5.3 (2022-10-16) + +- ee0a882 `src-output` -> `__snapshots__` +- 16593ff v0.5.3 +- 2a5752b docs: v0.5.3 +- 236730a refactor: use vite-plugin-utils +- bb793a5 chore: update config +- 1148671 feat: support `"type": "module"` +- f590da1 chore: `"strict": true` +- cf98458 bump deps +- 04f95f7 chore: bump deps +- 0434172 chore: update comments + +--- ## [2022-05-04] v0.3.0 diff --git a/README.md b/README.md index 5c3e358..0f7992e 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,10 @@ export default { } ``` -## API +## API (Define) ```ts export interface Options { - extensions?: string[] filter?: (id: string) => false | undefined dynamic?: { /** diff --git a/README.zh-CN.md b/README.zh-CN.md index 332715a..c2a6c70 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -26,11 +26,10 @@ export default { } ``` -## API +## API (Define) ```ts export interface Options { - extensions?: string[] filter?: (id: string) => false | undefined dynamic?: { /** diff --git a/package.json b/package.json index e6c8259..b056922 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,16 @@ { "name": "vite-plugin-commonjs", - "version": "0.5.2", + "version": "0.5.3", "description": "A pure JavaScript implementation of CommonJs", - "main": "dist/index.js", + "type": "module", + "main": "index.js", + "types": "src", + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + } + }, "repository": { "type": "git", "url": "git+https://github.com/vite-plugin/vite-plugin-commonjs.git" @@ -10,18 +18,20 @@ "author": "草鞋没号 <308487730@qq.com>", "license": "MIT", "scripts": { - "build": "rm -rf dist && tsc", - "prepublishOnly": "npm run build", - "test": "vite -c test/vite.config.ts" + "dev": "vite build --watch", + "build": "vite build", + "test": "vite -c test/vite.config.ts", + "prepublishOnly": "npm run build" }, "dependencies": { - "fast-glob": "^3.2.11", - "vite-plugin-dynamic-import": "^0.9.9" + "fast-glob": "~3.2.11" }, "devDependencies": { - "@types/node": "^17.0.36", - "typescript": "^4.7.2", - "vite": "^3.0.0-alpha.11" + "@types/node": "^18.7.14", + "typescript": "^4.7.4", + "vite": "^3.2.0-beta.2", + "vite-plugin-dynamic-import": "^1.2.3", + "vite-plugin-utils": "^0.3.3" }, "keywords": [ "vite", @@ -30,6 +40,8 @@ "require" ], "files": [ - "dist" + "src", + "index.cjs", + "index.js" ] } diff --git a/src/analyze.ts b/src/analyze.ts index 3a469f1..ce85913 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -1,5 +1,5 @@ -import { AcornNode } from './types' -import { simpleWalk } from './utils' +import type { AcornNode } from './types' +import { walk } from 'vite-plugin-utils/function' // ①(🎯): Top-level scope statement types, it also means statements that can be converted // 顶级作用于语句类型,这种可以被无缝换成 import @@ -56,7 +56,7 @@ export function analyzer(ast: AcornNode, code: string, id: string): Analyzed { exports: [], } - simpleWalk(ast, { + walk.sync(ast, { CallExpression(node, ancestors) { if (node.callee.name !== 'require') return @@ -71,7 +71,7 @@ export function analyzer(ast: AcornNode, code: string, id: string): Analyzed { dynamic: checkDynamicId(node), }) }, - AssignmentExpression(node, ancestors) { + AssignmentExpression(node) { if (node.left.type !== 'MemberExpression') return if (!(node.left.object.type === 'Identifier' && ['module', 'exports'].includes(node.left.object.name))) return @@ -109,7 +109,7 @@ function checkDynamicId(node: AcornNode): RequireStatement['dynamic'] { // // Will be return nearset scope ancestor node (🎯-①) // 这将返回最近作用域的祖先节点 -function findTopLevelScope(ancestors: AcornNode[]): AcornNode { +function findTopLevelScope(ancestors: AcornNode[]): AcornNode | undefined { const ances = ancestors.map(an => an.type).join() const arr = [...ancestors].reverse() diff --git a/src/dynamic-require.ts b/src/dynamic-require.ts index 50c18b3..7ff6c58 100644 --- a/src/dynamic-require.ts +++ b/src/dynamic-require.ts @@ -1,22 +1,18 @@ -import path from 'path' +import path from 'node:path' import type { ResolvedConfig } from 'vite' +import fastGlob from 'fast-glob' import { type Resolved, - dynamicImportToGlob, Resolve, - utils, + dynamicImportToGlob, + mappingPath, + toLooseGlob, } from 'vite-plugin-dynamic-import' -import fastGlob from 'fast-glob' +import { normalizePath, relativeify } from 'vite-plugin-utils/function' import type { Options } from '.' import type { Analyzed } from './analyze' -import { AcornNode } from './types' - -const { - normallyImporteeRE, - tryFixGlobSlash, - toDepthGlob, - mappingPath, -} = utils +import type { AcornNode } from './types' +import { normallyImporteeRE } from './utils' export interface DynamicRequireRecord { node: AcornNode @@ -33,7 +29,7 @@ export class DynaimcRequire { constructor( private config: ResolvedConfig, - private options: Options, + private options: Options & { extensions: string[] }, private resolve = new Resolve(config), ) { } @@ -53,7 +49,7 @@ export class DynaimcRequire { analyzed.code, analyzed.id, this.resolve, - options.extensions!, + this.options.extensions, options.dynamic?.loose !== false, ) if (!globResult) continue @@ -61,7 +57,7 @@ export class DynaimcRequire { let { files, resolved, normally } = globResult // skip itself - files = files.filter(f => path.join(path.dirname(id), f) !== id) + files = files!.filter(f => normalizePath(path.join(path.dirname(id), f)) !== id) // execute the dynamic.onFiles options.dynamic?.onFiles && (files = options.dynamic?.onFiles(files, id) || files) @@ -72,7 +68,10 @@ export class DynaimcRequire { if (!files?.length) continue - const maps = mappingPath(files, resolved) + const maps = mappingPath( + files, + resolved ? { [resolved.alias.relative]: resolved.alias.findString } : undefined, + ) let counter2 = 0 record.dynaimc = { importee: [], @@ -124,15 +123,15 @@ async function globFiles( resolved?: Resolved /** After `expressiontoglob()` processing, it may become a normally path */ normally?: string -}> { +} | undefined> { let files: string[] - let resolved: Resolved + let resolved: Resolved | undefined let normally: string const PAHT_FILL = '####/' const EXT_FILL = '.extension' - let glob: string - let globRaw: string + let glob: string | null + let globRaw!: string glob = await dynamicImportToGlob( node.arguments[0], @@ -162,21 +161,25 @@ async function globFiles( return } - glob = tryFixGlobSlash(glob) - loose !== false && (glob = toDepthGlob(glob)) - glob.includes(PAHT_FILL) && (glob = glob.replace(PAHT_FILL, '')) - glob.endsWith(EXT_FILL) && (glob = glob.replace(EXT_FILL, '')) - - const fileGlob = path.extname(glob) - ? glob - // If not ext is not specified, fill necessary extensions - // e.g. - // `./foo/*` -> `./foo/*.{js,ts,vue,...}` - : glob + `.{${extensions.map(e => e.replace(/^\./, '')).join(',')}}` + // @ts-ignore + const globs = [].concat(loose ? toLooseGlob(glob) : glob) + .map((g: any) => { + g.includes(PAHT_FILL) && (g = g.replace(PAHT_FILL, '')) + g.endsWith(EXT_FILL) && (g = g.replace(EXT_FILL, '')) + return g + }) + const fileGlobs = globs + .map(g => path.extname(g) + ? g + // If not ext is not specified, fill necessary extensions + // e.g. + // `./foo/*` -> `./foo/*.{js,ts,vue,...}` + : g + `.{${extensions.map(e => e.replace(/^\./, '')).join(',')}}` + ) files = fastGlob - .sync(fileGlob, { cwd: /* 🚧-① */path.dirname(importer) }) - .map(file => !file.startsWith('.') ? /* 🚧-② */`./${file}` : file) + .sync(fileGlobs, { cwd: /* 🚧-① */path.dirname(importer) }) + .map(file => relativeify(file)) return { files, resolved } } diff --git a/src/generate-export.ts b/src/generate-export.ts index d947a92..bf6f4c1 100644 --- a/src/generate-export.ts +++ b/src/generate-export.ts @@ -2,7 +2,7 @@ import { Analyzed } from './analyze' export interface ExportsRuntime { polyfill: string - exportDeclaration: string + exportDeclaration: string } export function generateExport(analyzed: Analyzed): ExportsRuntime | null { @@ -11,8 +11,8 @@ export function generateExport(analyzed: Analyzed): ExportsRuntime | null { } const memberDefault = analyzed.exports - // Find `module.exports` or `exports.default` - .find(exp => exp.token.left === 'module' || exp.token.right === 'default') + // Find `module.exports` or `exports.default` + .find(exp => exp.token.left === 'module' || exp.token.right === 'default') let members = analyzed.exports // Exclude `module.exports` and `exports.default` @@ -20,7 +20,7 @@ export function generateExport(analyzed: Analyzed): ExportsRuntime | null { .map(exp => exp.token.right) // Remove duplicate export members = [...new Set(members)] - + const membersDeclaration = members.map( m => `const __CJS__export_${m}__ = (module.exports == null ? {} : module.exports).${m}`, ) diff --git a/src/generate-import.ts b/src/generate-import.ts index 0ec9860..afb0378 100644 --- a/src/generate-import.ts +++ b/src/generate-import.ts @@ -48,14 +48,14 @@ export function generateImport(analyzed: Analyzed) { topScopeNode, dynamic, } = req - + // ③(🚧) // Processed in dynamic-require.ts if (dynamic === 'dynamic') continue const impt: ImportRecord = { node, topScopeNode } const importName = `__CJS__import__${count++}__` - + const requireIdNode = node.arguments[0] let requireId: string if (!requireIdNode) continue // Not value - require() @@ -65,13 +65,13 @@ export function generateImport(analyzed: Analyzed) { requireId = requireIdNode.quasis[0].value.raw } - if (!requireId) { + if (!requireId!) { const codeSnippets = analyzed.code.slice(node.start, node.end) throw new Error(`The following require statement cannot be converted. -> ${codeSnippets} ${'^'.repeat(codeSnippets.length)}`) } - + if (topScopeNode) { // ①(🎯) @@ -113,7 +113,7 @@ export function generateImport(analyzed: Analyzed) { impt.importee = `import { ${LV_str('as')} } from '${requireId}'` } } else if (init.type === 'MemberExpression') { - const onlyOneMember = ancestors.find(an => an.type === 'MemberExpression').property.name + const onlyOneMember = ancestors.find(an => an.type === 'MemberExpression')?.property.name const importDefault = onlyOneMember === 'default' if (typeof LV === 'string') { if (importDefault) { diff --git a/src/index.ts b/src/index.ts index 85131eb..be90140 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,19 @@ -import path from 'path' +import path from 'node:path' import type { Plugin, ResolvedConfig } from 'vite' -import { analyzer, TopScopeType } from './analyze' -import { generateImport } from './generate-import' -import { generateExport } from './generate-export' import { - cleanUrl, - isCommonjs, - JS_EXTENSIONS, + DEFAULT_EXTENSIONS, + KNOWN_SFC_EXTENSIONS, KNOWN_ASSET_TYPES, KNOWN_CSS_TYPES, - KNOWN_SFC_EXTENSIONS, - MagicString, -} from './utils' +} from 'vite-plugin-utils/constant' +import { MagicString } from 'vite-plugin-utils/function' +import { analyzer, TopScopeType } from './analyze' +import { generateImport } from './generate-import' +import { generateExport } from './generate-export' +import { isCommonjs } from './utils' import { DynaimcRequire } from './dynamic-require' export interface Options { - extensions?: string[] filter?: (id: string) => false | undefined dynamic?: { /** @@ -41,10 +39,7 @@ export interface Options { export default function commonjs(options: Options = {}): Plugin { let config: ResolvedConfig - const extensions = JS_EXTENSIONS - .concat(KNOWN_SFC_EXTENSIONS) - .concat(KNOWN_ASSET_TYPES) - .concat(KNOWN_CSS_TYPES) + let extensions = DEFAULT_EXTENSIONS let dynaimcRequire: DynaimcRequire return { @@ -52,18 +47,23 @@ export default function commonjs(options: Options = {}): Plugin { name: 'vite-plugin-commonjs', configResolved(_config) { config = _config - options.extensions = [...new Set((config.resolve?.extensions || extensions).concat(options.extensions || []))] - dynaimcRequire = new DynaimcRequire(_config, options) + // https://github.com/vitejs/vite/blob/37ac91e5f680aea56ce5ca15ce1291adc3cbe05e/packages/vite/src/node/plugins/resolve.ts#L450 + if (config.resolve?.extensions) extensions = config.resolve.extensions + dynaimcRequire = new DynaimcRequire(_config, { + ...options, + extensions: [ + ...extensions, + ...KNOWN_SFC_EXTENSIONS, + ...KNOWN_ASSET_TYPES.map(type => '.' + type), + ...KNOWN_CSS_TYPES.map(type => '.' + type), + ], + }) }, async transform(code, id) { - const pureId = cleanUrl(id) - const extensions = JS_EXTENSIONS.concat(KNOWN_SFC_EXTENSIONS) - const { ext } = path.parse(pureId) - - if (/node_modules\/(?!\.vite\/)/.test(pureId)) return - if (!extensions.includes(ext)) return + if (/node_modules\/(?!\.vite\/)/.test(id)) return + if (!extensions.includes(path.extname(id))) return if (!isCommonjs(code)) return - if (options.filter?.(pureId) === false) return + if (options.filter?.(id) === false) return const ast = this.parse(code) const analyzed = analyzer(ast, code, id) @@ -74,7 +74,7 @@ export default function commonjs(options: Options = {}): Plugin { : generateExport(analyzed) const dynamics = await dynaimcRequire.generateRuntime(analyzed) - const promotionImports = [] + const hoistImports = [] const ms = new MagicString(code) // require @@ -88,7 +88,7 @@ export default function commonjs(options: Options = {}): Plugin { } = impt const importee = imptee + ';' - let importStatement: string + let importStatement: string | undefined if (topScopeNode) { if (topScopeNode.type === TopScopeType.ExpressionStatement) { importStatement = importee @@ -97,7 +97,7 @@ export default function commonjs(options: Options = {}): Plugin { } } else { // TODO: Merge duplicated require id - promotionImports.push(importee) + hoistImports.push(importee) importStatement = importName } @@ -108,8 +108,8 @@ export default function commonjs(options: Options = {}): Plugin { } } - if (promotionImports.length) { - ms.prepend(['/* import-promotion-S */', ...promotionImports, '/* import-promotion-E */'].join(' ')) + if (hoistImports.length) { + ms.prepend(['/* import-hoist-S */', ...hoistImports, '/* import-hoist-E */'].join(' ')) } // exports diff --git a/src/utils.ts b/src/utils.ts index ae48182..6e8b25c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,69 +1,12 @@ -import { builtinModules } from 'module' -import { type AcornNode } from './types' +import { builtinModules } from 'node:module' +import { multilineCommentsRE, singlelineCommentsRE } from 'vite-plugin-utils/constant' // ------------------------------------------------- RegExp -export const multilineCommentsRE = /\/\*(.|[\r\n])*?\*\//gm -export const singlelineCommentsRE = /\/\/.*/g -export const queryRE = /\?.*$/s -export const hashRE = /#.*$/s +export const normallyImporteeRE = /^\.{1,2}\/[.-/\w]+(\.\w+)$/ // ------------------------------------------------- const -export const JS_EXTENSIONS = [ - '.mjs', - '.js', - '.ts', - '.jsx', - '.tsx', - '.cjs' -] -export const KNOWN_SFC_EXTENSIONS = [ - '.vue', - '.svelte', -] -// https://github.com/vitejs/vite/blob/d6418605577319b2f92ea37081e34376bb47b286/packages/vite/src/node/constants.ts#L66 -export const KNOWN_ASSET_TYPES = [ - // images - 'png', - 'jpg', - 'jpeg', - 'gif', - 'svg', - 'ico', - 'webp', - 'avif', - - // media - 'mp4', - 'webm', - 'ogg', - 'mp3', - 'wav', - 'flac', - 'aac', - - // fonts - 'woff2?', - 'eot', - 'ttf', - 'otf', - - // other - 'webmanifest', - 'pdf', - 'txt' -] -export const KNOWN_CSS_TYPES = [ - 'css', - 'less', - 'sass', - 'scss', - 'styl', - 'stylus', - 'pcss', - 'postcss', -] export const builtins = [ ...builtinModules.map(m => !m.startsWith('_')), ...builtinModules.map(m => !m.startsWith('_')).map(m => `node:${m}`) @@ -71,10 +14,6 @@ export const builtins = [ // ------------------------------------------------- function -export function cleanUrl(url: string): string { - return url.replace(hashRE, '').replace(queryRE, '') -} - export function isCommonjs(code: string) { // Avoid matching the content of the comment code = code @@ -82,71 +21,3 @@ export function isCommonjs(code: string) { .replace(singlelineCommentsRE, '') return /\b(?:require|module|exports)\b/.test(code) } - -export function simpleWalk( - ast: AcornNode, - visitors: { - [type: string]: (node: AcornNode, ancestors: AcornNode[]) => void, - }, - ancestors: AcornNode[] = [], -) { - if (!ast) return - if (Array.isArray(ast)) { - for (const element of ast as AcornNode[]) { - simpleWalk(element, visitors, ancestors) - } - } else { - ancestors = ancestors.concat(ast) - for (const key of Object.keys(ast)) { - (typeof ast[key] === 'object' && - simpleWalk(ast[key], visitors, ancestors)) - } - } - visitors[ast.type]?.(ast, ancestors) -} -// TODO -simpleWalk.async = function simpleWalkAsync() { } - -export class MagicString { - private overwrites: { loc: [number, number]; content: string }[] - private starts = '' - private ends = '' - - constructor( - public str: string - ) { } - - public append(content: string) { - this.ends += content - return this - } - - public prepend(content: string) { - this.starts = content + this.starts - return this - } - - public overwrite(start: number, end: number, content: string) { - if (end < start) { - throw new Error(`"end" con't be less than "start".`) - } - if (!this.overwrites) { - this.overwrites = [] - } - - this.overwrites.push({ loc: [start, end], content }) - return this - } - - public toString() { - let str = this.str - if (this.overwrites) { - const arr = [...this.overwrites].sort((a, b) => b.loc[0] - a.loc[0]) - for (const { loc: [start, end], content } of arr) { - // TODO: check start or end overlap - str = str.slice(0, start) + content + str.slice(end) - } - } - return this.starts + str + this.ends - } -} diff --git a/test/src-output/dynamic.tsx b/test/__snapshots__/dynamic.tsx similarity index 100% rename from test/src-output/dynamic.tsx rename to test/__snapshots__/dynamic.tsx diff --git a/test/src-output/exports.js b/test/__snapshots__/exports.js similarity index 100% rename from test/src-output/exports.js rename to test/__snapshots__/exports.js diff --git a/test/src-output/main.ts b/test/__snapshots__/main.ts similarity index 100% rename from test/src-output/main.ts rename to test/__snapshots__/main.ts diff --git a/test/src-output/module-exports/hello.cjs b/test/__snapshots__/module-exports/hello.cjs similarity index 100% rename from test/src-output/module-exports/hello.cjs rename to test/__snapshots__/module-exports/hello.cjs diff --git a/test/src-output/module-exports/world.cjs b/test/__snapshots__/module-exports/world.cjs similarity index 100% rename from test/src-output/module-exports/world.cjs rename to test/__snapshots__/module-exports/world.cjs diff --git a/test/vite.config.ts b/test/vite.config.ts index be2b540..a9aa9de 100644 --- a/test/vite.config.ts +++ b/test/vite.config.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { defineConfig } from 'vite' import commonjs from '..' -fs.rmSync(path.join(__dirname, 'src-output'), { force: true, recursive: true }) +fs.rmSync(path.join(__dirname, '__snapshots__'), { force: true, recursive: true }) export default defineConfig({ root: __dirname, @@ -14,7 +14,7 @@ export default defineConfig({ transform(code, id) { if (/\/src\//.test(id)) { // Write transformed code to output/ - const filename = id.replace('src', 'src-output') + const filename = id.replace('src', '__snapshots__') const dirname = path.dirname(filename) if (!fs.existsSync(dirname)) fs.mkdirSync(dirname) fs.writeFileSync(filename, code) @@ -26,5 +26,15 @@ export default defineConfig({ alias: { '@': path.join(__dirname, 'src'), }, + extensions: [ + '.cjs', + '.mjs', + '.js', + '.mts', + '.ts', + '.jsx', + '.tsx', + '.json', + ], }, }) diff --git a/tsconfig.json b/tsconfig.json index 9f92da1..2aa8ac1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,15 @@ { "compilerOptions": { "target": "ES2019", - "module": "CommonJS", + "module": "ES2022", "esModuleInterop": true, "moduleResolution": "Node", "baseUrl": ".", "declaration": true, "outDir": "dist", "allowSyntheticDefaultImports": true, - "skipLibCheck": true + "skipLibCheck": true, + "strict": true }, "include": ["src/**/*.ts"] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..7a389b4 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,27 @@ +import path from 'path' +import { builtinModules } from 'module' +import { defineConfig } from 'vite' +import pkg from './package.json' + +export default defineConfig({ + build: { + minify: false, + outDir: '', + emptyOutDir: false, + target: 'node14', + lib: { + entry: path.join(__dirname, 'src/index.ts'), + formats: ['es', 'cjs'], + fileName: format => format === 'cjs' ? '[name].cjs' : '[name].js', + }, + rollupOptions: { + external: [ + ...builtinModules + .filter(m => !m.startsWith('_')) + .map(m => [m, `node:${m}`]) + .flat(), + ...Object.keys(pkg.dependencies), + ], + }, + }, +})