@knighted/module 1.0.0-beta.1 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,12 @@ Node.js utility for transforming a JavaScript or TypeScript file from an ES modu
9
9
  - ES module ➡️ CommonJS
10
10
  - CommonJS ➡️ ES module
11
11
 
12
+ Highlights
13
+
14
+ - Defaults to safe CommonJS output: strict live bindings, import.meta shims, and specifier preservation.
15
+ - Opt into stricter/looser behaviors: live binding enforcement, import.meta.main gating, and top-level await strategies.
16
+ - Can optionally rewrite relative specifiers and write transformed output to disk.
17
+
12
18
  > [!IMPORTANT]
13
19
  > All parsing logic is applied under the assumption the code is in [strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) which [modules run under by default](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_classic_scripts).
14
20
 
@@ -18,9 +24,15 @@ By default `@knighted/module` transforms the one-to-one [differences between ES
18
24
 
19
25
  - Node >= 20.11.0
20
26
 
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install @knighted/module
31
+ ```
32
+
21
33
  ## Example
22
34
 
23
- Given an ES module
35
+ Given an ES module:
24
36
 
25
37
  **file.js**
26
38
 
@@ -40,7 +52,7 @@ const detectCalledFromCli = async path => {
40
52
  detectCalledFromCli(argv[1])
41
53
  ```
42
54
 
43
- You can transform it to the equivalent CommonJS module
55
+ Transform it to CommonJS:
44
56
 
45
57
  ```js
46
58
  import { transform } from '@knighted/module'
@@ -51,7 +63,7 @@ await transform('./file.js', {
51
63
  })
52
64
  ```
53
65
 
54
- Which produces
66
+ Which produces:
55
67
 
56
68
  **file.cjs**
57
69
 
@@ -99,6 +111,7 @@ type ModuleOptions = {
99
111
  | ((value: string) => string | null | undefined)
100
112
  dirFilename?: 'inject' | 'preserve' | 'error'
101
113
  importMeta?: 'preserve' | 'shim' | 'error'
114
+ importMetaMain?: 'shim' | 'warn' | 'error'
102
115
  requireSource?: 'builtin' | 'create-require'
103
116
  cjsDefault?: 'module-exports' | 'auto' | 'none'
104
117
  topLevelAwait?: 'error' | 'wrap' | 'preserve'
@@ -107,7 +120,24 @@ type ModuleOptions = {
107
120
  }
108
121
  ```
109
122
 
123
+ Behavior notes (defaults in parentheses)
124
+
125
+ - `target` (`commonjs`): output module system.
126
+ - `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass.
127
+ - `liveBindings` (`strict`): getter-based live bindings, or snapshot (`loose`/`off`).
128
+ - `dirFilename` (`inject`): inject `__dirname`/`__filename`, preserve existing, or throw.
129
+ - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents.
130
+ - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old.
131
+ - `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
132
+ - `rewriteSpecifier` (off): rewrite relative specifiers to a chosen extension or via a callback.
133
+ - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
134
+ - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
135
+ - `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
136
+
137
+ See [docs/esm-to-cjs.md](docs/esm-to-cjs.md) for deeper notes on live bindings, interop helpers, top-level await behavior, and `import.meta.main` handling. For CommonJS to ESM lowering details, read [docs/cjs-to-esm.md](docs/cjs-to-esm.md).
138
+
110
139
  ## Roadmap
111
140
 
112
141
  - Remove `@knighted/specifier` and avoid double parsing.
113
- - Flesh out live-binding and top-level await handling.
142
+ - Emit source maps and clearer diagnostics for transform choices.
143
+ - Broaden fixtures covering live-binding and top-level await edge cases across Node versions.
@@ -15,6 +15,313 @@ var _identifiers = require("#utils/identifiers.js");
15
15
  var _identifier2 = require("#helpers/identifier.js");
16
16
  var _walk = require("#walk");
17
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
+ const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
19
+ const exportAssignment = (name, expr, live) => {
20
+ const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
21
+ if (live === 'strict') {
22
+ const key = JSON.stringify(name);
23
+ return `Object.defineProperty(exports, ${key}, { enumerable: true, get: () => ${expr} });`;
24
+ }
25
+ return `exports${prop} = ${expr};`;
26
+ };
27
+ const defaultInteropName = '__interopDefault';
28
+ const interopHelper = `const ${defaultInteropName} = mod => (mod && mod.__esModule ? mod.default : mod);\n`;
29
+ const isRequireCallee = (callee, shadowed) => {
30
+ if (callee.type === 'Identifier' && callee.name === 'require' && !shadowed.has('require')) {
31
+ return true;
32
+ }
33
+ if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'module' && !shadowed.has('module') && callee.property.type === 'Identifier' && callee.property.name === 'require') {
34
+ return true;
35
+ }
36
+ return false;
37
+ };
38
+ const isStaticRequire = (node, shadowed) => node.type === 'CallExpression' && isRequireCallee(node.callee, shadowed) && node.arguments.length === 1 && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'string';
39
+ const isRequireCall = (node, shadowed) => node.type === 'CallExpression' && isRequireCallee(node.callee, shadowed);
40
+ const lowerCjsRequireToImports = (program, code, shadowed) => {
41
+ const transforms = [];
42
+ const imports = [];
43
+ let nsIndex = 0;
44
+ let needsCreateRequire = false;
45
+ for (const stmt of program.body) {
46
+ if (stmt.type === 'VariableDeclaration') {
47
+ const decls = stmt.declarations;
48
+ const allStatic = decls.length > 0 && decls.every(decl => decl.init && isStaticRequire(decl.init, shadowed));
49
+ if (allStatic) {
50
+ for (const decl of decls) {
51
+ const init = decl.init;
52
+ const source = code.slice(init.arguments[0].start, init.arguments[0].end);
53
+ if (decl.id.type === 'Identifier') {
54
+ imports.push(`import * as ${decl.id.name} from ${source};\n`);
55
+ } else if (decl.id.type === 'ObjectPattern') {
56
+ const ns = `__cjsImport${nsIndex++}`;
57
+ const pattern = code.slice(decl.id.start, decl.id.end);
58
+ imports.push(`import * as ${ns} from ${source};\n`);
59
+ imports.push(`const ${pattern} = ${ns};\n`);
60
+ } else {
61
+ needsCreateRequire = true;
62
+ }
63
+ }
64
+ transforms.push({
65
+ start: stmt.start,
66
+ end: stmt.end,
67
+ code: ';\n'
68
+ });
69
+ continue;
70
+ }
71
+ for (const decl of decls) {
72
+ const init = decl.init;
73
+ if (init && isRequireCall(init, shadowed)) {
74
+ needsCreateRequire = true;
75
+ }
76
+ }
77
+ }
78
+ if (stmt.type === 'ExpressionStatement') {
79
+ const expr = stmt.expression;
80
+ if (expr && isStaticRequire(expr, shadowed)) {
81
+ const source = code.slice(expr.arguments[0].start, expr.arguments[0].end);
82
+ imports.push(`import ${source};\n`);
83
+ transforms.push({
84
+ start: stmt.start,
85
+ end: stmt.end,
86
+ code: ';\n'
87
+ });
88
+ continue;
89
+ }
90
+ if (expr && isRequireCall(expr, shadowed)) {
91
+ needsCreateRequire = true;
92
+ }
93
+ }
94
+ }
95
+ return {
96
+ transforms,
97
+ imports,
98
+ needsCreateRequire
99
+ };
100
+ };
101
+ const isRequireMainMember = (node, shadowed) => node && node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'require' && !shadowed.has('require') && node.property.type === 'Identifier' && node.property.name === 'main';
102
+ const hasTopLevelAwait = program => {
103
+ let found = false;
104
+ const walkNode = (node, inFunction) => {
105
+ if (found) return;
106
+ switch (node.type) {
107
+ case 'FunctionDeclaration':
108
+ case 'FunctionExpression':
109
+ case 'ArrowFunctionExpression':
110
+ case 'ClassDeclaration':
111
+ case 'ClassExpression':
112
+ inFunction = true;
113
+ break;
114
+ }
115
+ if (!inFunction && node.type === 'AwaitExpression') {
116
+ found = true;
117
+ return;
118
+ }
119
+ const keys = Object.keys(node);
120
+ for (const key of keys) {
121
+ const value = node[key];
122
+ if (!value) continue;
123
+ if (Array.isArray(value)) {
124
+ for (const item of value) {
125
+ if (item && typeof item === 'object') {
126
+ walkNode(item, inFunction);
127
+ if (found) return;
128
+ }
129
+ }
130
+ } else if (value && typeof value === 'object') {
131
+ walkNode(value, inFunction);
132
+ if (found) return;
133
+ }
134
+ }
135
+ };
136
+ walkNode(program, false);
137
+ return found;
138
+ };
139
+ const lowerEsmToCjs = (program, code, opts, containsTopLevelAwait) => {
140
+ const live = opts.liveBindings ?? 'strict';
141
+ const importTransforms = [];
142
+ const exportTransforms = [];
143
+ let needsInterop = false;
144
+ let importIndex = 0;
145
+ for (const node of program.body) {
146
+ if (node.type === 'ImportDeclaration') {
147
+ const srcLiteral = code.slice(node.source.start, node.source.end);
148
+ const specifiers = node.specifiers ?? [];
149
+ const defaultSpec = specifiers.find(s => s.type === 'ImportDefaultSpecifier');
150
+ const namespaceSpec = specifiers.find(s => s.type === 'ImportNamespaceSpecifier');
151
+ const namedSpecs = specifiers.filter(s => s.type === 'ImportSpecifier');
152
+
153
+ // Side-effect import
154
+ if (!specifiers.length) {
155
+ importTransforms.push({
156
+ start: node.start,
157
+ end: node.end,
158
+ code: `require(${srcLiteral});\n`,
159
+ needsInterop: false
160
+ });
161
+ continue;
162
+ }
163
+ const modIdent = `__mod${importIndex++}`;
164
+ const lines = [];
165
+ lines.push(`const ${modIdent} = require(${srcLiteral});`);
166
+ if (namespaceSpec) {
167
+ lines.push(`const ${namespaceSpec.local.name} = ${modIdent};`);
168
+ }
169
+ if (defaultSpec) {
170
+ let init = modIdent;
171
+ switch (opts.cjsDefault) {
172
+ case 'module-exports':
173
+ init = modIdent;
174
+ break;
175
+ case 'none':
176
+ init = `${modIdent}.default`;
177
+ break;
178
+ case 'auto':
179
+ default:
180
+ init = `${defaultInteropName}(${modIdent})`;
181
+ needsInterop = true;
182
+ break;
183
+ }
184
+ lines.push(`const ${defaultSpec.local.name} = ${init};`);
185
+ }
186
+ if (namedSpecs.length) {
187
+ const pairs = namedSpecs.map(s => {
188
+ const imported = s.imported.name;
189
+ const local = s.local.name;
190
+ return imported === local ? imported : `${imported}: ${local}`;
191
+ });
192
+ lines.push(`const { ${pairs.join(', ')} } = ${modIdent};`);
193
+ }
194
+ importTransforms.push({
195
+ start: node.start,
196
+ end: node.end,
197
+ code: `${lines.join('\n')}\n`,
198
+ needsInterop
199
+ });
200
+ }
201
+ if (node.type === 'ExportNamedDeclaration') {
202
+ // Handle declaration exports
203
+ if (node.declaration) {
204
+ const decl = node.declaration;
205
+ const declSrc = code.slice(decl.start, decl.end);
206
+ const exportedNames = [];
207
+ if (decl.type === 'VariableDeclaration') {
208
+ for (const d of decl.declarations) {
209
+ if (d.id.type === 'Identifier') {
210
+ exportedNames.push(d.id.name);
211
+ }
212
+ }
213
+ } else if (decl.id?.type === 'Identifier') {
214
+ exportedNames.push(decl.id.name);
215
+ }
216
+ const exportLines = exportedNames.map(name => exportAssignment(name, name, live));
217
+ exportTransforms.push({
218
+ start: node.start,
219
+ end: node.end,
220
+ code: `${declSrc}\n${exportLines.join('\n')}\n`
221
+ });
222
+ continue;
223
+ }
224
+
225
+ // Handle re-export or local specifiers
226
+ if (node.specifiers?.length) {
227
+ if (node.source) {
228
+ const srcLiteral = code.slice(node.source.start, node.source.end);
229
+ const modIdent = `__mod${importIndex++}`;
230
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
231
+ for (const spec of node.specifiers) {
232
+ if (spec.type !== 'ExportSpecifier') continue;
233
+ const exported = spec.exported.name;
234
+ const imported = spec.local.name;
235
+ let rhs = `${modIdent}.${imported}`;
236
+ if (imported === 'default') {
237
+ rhs = `${defaultInteropName}(${modIdent})`;
238
+ needsInterop = true;
239
+ }
240
+ lines.push(exportAssignment(exported, rhs, live));
241
+ }
242
+ exportTransforms.push({
243
+ start: node.start,
244
+ end: node.end,
245
+ code: `${lines.join('\n')}\n`,
246
+ needsInterop
247
+ });
248
+ } else {
249
+ const lines = [];
250
+ for (const spec of node.specifiers) {
251
+ if (spec.type !== 'ExportSpecifier') continue;
252
+ const exported = spec.exported.name;
253
+ const local = spec.local.name;
254
+ lines.push(exportAssignment(exported, local, live));
255
+ }
256
+ exportTransforms.push({
257
+ start: node.start,
258
+ end: node.end,
259
+ code: `${lines.join('\n')}\n`
260
+ });
261
+ }
262
+ }
263
+ }
264
+ if (node.type === 'ExportDefaultDeclaration') {
265
+ const decl = node.declaration;
266
+ const useExportsObject = containsTopLevelAwait && opts.topLevelAwait !== 'error';
267
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
268
+ if (decl.id?.name) {
269
+ const declSrc = code.slice(decl.start, decl.end);
270
+ const assign = useExportsObject ? `exports.default = ${decl.id.name};` : `module.exports = ${decl.id.name};`;
271
+ exportTransforms.push({
272
+ start: node.start,
273
+ end: node.end,
274
+ code: `${declSrc}\n${assign}\n`
275
+ });
276
+ } else {
277
+ const declSrc = code.slice(decl.start, decl.end);
278
+ const assign = useExportsObject ? `exports.default = ${declSrc};` : `module.exports = ${declSrc};`;
279
+ exportTransforms.push({
280
+ start: node.start,
281
+ end: node.end,
282
+ code: `${assign}\n`
283
+ });
284
+ }
285
+ } else {
286
+ const exprSrc = code.slice(decl.start, decl.end);
287
+ const assign = useExportsObject ? `exports.default = ${exprSrc};` : `module.exports = ${exprSrc};`;
288
+ exportTransforms.push({
289
+ start: node.start,
290
+ end: node.end,
291
+ code: `${assign}\n`
292
+ });
293
+ }
294
+ }
295
+ if (node.type === 'ExportAllDeclaration') {
296
+ const srcLiteral = code.slice(node.source.start, node.source.end);
297
+ if (node.exported) {
298
+ const exported = node.exported.name;
299
+ const modIdent = `__mod${importIndex++}`;
300
+ const lines = [`const ${modIdent} = require(${srcLiteral});`, exportAssignment(exported, modIdent, live)];
301
+ exportTransforms.push({
302
+ start: node.start,
303
+ end: node.end,
304
+ code: `${lines.join('\n')}\n`
305
+ });
306
+ } else {
307
+ const modIdent = `__mod${importIndex++}`;
308
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
309
+ const loop = `for (const k in ${modIdent}) {\n if (k === 'default') continue;\n if (!Object.prototype.hasOwnProperty.call(${modIdent}, k)) continue;\n Object.defineProperty(exports, k, { enumerable: true, get: () => ${modIdent}[k] });\n}`;
310
+ lines.push(loop);
311
+ exportTransforms.push({
312
+ start: node.start,
313
+ end: node.end,
314
+ code: `${lines.join('\n')}\n`
315
+ });
316
+ }
317
+ }
318
+ }
319
+ return {
320
+ importTransforms,
321
+ exportTransforms,
322
+ needsInterop
323
+ };
324
+ };
18
325
  /**
19
326
  * Node added support for import.meta.main.
20
327
  * Added in: v24.2.0, v22.18.0
@@ -28,21 +335,67 @@ const format = async (src, ast, opts) => {
28
335
  hasDefaultExportBeenReassigned: false,
29
336
  hasDefaultExportBeenAssigned: false
30
337
  };
31
- const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
32
- await (0, _identifiers.collectModuleIdentifiers)(ast.program);
338
+ const moduleIdentifiers = await (0, _identifiers.collectModuleIdentifiers)(ast.program);
339
+ const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
33
340
  if (opts.target === 'module' && opts.transformSyntax) {
34
- /**
35
- * Prepare ESM output by renaming `exports` to `__exports` and seeding an
36
- * `import.meta.filename` touch so import.meta is present even when the
37
- * original source never referenced it.
38
- */
39
- code.prepend(`let ${_exports.exportsRename} = {};
40
- void import.meta.filename;
41
- `);
341
+ if (shadowedBindings.has('module') || shadowedBindings.has('exports')) {
342
+ throw new Error('Cannot transform to ESM: module or exports is shadowed in module scope.');
343
+ }
344
+ }
345
+ const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
346
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
347
+ const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
348
+ const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
349
+ const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
350
+ let hoistedImports = [];
351
+ let pendingRequireTransforms = [];
352
+ let needsCreateRequire = false;
353
+ let pendingCjsTransforms = null;
354
+ if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
355
+ throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
356
+ }
357
+ if (shouldRaiseEsm) {
358
+ const {
359
+ transforms,
360
+ imports,
361
+ needsCreateRequire: reqCreate
362
+ } = lowerCjsRequireToImports(ast.program, code, shadowedBindings);
363
+ pendingRequireTransforms = transforms;
364
+ hoistedImports = imports;
365
+ needsCreateRequire = reqCreate;
42
366
  }
