diff --git a/README.md b/README.md index cb1f833..da261c1 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,9 @@ type ModuleOptions = { requireMainStrategy?: 'import-meta-main' | 'realpath' detectCircularRequires?: 'off' | 'warn' | 'error' requireSource?: 'builtin' | 'create-require' + importMetaPrelude?: 'off' | 'auto' | 'on' cjsDefault?: 'module-exports' | 'auto' | 'none' + idiomaticExports?: 'off' | 'safe' | 'aggressive' topLevelAwait?: 'error' | 'wrap' | 'preserve' out?: string inPlace?: boolean @@ -149,11 +151,13 @@ type ModuleOptions = { - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents. - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old. - `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check. +- `importMetaPrelude` (`auto`): emit a no-op `void import.meta.filename;` touch. `on` always emits; `off` never emits; `auto` emits only when helpers that reference `import.meta.*` are synthesized (e.g., `__dirname`/`__filename` in CJS→ESM, require-main shims, createRequire helpers). Useful for bundlers/transpilers that do usage-based `import.meta` polyfilling. - `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw. - `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output. - `rewriteSpecifier` (off): rewrite relative specifiers to a chosen extension or via a callback. Precedence: the callback (if provided) runs first; if it returns a string, that wins. If it returns `undefined` or `null`, the appenders still apply. - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`. - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`. +- `idiomaticExports` (`safe`): when raising CJS to ESM, attempt to synthesize `export` statements directly when it is safe. `off` always uses the helper bag; `aggressive` currently matches `safe` heuristics. - `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only. - CommonJS → ESM lowering will throw on `with` statements and unshadowed `eval` calls to avoid unsound rewrites. diff --git a/package-lock.json b/package-lock.json index d344e6d..128ae45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@knighted/module", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@knighted/module", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "magic-string": "^0.30.21", diff --git a/package.json b/package.json index ad577b6..abb74d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@knighted/module", - "version": "1.0.0", + "version": "1.1.0", "description": "Bidirectional transform for ES modules and CommonJS.", "type": "module", "main": "dist/module.js", diff --git a/src/format.ts b/src/format.ts index 30790cc..0972265 100644 --- a/src/format.ts +++ b/src/format.ts @@ -809,6 +809,8 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => let requireMainNeedsRealpath = false let needsRequireResolveHelper = false const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require' + const importMetaPreludeMode = opts.importMetaPrelude ?? 'auto' + let importMetaRef = false const shouldLowerCjs = opts.target === 'commonjs' && fullTransform const shouldRaiseEsm = opts.target === 'module' && fullTransform @@ -884,6 +886,10 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => : 'import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href' if (requireMainStrategy === 'realpath') { requireMainNeedsRealpath = true + importMetaRef = true + } + if (requireMainStrategy === 'import-meta-main') { + importMetaRef = true } code.update(node.start, node.end, negate ? `!(${mainExpr})` : mainExpr) return @@ -1077,6 +1083,14 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => } if (isIdentifierName(node)) { + if ( + shouldRaiseEsm && + node.type === 'Identifier' && + (node.name === '__dirname' || node.name === '__filename') + ) { + importMetaRef = true + } + identifier({ node, ancestors, @@ -1146,8 +1160,14 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => ? `${exportsRename}.${name}` : `${exportsRename}[${JSON.stringify(name)}]` const exportValueFor = (name: string) => { - if (name === '__dirname') return 'import.meta.dirname' - if (name === '__filename') return 'import.meta.filename' + if (name === '__dirname') { + importMetaRef = true + return 'import.meta.dirname' + } + if (name === '__filename') { + importMetaRef = true + return 'import.meta.filename' + } return name } const tempNameFor = (name: string) => { @@ -1217,6 +1237,10 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => if (shouldRaiseEsm && fullTransform) { const importPrelude: string[] = [] + if (needsCreateRequire || needsRequireResolveHelper) { + importMetaRef = true + } + if (needsCreateRequire || needsRequireResolveHelper) { importPrelude.push('import { createRequire } from "node:module";\n') } @@ -1270,10 +1294,15 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) => const prelude = `${importPrelude.join('')}${ importPrelude.length ? '\n' : '' - }${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}void import.meta.filename; -` + }${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}` + + const importMetaTouch = (() => { + if (importMetaPreludeMode === 'on') return 'void import.meta.filename;\n' + if (importMetaPreludeMode === 'off') return '' + return importMetaRef ? 'void import.meta.filename;\n' : '' + })() - code.prepend(prelude) + code.prepend(`${prelude}${importMetaTouch}`) } if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) { diff --git a/src/module.ts b/src/module.ts index 663046d..f87a0c2 100644 --- a/src/module.ts +++ b/src/module.ts @@ -224,6 +224,7 @@ const defaultOptions = { nestedRequireStrategy: 'create-require', cjsDefault: 'auto', idiomaticExports: 'safe', + importMetaPrelude: 'auto', topLevelAwait: 'error', out: undefined, inPlace: false, diff --git a/src/types.ts b/src/types.ts index 43cba1c..4de2bd0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,8 @@ export type ModuleOptions = { cjsDefault?: 'module-exports' | 'auto' | 'none' /** Emit idiomatic exports when raising CJS to ESM. */ idiomaticExports?: 'off' | 'safe' | 'aggressive' + /** Control whether a no-op import.meta prelude is emitted. */ + importMetaPrelude?: 'off' | 'auto' | 'on' /** Handling for top-level await constructs. */ topLevelAwait?: 'error' | 'wrap' | 'preserve' /** Optional diagnostics sink for warnings/errors emitted during transform. */ diff --git a/test/module.ts b/test/module.ts index 38de420..949ec7a 100644 --- a/test/module.ts +++ b/test/module.ts @@ -1231,6 +1231,44 @@ describe('@knighted/module', () => { assert.ok(diagnostics.some(d => d.code === 'idiomatic-exports-fallback')) }) + it('omits import.meta prelude in auto mode when not needed', async () => { + const fixturePath = join(fixtures, 'idiomaticSafe.cjs') + + const result = await transform(fixturePath, { target: 'module' }) + + assert.equal(result.includes('void import.meta.filename;'), false) + }) + + it('emits import.meta prelude in auto mode when helpers use import.meta', async () => { + const fixturePath = join(fixtures, '__dirname.cjs') + + const result = await transform(fixturePath, { target: 'module' }) + + assert.ok(result.includes('void import.meta.filename;')) + }) + + it('honors importMetaPrelude: off', async () => { + const fixturePath = join(fixtures, '__dirname.cjs') + + const result = await transform(fixturePath, { + target: 'module', + importMetaPrelude: 'off', + }) + + assert.equal(result.includes('void import.meta.filename;'), false) + }) + + it('honors importMetaPrelude: on', async () => { + const fixturePath = join(fixtures, 'idiomaticSafe.cjs') + + const result = await transform(fixturePath, { + target: 'module', + importMetaPrelude: 'on', + }) + + assert.ok(result.includes('void import.meta.filename;')) + }) + it('emits diagnostics for CJS to ESM edge cases', async () => { const fixturePath = join(fixtures, 'diagnostics.cjs') const diagnostics: Array<{ code: string }> = []