Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
39 changes: 34 additions & 5 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const defaultOptions = {
nestedRequireStrategy: 'create-require',
cjsDefault: 'auto',
idiomaticExports: 'safe',
importMetaPrelude: 'auto',
topLevelAwait: 'error',
out: undefined,
inPlace: false,
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
38 changes: 38 additions & 0 deletions test/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }> = []
Expand Down