43
367
  await (0, _walk.ancestorWalk)(ast.program, {
44
368
  async enter(node, ancestors) {
45
369
  const parent = ancestors[ancestors.length - 2] ?? null;
370
+ if (shouldRaiseEsm && node.type === 'BinaryExpression') {
371
+ const op = node.operator;
372
+ const isEquality = op === '===' || op === '==' || op === '!==' || op === '!=';
373
+ if (isEquality) {
374
+ const leftMain = isRequireMainMember(node.left, shadowedBindings);
375
+ const rightMain = isRequireMainMember(node.right, shadowedBindings);
376
+ const leftModule = node.left.type === 'Identifier' && node.left.name === 'module' && !shadowedBindings.has('module');
377
+ const rightModule = node.right.type === 'Identifier' && node.right.name === 'module' && !shadowedBindings.has('module');
378
+ if (leftMain && rightModule || rightMain && leftModule) {
379
+ const negate = op === '!==' || op === '!=';
380
+ code.update(node.start, node.end, negate ? '!import.meta.main' : 'import.meta.main');
381
+ return;
382
+ }
383
+ }
384
+ }
385
+ if (shouldRaiseEsm && node.type === 'CallExpression' && isRequireCall(node, shadowedBindings)) {
386
+ const isStatic = isStaticRequire(node, shadowedBindings);
387
+ const parent = ancestors[ancestors.length - 2] ?? null;
388
+ const grandparent = ancestors[ancestors.length - 3] ?? null;
389
+ const greatGrandparent = ancestors[ancestors.length - 4] ?? null;
390
+
391
+ // Hoistable cases are handled separately and don't need createRequire.
392
+ const topLevelExprStmt = parent?.type === 'ExpressionStatement' && grandparent?.type === 'Program';
393
+ const topLevelVarDecl = parent?.type === 'VariableDeclarator' && grandparent?.type === 'VariableDeclaration' && greatGrandparent?.type === 'Program';
394
+ const hoistableTopLevel = isStatic && (topLevelExprStmt || topLevelVarDecl);
395
+ if (!isStatic || !hoistableTopLevel) {
396
+ needsCreateRequire = true;
397
+ }
398
+ }
46
399
  if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
47
400
  const skipped = ['__filename', '__dirname'];
48
401
  const skippedParams = node.params.filter(param => param.type === 'Identifier' && skipped.includes(param.name));
@@ -101,7 +454,7 @@ void import.meta.filename;
101
454
  (0, _metaProperty.metaProperty)(node, parent, code, opts);
102
455
  }
103
456
  if (node.type === 'MemberExpression') {
104
- (0, _memberExpression.memberExpression)(node, parent, code, opts);
457
+ (0, _memberExpression.memberExpression)(node, parent, code, opts, shadowedBindings);
105
458
  }
106
459
  if ((0, _identifier2.isIdentifierName)(node)) {
107
460
  (0, _identifier.identifier)({
@@ -109,11 +462,36 @@ void import.meta.filename;
109
462
  ancestors,
110
463
  code,
111
464
  opts,
112
- meta: exportsMeta
465
+ meta: exportsMeta,
466
+ shadowed: shadowedBindings
113
467
  });
114
468
  }
115
469
  }
116
470
  });
