Skip to content

Commit b1e8848

Browse files
feat: importMetaPrelude. (#32)
1 parent a7cf267 commit b1e8848

7 files changed

Lines changed: 82 additions & 8 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ type ModuleOptions = {
130130
requireMainStrategy?: 'import-meta-main' | 'realpath'
131131
detectCircularRequires?: 'off' | 'warn' | 'error'
132132
requireSource?: 'builtin' | 'create-require'
133+
importMetaPrelude?: 'off' | 'auto' | 'on'
133134
cjsDefault?: 'module-exports' | 'auto' | 'none'
135+
idiomaticExports?: 'off' | 'safe' | 'aggressive'
134136
topLevelAwait?: 'error' | 'wrap' | 'preserve'
135137
out?: string
136138
inPlace?: boolean
@@ -149,11 +151,13 @@ type ModuleOptions = {
149151
- `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents.
150152
- `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old.
151153
- `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check.
154+
- `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.
152155
- `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw.
153156
- `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
154157
- `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.
155158
- `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
156159
- `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
160+
- `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.
157161
- `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
158162
- CommonJS → ESM lowering will throw on `with` statements and unshadowed `eval` calls to avoid unsound rewrites.
159163

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/module",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Bidirectional transform for ES modules and CommonJS.",
55
"type": "module",
66
"main": "dist/module.js",

src/format.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,8 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
809809
let requireMainNeedsRealpath = false
810810
let needsRequireResolveHelper = false
811811
const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require'
812+
const importMetaPreludeMode = opts.importMetaPrelude ?? 'auto'
813+
let importMetaRef = false
812814

813815
const shouldLowerCjs = opts.target === 'commonjs' && fullTransform
814816
const shouldRaiseEsm = opts.target === 'module' && fullTransform
@@ -884,6 +886,10 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
884886
: 'import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href'
885887
if (requireMainStrategy === 'realpath') {
886888
requireMainNeedsRealpath = true
889+
importMetaRef = true
890+
}
891+
if (requireMainStrategy === 'import-meta-main') {
892+
importMetaRef = true
887893
}
888894
code.update(node.start, node.end, negate ? `!(${mainExpr})` : mainExpr)
889895
return
@@ -1077,6 +1083,14 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
10771083
}
10781084

10791085
if (isIdentifierName(node)) {
1086+
if (
1087+
shouldRaiseEsm &&
1088+
node.type === 'Identifier' &&
1089+
(node.name === '__dirname' || node.name === '__filename')
1090+
) {
1091+
importMetaRef = true
1092+
}
1093+
10801094
identifier({
10811095
node,
10821096
ancestors,
@@ -1146,8 +1160,14 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
11461160
? `${exportsRename}.${name}`
11471161
: `${exportsRename}[${JSON.stringify(name)}]`
11481162
const exportValueFor = (name: string) => {
1149-
if (name === '__dirname') return 'import.meta.dirname'
1150-
if (name === '__filename') return 'import.meta.filename'
1163+
if (name === '__dirname') {
1164+
importMetaRef = true
1165+
return 'import.meta.dirname'
1166+
}
1167+
if (name === '__filename') {
1168+
importMetaRef = true
1169+
return 'import.meta.filename'
1170+
}
11511171
return name
11521172
}
11531173
const tempNameFor = (name: string) => {
@@ -1217,6 +1237,10 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
12171237
if (shouldRaiseEsm && fullTransform) {
12181238
const importPrelude: string[] = []
12191239

1240+
if (needsCreateRequire || needsRequireResolveHelper) {
1241+
importMetaRef = true
1242+
}
1243+
12201244
if (needsCreateRequire || needsRequireResolveHelper) {
12211245
importPrelude.push('import { createRequire } from "node:module";\n')
12221246
}
@@ -1270,10 +1294,15 @@ const format = async (src: string, ast: ParseResult, opts: FormatterOptions) =>
12701294

12711295
const prelude = `${importPrelude.join('')}${
12721296
importPrelude.length ? '\n' : ''
1273-
}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}void import.meta.filename;
1274-
`
1297+
}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}`
1298+
1299+
const importMetaTouch = (() => {
1300+
if (importMetaPreludeMode === 'on') return 'void import.meta.filename;\n'
1301+
if (importMetaPreludeMode === 'off') return ''
1302+
return importMetaRef ? 'void import.meta.filename;\n' : ''
1303+
})()
12751304

1276-
code.prepend(prelude)
1305+
code.prepend(`${prelude}${importMetaTouch}`)
12771306
}
12781307

12791308
if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) {

src/module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ const defaultOptions = {
224224
nestedRequireStrategy: 'create-require',
225225
cjsDefault: 'auto',
226226
idiomaticExports: 'safe',
227+
importMetaPrelude: 'auto',
227228
topLevelAwait: 'error',
228229
out: undefined,
229230
inPlace: false,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export type ModuleOptions = {
5757
cjsDefault?: 'module-exports' | 'auto' | 'none'
5858
/** Emit idiomatic exports when raising CJS to ESM. */
5959
idiomaticExports?: 'off' | 'safe' | 'aggressive'
60+
/** Control whether a no-op import.meta prelude is emitted. */
61+
importMetaPrelude?: 'off' | 'auto' | 'on'
6062
/** Handling for top-level await constructs. */
6163
topLevelAwait?: 'error' | 'wrap' | 'preserve'
6264
/** Optional diagnostics sink for warnings/errors emitted during transform. */

test/module.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,44 @@ describe('@knighted/module', () => {
12311231
assert.ok(diagnostics.some(d => d.code === 'idiomatic-exports-fallback'))
12321232
})
12331233

1234+
it('omits import.meta prelude in auto mode when not needed', async () => {
1235+
const fixturePath = join(fixtures, 'idiomaticSafe.cjs')
1236+
1237+
const result = await transform(fixturePath, { target: 'module' })
1238+
1239+
assert.equal(result.includes('void import.meta.filename;'), false)
1240+
})
1241+
1242+
it('emits import.meta prelude in auto mode when helpers use import.meta', async () => {
1243+
const fixturePath = join(fixtures, '__dirname.cjs')
1244+
1245+
const result = await transform(fixturePath, { target: 'module' })
1246+
1247+
assert.ok(result.includes('void import.meta.filename;'))
1248+
})
1249+
1250+
it('honors importMetaPrelude: off', async () => {
1251+
const fixturePath = join(fixtures, '__dirname.cjs')
1252+
1253+
const result = await transform(fixturePath, {
1254+
target: 'module',
1255+
importMetaPrelude: 'off',
1256+
})
1257+
1258+
assert.equal(result.includes('void import.meta.filename;'), false)
1259+
})
1260+
1261+
it('honors importMetaPrelude: on', async () => {
1262+
const fixturePath = join(fixtures, 'idiomaticSafe.cjs')
1263+
1264+
const result = await transform(fixturePath, {
1265+
target: 'module',
1266+
importMetaPrelude: 'on',
1267+
})
1268+
1269+
assert.ok(result.includes('void import.meta.filename;'))
1270+
})
1271+
12341272
it('emits diagnostics for CJS to ESM edge cases', async () => {
12351273
const fixturePath = join(fixtures, 'diagnostics.cjs')
12361274
const diagnostics: Array<{ code: string }> = []

0 commit comments

Comments
 (0)