471
+ if (pendingRequireTransforms.length) {
472
+ for (const t of pendingRequireTransforms) {
473
+ code.overwrite(t.start, t.end, t.code);
474
+ }
475
+ }
476
+ if (shouldLowerCjs) {
477
+ const {
478
+ importTransforms,
479
+ exportTransforms,
480
+ needsInterop
481
+ } = lowerEsmToCjs(ast.program, code, opts, containsTopLevelAwait);
482
+ pendingCjsTransforms = {
483
+ transforms: [...importTransforms, ...exportTransforms].sort((a, b) => a.start - b.start),
484
+ needsInterop
485
+ };
486
+ }
487
+ if (pendingCjsTransforms) {
488
+ for (const t of pendingCjsTransforms.transforms) {
489
+ code.overwrite(t.start, t.end, t.code);
490
+ }
491
+ if (pendingCjsTransforms.needsInterop) {
492
+ code.prepend(`${interopHelper}exports.__esModule = true;\n`);
493
+ }
494
+ }
117
495
  if (opts.target === 'module' && opts.transformSyntax && exportTable) {
118
496
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
119
497
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
@@ -143,6 +521,30 @@ void import.meta.filename;
143
521
  code.append(`\n${lines.join('\n')}\n`);
144
522
  }
145
523
  }
524
+ if (shouldRaiseEsm && opts.transformSyntax) {
525
+ const importPrelude = [];
526
+ if (needsCreateRequire) {
527
+ importPrelude.push('import { createRequire } from "node:module";\n');
528
+ }
529
+ if (hoistedImports.length) {
530
+ importPrelude.push(...hoistedImports);
531
+ }
532
+ const requireInit = needsCreateRequire ? 'const require = createRequire(import.meta.url);\n' : '';
533
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${requireInit}let ${_exports.exportsRename} = {};
534
+ void import.meta.filename;
535
+ `;
536
+ code.prepend(prelude);
537
+ }
538
+ if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
539
+ const body = code.toString();
540
+ if (opts.topLevelAwait === 'wrap') {
541
+ const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
542
+ const setPromise = `const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== 'object' && type !== 'function') return;\n target.__tla = __tla;\n};\n`;
543
+ const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
544
+ return `${tlaPromise}${setPromise}${attach}`;
545
+ }
546
+ return `;(async () => {\n${body}\n})();\n`;
547
+ }
146
548
  return code.toString();
147
549
  };
148
550
  exports.format = format;
@@ -11,7 +11,8 @@ const identifier = ({
11
11
  ancestors,
12
12
  code,
13
13
  opts,
14
- meta
14
+ meta,
15
+ shadowed
15
16
  }) => {
16
17
  if (opts.target === 'module') {
17
18
  const {
@@ -19,6 +20,9 @@ const identifier = ({
19
20
  end,
20
21
  name
21
22
  } = node;
23
+ if (shadowed?.has(name)) {
24
+ return;
25
+ }
22
26
  switch (name) {
23
27
  case '__filename':
24
28
  code.update(start, end, 'import.meta.url');
@@ -5,8 +5,11 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.memberExpression = void 0;
7
7
  var _exports = require("#utils/exports.js");
8
- const memberExpression = (node, parent, src, options) => {
8
+ const memberExpression = (node, parent, src, options, shadowed) => {
9
9
  if (options.target === 'module') {
10
+ if (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name)) {
11
+ return;
12
+ }
10
13
  if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
11
14
  src.update(node.start, node.end, _exports.exportsRename);
12
15
  return;
@@ -23,18 +26,10 @@ const memberExpression = (node, parent, src, options) => {
23
26
  // CommonJS properties of `require`
24
27
  switch (name) {
25
28
  case 'main':
26
- /**
27
- * Node.js team still quibbling over import.meta.main ¯\_(ツ)_/¯
28
- * @see https://github.com/nodejs/node/pull/32223
29
- */
30
- if (parent?.type === 'ExpressionStatement') {
31
- // This is a standalone expression so remove it to not cause run-time errors.
32
- src.remove(start, end);
29
+ if (parent?.type === 'BinaryExpression') {
30
+ return;
33
31
  }
34
- /**
35
- * Transform require.main === module.
36
- */
37
- if (parent?.type === 'BinaryExpression') {}
32
+ src.update(start, end, 'import.meta.main');
38
33
  break;
39
34
  case 'resolve':
40
35
  src.update(start, end, 'import.meta.resolve');
@@ -48,6 +43,11 @@ const memberExpression = (node, parent, src, options) => {
48
43
  break;
49
44
  }
50
45
  }
46
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'require') {
47
+ if (!shadowed?.has('module')) {
48
+ src.update(node.start, node.end, 'require');
49
+ }
50
+ }
51
51
  }
52
52
  };
53
53
  exports.memberExpression = memberExpression;
@@ -4,6 +4,19 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.metaProperty = void 0;
7
+ const importMetaMainSupport = '(() => { const [__nmaj, __nmin] = process.versions.node.split(".").map(n => parseInt(n, 10) || 0); return (__nmaj > 24 || (__nmaj === 24 && __nmin >= 2) || (__nmaj === 22 && __nmin >= 18)); })()';
8
+ const importMetaMainShim = 'process.argv[1] === __filename';
9
+ const importMetaMainExpr = mode => {
10
+ switch (mode) {
11
+ case 'warn':
12
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (console.warn("import.meta.main is not supported before Node 22.18/24.2; falling back to shim."), ${importMetaMainShim}))`;
13
+ case 'error':
14
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (() => { throw new Error("import.meta.main is not supported before Node 22.18/24.2"); })())`;
15
+ case 'shim':
16
+ default:
17
+ return importMetaMainShim;
18
+ }
19
+ };
7
20
  const metaProperty = (node, parent, src, options) => {
8
21
  if (options.target === 'commonjs') {
9
22
  if (parent?.type !== 'MemberExpression') {
@@ -27,10 +40,15 @@ const metaProperty = (node, parent, src, options) => {
27
40
  break;
28
41
  case 'resolve':
29
42
  /**
30
- * Should this be `require('node:url').pathToFileURL(require.resolve(<parsed specifier>)).href`?
43
+ * Map to require.resolve intentionally: matches CJS resolution semantics.
44
+ * Wrapping in pathToFileURL(...) would change the return shape (URL string)
45
+ * without truly emulating ESM import.meta.resolve rules.
31
46
  */
32
47
  src.update(parent.start, parent.end, 'require.resolve');
33
48
  break;
49
+ case 'main':
50
+ src.update(parent.start, parent.end, importMetaMainExpr(options.importMetaMain));
51
+ break;
34
52
  }
35
53
  }
36
54
  }
@@ -1,4 +1,4 @@
1
1
  import MagicString from 'magic-string';
2
2
  import type { MemberExpression, Node } from 'oxc-parser';
3
3
  import type { FormatterOptions } from '../types.cjs';
4
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions) => void;
4
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>) => void;
@@ -18,6 +18,7 @@ const defaultOptions = {
18
18
  rewriteSpecifier: undefined,
19
19
  dirFilename: 'inject',
20
20
  importMeta: 'shim',
21
+ importMetaMain: 'shim',
21
22
  requireSource: 'builtin',
22
23
  cjsDefault: 'auto',
23
24
  topLevelAwait: 'error',
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/dist/format.js CHANGED
@@ -8,7 +8,313 @@ import { exportsRename, collectCjsExports } from '#utils/exports.js';
8
8
  import { collectModuleIdentifiers } from '#utils/identifiers.js';
9
9
  import { isIdentifierName } from '#helpers/identifier.js';
10
10
  import { ancestorWalk } from '#walk';
11
+ const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
12
+ const exportAssignment = (name, expr, live) => {
13
+ const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
14
+ if (live === 'strict') {
15
+ const key = JSON.stringify(name);
16
+ return `Object.defineProperty(exports, ${key}, { enumerable: true, get: () => ${expr} });`;
17
+ }
18
+ return `exports${prop} = ${expr};`;
19
+ };
20
+ const defaultInteropName = '__interopDefault';
21
+ const interopHelper = `const ${defaultInteropName} = mod => (mod && mod.__esModule ? mod.default : mod);\n`;
22
+ const isRequireCallee = (callee, shadowed) => {
23
+ if (callee.type === 'Identifier' && callee.name === 'require' && !shadowed.has('require')) {
24
+ return true;
25
+ }
26
+ if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier' && callee.object.name === 'module' && !shadowed.has('module') && callee.property.type === 'Identifier' && callee.property.name === 'require') {
27
+ return true;
28
+ }
29
+ return false;
30
+ };
31
+ const isStaticRequire = (node, shadowed) => node.type === 'CallExpression' && isRequireCallee(node.callee, shadowed) && node.arguments.length === 1 && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'string';
32
+ const isRequireCall = (node, shadowed) => node.type === 'CallExpression' && isRequireCallee(node.callee, shadowed);
33
+ const lowerCjsRequireToImports = (program, code, shadowed) => {
34
+ const transforms = [];
35
+ const imports = [];
36
+ let nsIndex = 0;
37
+ let needsCreateRequire = false;
38
+ for (const stmt of program.body) {
39
+ if (stmt.type === 'VariableDeclaration') {
40
+ const decls = stmt.declarations;
41
+ const allStatic = decls.length > 0 && decls.every(decl => decl.init && isStaticRequire(decl.init, shadowed));
42
+ if (allStatic) {
43
+ for (const decl of decls) {
44
+ const init = decl.init;
45
+ const source = code.slice(init.arguments[0].start, init.arguments[0].end);
46
+ if (decl.id.type === 'Identifier') {
47
+ imports.push(`import * as ${decl.id.name} from ${source};\n`);
48
+ } else if (decl.id.type === 'ObjectPattern') {
49
+ const ns = `__cjsImport${nsIndex++}`;
50
+ const pattern = code.slice(decl.id.start, decl.id.end);
51
+ imports.push(`import * as ${ns} from ${source};\n`);
52
+ imports.push(`const ${pattern} = ${ns};\n`);
53
+ } else {
54
+ needsCreateRequire = true;
55
+ }
56
+ }
57
+ transforms.push({
58
+ start: stmt.start,
59
+ end: stmt.end,
60
+ code: ';\n'
61
+ });
62
+ continue;
63
+ }
64
+ for (const decl of decls) {
65
+ const init = decl.init;
66
+ if (init && isRequireCall(init, shadowed)) {
67
+ needsCreateRequire = true;
68
+ }
69
+ }
70
+ }
71
+ if (stmt.type === 'ExpressionStatement') {
72
+ const expr = stmt.expression;
73
+ if (expr && isStaticRequire(expr, shadowed)) {
74
+ const source = code.slice(expr.arguments[0].start, expr.arguments[0].end);
75
+ imports.push(`import ${source};\n`);
76
+ transforms.push({
77
+ start: stmt.start,
78
+ end: stmt.end,
79
+ code: ';\n'
80
+ });
81
+ continue;
82
+ }
83
+ if (expr && isRequireCall(expr, shadowed)) {
84
+ needsCreateRequire = true;
85
+ }
86
+ }
87
+ }
88
+ return {
89
+ transforms,
90
+ imports,
91
+ needsCreateRequire
92
+ };
93
+ };
94
+ const isRequireMainMember = (node, shadowed) => node && node.type === 'MemberExpression' && node.object.type === 'Identifier' && node.object.name === 'require' && !shadowed.has('require') && node.property.type === 'Identifier' && node.property.name === 'main';
95
+ const hasTopLevelAwait = program => {
96
+ let found = false;
97
+ const walkNode = (node, inFunction) => {
98
+ if (found) return;
99
+ switch (node.type) {
100
+ case 'FunctionDeclaration':
101
+ case 'FunctionExpression':
102
+ case 'ArrowFunctionExpression':
103
+ case 'ClassDeclaration':
104
+ case 'ClassExpression':
105
+ inFunction = true;
106
+ break;
107
+ }
108
+ if (!inFunction && node.type === 'AwaitExpression') {
109
+ found = true;
110
+ return;
111
+ }
112
+ const keys = Object.keys(node);
113
+ for (const key of keys) {
114
+ const value = node[key];
115
+ if (!value) continue;
116
+ if (Array.isArray(value)) {
117
+ for (const item of value) {
118
+ if (item && typeof item === 'object') {
119
+ walkNode(item, inFunction);
120
+ if (found) return;
121
+ }
122
+ }
123
+ } else if (value && typeof value === 'object') {
124
+ walkNode(value, inFunction);
125
+ if (found) return;
126
+ }
127
+ }
128
+ };
129
+ walkNode(program, false);
130
+ return found;
131
+ };
132
+ const lowerEsmToCjs = (program, code, opts, containsTopLevelAwait) => {
133
+ const live = opts.liveBindings ?? 'strict';
134
+ const importTransforms = [];
135
+ const exportTransforms = [];
136
+ let needsInterop = false;
137
+ let importIndex = 0;
138
+ for (const node of program.body) {
139
+ if (node.type === 'ImportDeclaration') {
140
+ const srcLiteral = code.slice(node.source.start, node.source.end);
141
+ const specifiers = node.specifiers ?? [];
142
+ const defaultSpec = specifiers.find(s => s.type === 'ImportDefaultSpecifier');
143
+ const namespaceSpec = specifiers.find(s => s.type === 'ImportNamespaceSpecifier');
144
+ const namedSpecs = specifiers.filter(s => s.type === 'ImportSpecifier');
11
145
 
146
+ // Side-effect import
147
+ if (!specifiers.length) {
148
+ importTransforms.push({
149
+ start: node.start,
150
+ end: node.end,
151
+ code: `require(${srcLiteral});\n`,
152
+ needsInterop: false
153
+ });
154
+ continue;
155
+ }
156
+ const modIdent = `__mod${importIndex++}`;
157
+ const lines = [];
158
+ lines.push(`const ${modIdent} = require(${srcLiteral});`);
159
+ if (namespaceSpec) {
160
+ lines.push(`const ${namespaceSpec.local.name} = ${modIdent};`);
161
+ }
162
+ if (defaultSpec) {
163
+ let init = modIdent;
164
+ switch (opts.cjsDefault) {
165
+ case 'module-exports':
166
+ init = modIdent;
167
+ break;
168
+ case 'none':
169
+ init = `${modIdent}.default`;
170
+ break;
171
+ case 'auto':
172
+ default:
173
+ init = `${defaultInteropName}(${modIdent})`;
174
+ needsInterop = true;
175
+ break;
176
+ }
177
+ lines.push(`const ${defaultSpec.local.name} = ${init};`);
178
+ }
179
+ if (namedSpecs.length) {
180
+ const pairs = namedSpecs.map(s => {
181
+ const imported = s.imported.name;
182
+ const local = s.local.name;
183
+ return imported === local ? imported : `${imported}: ${local}`;
184
+ });
185
+ lines.push(`const { ${pairs.join(', ')} } = ${modIdent};`);
186
+ }
187
+ importTransforms.push({
188
+ start: node.start,
189
+ end: node.end,
190
+ code: `${lines.join('\n')}\n`,
191
+ needsInterop
192
+ });
193
+ }
194
+ if (node.type === 'ExportNamedDeclaration') {
195
+ // Handle declaration exports
196
+ if (node.declaration) {
197
+ const decl = node.declaration;
198
+ const declSrc = code.slice(decl.start, decl.end);
199
+ const exportedNames = [];
200
+ if (decl.type === 'VariableDeclaration') {
201
+ for (const d of decl.declarations) {
202
+ if (d.id.type === 'Identifier') {
203
+ exportedNames.push(d.id.name);
204
+ }
205
+ }
206
+ } else if (decl.id?.type === 'Identifier') {
207
+ exportedNames.push(decl.id.name);
208
+ }
209
+ const exportLines = exportedNames.map(name => exportAssignment(name, name, live));
210
+ exportTransforms.push({
211
+ start: node.start,
212
+ end: node.end,
213
+ code: `${declSrc}\n${exportLines.join('\n')}\n`
214
+ });
215
+ continue;
216
+ }
217
+
218
+ // Handle re-export or local specifiers
219
+ if (node.specifiers?.length) {
220
+ if (node.source) {
221
+ const srcLiteral = code.slice(node.source.start, node.source.end);
222
+ const modIdent = `__mod${importIndex++}`;
223
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
224
+ for (const spec of node.specifiers) {
225
+ if (spec.type !== 'ExportSpecifier') continue;
226
+ const exported = spec.exported.name;
227
+ const imported = spec.local.name;
228
+ let rhs = `${modIdent}.${imported}`;
229
+ if (imported === 'default') {
230
+ rhs = `${defaultInteropName}(${modIdent})`;
231
+ needsInterop = true;
232
+ }
233
+ lines.push(exportAssignment(exported, rhs, live));
234
+ }
235
+ exportTransforms.push({
236
+ start: node.start,
237
+ end: node.end,
238
+ code: `${lines.join('\n')}\n`,
239
+ needsInterop
240
+ });
241
+ } else {
242
+ const lines = [];
243
+ for (const spec of node.specifiers) {
244
+ if (spec.type !== 'ExportSpecifier') continue;
245
+ const exported = spec.exported.name;
246
+ const local = spec.local.name;
247
+ lines.push(exportAssignment(exported, local, live));
248
+ }
249
+ exportTransforms.push({
250
+ start: node.start,
251
+ end: node.end,
252
+ code: `${lines.join('\n')}\n`
253
+ });
254
+ }
255
+ }
256
+ }
257
+ if (node.type === 'ExportDefaultDeclaration') {
258
+ const decl = node.declaration;
259
+ const useExportsObject = containsTopLevelAwait && opts.topLevelAwait !== 'error';
260
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
261
+ if (decl.id?.name) {
262
+ const declSrc = code.slice(decl.start, decl.end);
263
+ const assign = useExportsObject ? `exports.default = ${decl.id.name};` : `module.exports = ${decl.id.name};`;
264
+ exportTransforms.push({
265
+ start: node.start,
266
+ end: node.end,
267
+ code: `${declSrc}\n${assign}\n`
268
+ });
269
+ } else {
270
+ const declSrc = code.slice(decl.start, decl.end);
271
+ const assign = useExportsObject ? `exports.default = ${declSrc};` : `module.exports = ${declSrc};`;
272
+ exportTransforms.push({
273
+ start: node.start,
274
+ end: node.end,
275
+ code: `${assign}\n`
276
+ });
277
+ }
278
+ } else {
279
+ const exprSrc = code.slice(decl.start, decl.end);
280
+ const assign = useExportsObject ? `exports.default = ${exprSrc};` : `module.exports = ${exprSrc};`;
281
+ exportTransforms.push({
282
+ start: node.start,
283
+ end: node.end,
284
+ code: `${assign}\n`
285
+ });
286
+ }
287
+ }
288
+ if (node.type === 'ExportAllDeclaration') {
289
+ const srcLiteral = code.slice(node.source.start, node.source.end);
290
+ if (node.exported) {
291
+ const exported = node.exported.name;
292
+ const modIdent = `__mod${importIndex++}`;
293
+ const lines = [`const ${modIdent} = require(${srcLiteral});`, exportAssignment(exported, modIdent, live)];
294
+ exportTransforms.push({
295
+ start: node.start,
296
+ end: node.end,
297
+ code: `${lines.join('\n')}\n`
298
+ });
299
+ } else {
300
+ const modIdent = `__mod${importIndex++}`;
301
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
302
+ const loop = `for (const k in ${modIdent}) {\n if (k === 'default') continue;\n if (!Object.prototype.hasOwnProperty.call(${modIdent}, k)) continue;\n Object.defineProperty(exports, k, { enumerable: true, get: () => ${modIdent}[k] });\n}`;
303
+ lines.push(loop);
304
+ exportTransforms.push({
305
+ start: node.start,
306
+ end: node.end,
307
+ code: `${lines.join('\n')}\n`
308
+ });
309
+ }
310
+ }
311
+ }
312
+ return {
313
+ importTransforms,
314
+ exportTransforms,
315
+ needsInterop
316
+ };
317
+ };
12
318
  /**
13
319
  * Node added support for import.meta.main.
14
320
  * Added in: v24.2.0, v22.18.0
@@ -22,21 +328,67 @@ const format = async (src, ast, opts) => {
22
328
  hasDefaultExportBeenReassigned: false,
23
329
  hasDefaultExportBeenAssigned: false
24
330
  };
25
- const exportTable = opts.target === 'module' ? await collectCjsExports(ast.program) : null;
26
- await collectModuleIdentifiers(ast.program);
331
+ const moduleIdentifiers = await collectModuleIdentifiers(ast.program);
332
+ const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
27
333
  if (opts.target === 'module' && opts.transformSyntax) {
28
- /**
29
- * Prepare ESM output by renaming `exports` to `__exports` and seeding an
30
- * `import.meta.filename` touch so import.meta is present even when the
31
- * original source never referenced it.
32
- */
33
- code.prepend(`let ${exportsRename} = {};
34
- void import.meta.filename;
35
- `);
334
+ if (shadowedBindings.has('module') || shadowedBindings.has('exports')) {
335
+ throw new Error('Cannot transform to ESM: module or exports is shadowed in module scope.');
336
+ }
337
+ }
338
+ const exportTable = opts.target === 'module' ? await collectCjsExports(ast.program) : null;
339
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
340
+ const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
341
+ const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
342
+ const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
343
+ let hoistedImports = [];
344
+ let pendingRequireTransforms = [];
345
+ let needsCreateRequire = false;
346
+ let pendingCjsTransforms = null;
347
+ if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
348
+ throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
349
+ }
350
+ if (shouldRaiseEsm) {
351
+ const {
352
+ transforms,
353
+ imports,
354
+ needsCreateRequire: reqCreate
355
+ } = lowerCjsRequireToImports(ast.program, code, shadowedBindings);
356
+ pendingRequireTransforms = transforms;
357
+ hoistedImports = imports;
358
+ needsCreateRequire = reqCreate;
36
359
  }
37
360
  await ancestorWalk(ast.program, {
38
361
  async enter(node, ancestors) {
39
362
  const parent = ancestors[ancestors.length - 2] ?? null;
363
+ if (shouldRaiseEsm && node.type === 'BinaryExpression') {
364
+ const op = node.operator;
365
+ const isEquality = op === '===' || op === '==' || op === '!==' || op === '!=';
366
+ if (isEquality) {
367
+ const leftMain = isRequireMainMember(node.left, shadowedBindings);
368
+ const rightMain = isRequireMainMember(node.right, shadowedBindings);
369
+ const leftModule = node.left.type === 'Identifier' && node.left.name === 'module' && !shadowedBindings.has('module');
370
+ const rightModule = node.right.type === 'Identifier' && node.right.name === 'module' && !shadowedBindings.has('module');
371
+ if (leftMain && rightModule || rightMain && leftModule) {
372
+ const negate = op === '!==' || op === '!=';
373
+ code.update(node.start, node.end, negate ? '!import.meta.main' : 'import.meta.main');
374
+ return;
375
+ }
376
+ }
377
+ }
378
+ if (shouldRaiseEsm && node.type === 'CallExpression' && isRequireCall(node, shadowedBindings)) {
379
+ const isStatic = isStaticRequire(node, shadowedBindings);
380
+ const parent = ancestors[ancestors.length - 2] ?? null;
381
+ const grandparent = ancestors[ancestors.length - 3] ?? null;
382
+ const greatGrandparent = ancestors[ancestors.length - 4] ?? null;
383
+
384
+ // Hoistable cases are handled separately and don't need createRequire.
385
+ const topLevelExprStmt = parent?.type === 'ExpressionStatement' && grandparent?.type === 'Program';
386
+ const topLevelVarDecl = parent?.type === 'VariableDeclarator' && grandparent?.type === 'VariableDeclaration' && greatGrandparent?.type === 'Program';
387
+ const hoistableTopLevel = isStatic && (topLevelExprStmt || topLevelVarDecl);
388
+ if (!isStatic || !hoistableTopLevel) {
389
+ needsCreateRequire = true;
390
+ }
391
+ }
40
392
  if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
41
393
  const skipped = ['__filename', '__dirname'];
42
394
  const skippedParams = node.params.filter(param => param.type === 'Identifier' && skipped.includes(param.name));
@@ -95,7 +447,7 @@ void import.meta.filename;
95
447
  metaProperty(node, parent, code, opts);
96
448
  }
97
449
  if (node.type === 'MemberExpression') {
98
- memberExpression(node, parent, code, opts);
450
+ memberExpression(node, parent, code, opts, shadowedBindings);
99
451
  }
100
452
  if (isIdentifierName(node)) {
101
453
  identifier({
@@ -103,11 +455,36 @@ void import.meta.filename;
103
455
  ancestors,
104
456
  code,
105
457
  opts,
106
- meta: exportsMeta
458
+ meta: exportsMeta,
459
+ shadowed: shadowedBindings
107
460
  });
108
461
  }
109
462
  }
110
463
  });
464
+ if (pendingRequireTransforms.length) {
465
+ for (const t of pendingRequireTransforms) {
466
+ code.overwrite(t.start, t.end, t.code);
467
+ }
468
+ }
469
+ if (shouldLowerCjs) {
470
+ const {
471
+ importTransforms,
472
+ exportTransforms,
473
+ needsInterop
474
+ } = lowerEsmToCjs(ast.program, code, opts, containsTopLevelAwait);
475
+ pendingCjsTransforms = {
476
+ transforms: [...importTransforms, ...exportTransforms].sort((a, b) => a.start - b.start),
477
+ needsInterop
478
+ };
479
+ }
480
+ if (pendingCjsTransforms) {
481
+ for (const t of pendingCjsTransforms.transforms) {
482
+ code.overwrite(t.start, t.end, t.code);
483
+ }
484
+ if (pendingCjsTransforms.needsInterop) {
485
+ code.prepend(`${interopHelper}exports.__esModule = true;\n`);
486
+ }
487
+ }
111
488
  if (opts.target === 'module' && opts.transformSyntax && exportTable) {
112
489
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
113
490
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
@@ -137,6 +514,30 @@ void import.meta.filename;
137
514
  code.append(`\n${lines.join('\n')}\n`);
138
515
  }
139
516
  }
517
+ if (shouldRaiseEsm && opts.transformSyntax) {
518
+ const importPrelude = [];
519
+ if (needsCreateRequire) {
520
+ importPrelude.push('import { createRequire } from "node:module";\n');
521
+ }
522
+ if (hoistedImports.length) {
523
+ importPrelude.push(...hoistedImports);
524
+ }
525
+ const requireInit = needsCreateRequire ? 'const require = createRequire(import.meta.url);\n' : '';
526
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${requireInit}let ${exportsRename} = {};
527
+ void import.meta.filename;
528
+ `;
529
+ code.prepend(prelude);
530
+ }
531
+ if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
532
+ const body = code.toString();
533
+ if (opts.topLevelAwait === 'wrap') {
534
+ const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
535
+ const setPromise = `const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== 'object' && type !== 'function') return;\n target.__tla = __tla;\n};\n`;
536
+ const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
537
+ return `${tlaPromise}${setPromise}${attach}`;
538
+ }
539
+ return `;(async () => {\n${body}\n})();\n`;
540
+ }
140
541
  return code.toString();
141
542
  };
142
543
  export { format };
@@ -5,7 +5,8 @@ export const identifier = ({
5
5
  ancestors,
6
6
  code,
7
7
  opts,
8
- meta
8
+ meta,
9
+ shadowed
9
10
  }) => {
10
11
  if (opts.target === 'module') {
11
12
  const {
@@ -13,6 +14,9 @@ export const identifier = ({
13
14
  end,
14
15
  name
15
16
  } = node;
17
+ if (shadowed?.has(name)) {
18
+ return;
19
+ }
16
20
  switch (name) {
17
21
  case '__filename':
18
22
  code.update(start, end, 'import.meta.url');
@@ -1,6 +1,9 @@
1
1
  import { exportsRename } from '#utils/exports.js';
2
- export const memberExpression = (node, parent, src, options) => {
2
+ export const memberExpression = (node, parent, src, options, shadowed) => {
3
3
  if (options.target === 'module') {
4
+ if (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name)) {
5
+ return;
6
+ }
4
7
  if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
5
8
  src.update(node.start, node.end, exportsRename);
6
9
  return;
@@ -17,18 +20,10 @@ export const memberExpression = (node, parent, src, options) => {
17
20
  // CommonJS properties of `require`
18
21
  switch (name) {
19
22
  case 'main':
20
- /**
21
- * Node.js team still quibbling over import.meta.main ¯\_(ツ)_/¯
22
- * @see https://github.com/nodejs/node/pull/32223
23
- */
24
- if (parent?.type === 'ExpressionStatement') {
25
- // This is a standalone expression so remove it to not cause run-time errors.
26
- src.remove(start, end);
23
+ if (parent?.type === 'BinaryExpression') {
24
+ return;
27
25
  }
28
- /**
29
- * Transform require.main === module.
30
- */
31
- if (parent?.type === 'BinaryExpression') {}
26
+ src.update(start, end, 'import.meta.main');
32
27
  break;
33
28
  case 'resolve':
34
29
  src.update(start, end, 'import.meta.resolve');
@@ -42,5 +37,10 @@ export const memberExpression = (node, parent, src, options) => {
42
37
  break;
43
38
  }
44
39
  }
40
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'require') {
41
+ if (!shadowed?.has('module')) {
42
+ src.update(node.start, node.end, 'require');
43
+ }
44
+ }
45
45
  }
46
46
  };
@@ -1,3 +1,16 @@
1
+ const importMetaMainSupport = '(() => { const [__nmaj, __nmin] = process.versions.node.split(".").map(n => parseInt(n, 10) || 0); return (__nmaj > 24 || (__nmaj === 24 && __nmin >= 2) || (__nmaj === 22 && __nmin >= 18)); })()';
2
+ const importMetaMainShim = 'process.argv[1] === __filename';
3
+ const importMetaMainExpr = mode => {
4
+ switch (mode) {
5
+ case 'warn':
6
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (console.warn("import.meta.main is not supported before Node 22.18/24.2; falling back to shim."), ${importMetaMainShim}))`;
7
+ case 'error':
8
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (() => { throw new Error("import.meta.main is not supported before Node 22.18/24.2"); })())`;
9
+ case 'shim':
10
+ default:
11
+ return importMetaMainShim;
12
+ }
13
+ };
1
14
  export const metaProperty = (node, parent, src, options) => {
2
15
  if (options.target === 'commonjs') {
3
16
  if (parent?.type !== 'MemberExpression') {
@@ -21,10 +34,15 @@ export const metaProperty = (node, parent, src, options) => {
21
34
  break;
22
35
  case 'resolve':
23
36
  /**
24
- * Should this be `require('node:url').pathToFileURL(require.resolve(<parsed specifier>)).href`?
37
+ * Map to require.resolve intentionally: matches CJS resolution semantics.
38
+ * Wrapping in pathToFileURL(...) would change the return shape (URL string)
39
+ * without truly emulating ESM import.meta.resolve rules.
25
40
  */
26
41
  src.update(parent.start, parent.end, 'require.resolve');
27
42
  break;
43
+ case 'main':
44
+ src.update(parent.start, parent.end, importMetaMainExpr(options.importMetaMain));
45
+ break;
28
46
  }
29
47
  }
30
48
  }
@@ -1,4 +1,4 @@
1
1
  import MagicString from 'magic-string';
2
2
  import type { MemberExpression, Node } from 'oxc-parser';
3
3
  import type { FormatterOptions } from '../types.js';
4
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions) => void;
4
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>) => void;
package/dist/module.js CHANGED
@@ -12,6 +12,7 @@ const defaultOptions = {
12
12
  rewriteSpecifier: undefined,
13
13
  dirFilename: 'inject',
14
14
  importMeta: 'shim',
15
+ importMetaMain: 'shim',
15
16
  requireSource: 'builtin',
16
17
  cjsDefault: 'auto',
17
18
  topLevelAwait: 'error',
@@ -7,6 +7,7 @@ type IdentifierArg = {
7
7
  code: MagicString;
8
8
  opts: FormatterOptions;
9
9
  meta: ExportsMeta;
10
+ shadowed?: Set<string>;
10
11
  };
11
- export declare const identifier: ({ node, ancestors, code, opts, meta }: IdentifierArg) => void;
12
+ export declare const identifier: ({ node, ancestors, code, opts, meta, shadowed, }: IdentifierArg) => void;
12
13
  export {};
@@ -1,4 +1,4 @@
1
1
  import MagicString from 'magic-string';
2
2
  import type { MemberExpression, Node } from 'oxc-parser';
3
3
  import type { FormatterOptions } from '../types.js';
4
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions) => void;
4
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>) => void;
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/module",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "Transforms differences between ES modules and CommonJS.",
5
5
  "type": "module",
6
6
  "main": "dist/module.js",