@knighted/module 1.0.0-rc.2 → 1.0.0-rc.4

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
@@ -101,6 +101,8 @@ type ModuleOptions = {
101
101
  sourceType?: 'auto' | 'module' | 'commonjs'
102
102
  transformSyntax?: boolean
103
103
  liveBindings?: 'strict' | 'loose' | 'off'
104
+ appendJsExtension?: 'off' | 'relative-only' | 'all'
105
+ appendDirectoryIndex?: string | false
104
106
  rewriteSpecifier?:
105
107
  | '.js'
106
108
  | '.mjs'
@@ -112,6 +114,8 @@ type ModuleOptions = {
112
114
  dirFilename?: 'inject' | 'preserve' | 'error'
113
115
  importMeta?: 'preserve' | 'shim' | 'error'
114
116
  importMetaMain?: 'shim' | 'warn' | 'error'
117
+ requireMainStrategy?: 'import-meta-main' | 'realpath'
118
+ detectCircularRequires?: 'off' | 'warn' | 'error'
115
119
  requireSource?: 'builtin' | 'create-require'
116
120
  cjsDefault?: 'module-exports' | 'auto' | 'none'
117
121
  topLevelAwait?: 'error' | 'wrap' | 'preserve'
@@ -125,9 +129,13 @@ Behavior notes (defaults in parentheses)
125
129
  - `target` (`commonjs`): output module system.
126
130
  - `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass.
127
131
  - `liveBindings` (`strict`): getter-based live bindings, or snapshot (`loose`/`off`).
132
+ - `appendJsExtension` (`relative-only` when targeting ESM): append `.js` to relative specifiers; never touches bare specifiers.
133
+ - `appendDirectoryIndex` (`index.js`): when a relative specifier ends with a slash, append this index filename (set `false` to disable).
128
134
  - `dirFilename` (`inject`): inject `__dirname`/`__filename`, preserve existing, or throw.
129
135
  - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents.
130
136
  - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old.
137
+ - `requireMainStrategy` (`import-meta-main`): use `import.meta.main` or the realpath-based `pathToFileURL(realpathSync(process.argv[1])).href` check.
138
+ - `detectCircularRequires` (`off`): optionally detect relative static require cycles and warn/throw.
131
139
  - `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
132
140
  - `rewriteSpecifier` (off): rewrite relative specifiers to a chosen extension or via a callback.
133
141
  - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
@@ -135,11 +143,68 @@ Behavior notes (defaults in parentheses)
135
143
  - `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
136
144
  - CommonJS → ESM lowering will throw on `with` statements and unshadowed `eval` calls to avoid unsound rewrites.
137
145
 
146
+ > [!NOTE]
147
+ > Package-level metadata (`package.json` updates such as setting `"type": "module"` or authoring `exports`) is not edited by this tool today; plan that change outside the per-file transform.
148
+
138
149
  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).
139
150
 
140
151
  > [!NOTE]
141
152
  > Known limitations: `with` and unshadowed `eval` are rejected when raising CJS to ESM because the rewrite would be unsound; bare specifiers are not rewritten—only relative specifiers participate in `rewriteSpecifier`.
142
153
 
154
+ ### Diagnostics callback example
155
+
156
+ Pass a `diagnostics` callback to surface CJS→ESM edge cases (mixed `module.exports`/`exports`, top-level `return`, legacy `require.cache`/`require.extensions`, live-binding reassignments, string-literal export names):
157
+
158
+ ```ts
159
+ import { transform } from '@knighted/module'
160
+
161
+ const diagnostics: any[] = []
162
+
163
+ await transform('./file.cjs', {
164
+ target: 'module',
165
+ diagnostics: diag => diagnostics.push(diag),
166
+ })
167
+
168
+ console.log(diagnostics)
169
+ // [
170
+ // {
171
+ // level: 'warning',
172
+ // code: 'cjs-mixed-exports',
173
+ // message: 'Both module.exports and exports are assigned in this module; CommonJS shadowing may not match synthesized ESM exports.',
174
+ // filePath: './file.cjs',
175
+ // loc: { start: 12, end: 48 }
176
+ // },
177
+ // ...
178
+ // ]
179
+ ```
180
+
181
+ > [!WARNING]
182
+ > When raising CommonJS to ESM, synthesized named exports rely on literal keys and `const` literal aliases (e.g., `const key = 'foo'; exports[key] = value`). `var`/`let` bindings used as export keys are not tracked, so prefer direct property names or `const` literals when exporting.
183
+
184
+ ## Pre-`tsc` transforms for TypeScript diagnostics
185
+
186
+ TypeScript reports asymmetric module-global errors (e.g., `import.meta` in CJS, `__dirname` in ESM) as tracked in [microsoft/TypeScript#58658](https://github.com/microsoft/TypeScript/issues/58658). You can mitigate this by running `@knighted/module` **before** `tsc` so the checker sees already-rewritten sources.
187
+
188
+ Minimal flow:
189
+
190
+ ```js
191
+ import { glob } from 'glob'
192
+ import { transform } from '@knighted/module'
193
+
194
+ const files = await glob('src/**/*.{ts,js,mts,cts}', { ignore: 'node_modules/**' })
195
+
196
+ for (const file of files) {
197
+ await transform(file, {
198
+ target: 'commonjs', // or 'module' when raising CJS → ESM
199
+ inPlace: true,
200
+ transformSyntax: true,
201
+ })
202
+ }
203
+ // then run `tsc`
204
+ ```
205
+
206
+ This pre-`tsc` step removes the flagged globals in the compiled orientation; runtime semantics still match the target build.
207
+
143
208
  ## Roadmap
144
209
 
145
210
  - Emit source maps and clearer diagnostics for transform choices.
@@ -26,6 +26,8 @@ const exportAssignment = (name, expr, live) => {
26
26
  };
27
27
  const defaultInteropName = '__interopDefault';
28
28
  const interopHelper = `const ${defaultInteropName} = mod => (mod && mod.__esModule ? mod.default : mod);\n`;
29
+ const requireInteropName = '__requireDefault';
30
+ const requireInteropHelper = `const ${requireInteropName} = mod => (mod && typeof mod === 'object' && 'default' in mod ? mod.default : mod);\n`;
29
31
  const isRequireCallee = (callee, shadowed) => {
30
32
  if (callee.type === 'Identifier' && callee.name === 'require' && !shadowed.has('require')) {
31
33
  return true;
@@ -40,8 +42,14 @@ const isRequireCall = (node, shadowed) => node.type === 'CallExpression' && isRe
40
42
  const lowerCjsRequireToImports = (program, code, shadowed) => {
41
43
  const transforms = [];
42
44
  const imports = [];
45
+ const hoisted = [];
43
46
  let nsIndex = 0;
44
47
  let needsCreateRequire = false;
48
+ let needsInteropHelper = false;
49
+ const isJsonSpecifier = value => {
50
+ const base = value.split(/[?#]/)[0] ?? value;
51
+ return base.endsWith('.json');
52
+ };
45
53
  for (const stmt of program.body) {
46
54
  if (stmt.type === 'VariableDeclaration') {
47
55
  const decls = stmt.declarations;
@@ -49,14 +57,21 @@ const lowerCjsRequireToImports = (program, code, shadowed) => {
49
57
  if (allStatic) {
50
58
  for (const decl of decls) {
51
59
  const init = decl.init;
52
- const source = code.slice(init.arguments[0].start, init.arguments[0].end);
60
+ const arg = init.arguments[0];
61
+ const source = code.slice(arg.start, arg.end);
62
+ const value = arg.value;
63
+ const isJson = typeof value === 'string' && isJsonSpecifier(value);
64
+ const ns = `__cjsImport${nsIndex++}`;
65
+ const jsonImport = isJson ? `${source} with { type: "json" }` : source;
53
66
  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++}`;
67
+ imports.push(isJson ? `import ${ns} from ${jsonImport};\n` : `import * as ${ns} from ${jsonImport};\n`);
68
+ hoisted.push(isJson ? `const ${decl.id.name} = ${ns};\n` : `const ${decl.id.name} = ${requireInteropName}(${ns});\n`);
69
+ needsInteropHelper ||= !isJson;
70
+ } else if (decl.id.type === 'ObjectPattern' || decl.id.type === 'ArrayPattern') {
57
71
  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`);
72
+ imports.push(isJson ? `import ${ns} from ${jsonImport};\n` : `import * as ${ns} from ${jsonImport};\n`);
73
+ hoisted.push(isJson ? `const ${pattern} = ${ns};\n` : `const ${pattern} = ${requireInteropName}(${ns});\n`);
74
+ needsInteropHelper ||= !isJson;
60
75
  } else {
61
76
  needsCreateRequire = true;
62
77
  }
@@ -78,8 +93,12 @@ const lowerCjsRequireToImports = (program, code, shadowed) => {
78
93
  if (stmt.type === 'ExpressionStatement') {
79
94
  const expr = stmt.expression;
80
95
  if (expr && isStaticRequire(expr, shadowed)) {
81
- const source = code.slice(expr.arguments[0].start, expr.arguments[0].end);
82
- imports.push(`import ${source};\n`);
96
+ const arg = expr.arguments[0];
97
+ const source = code.slice(arg.start, arg.end);
98
+ const value = arg.value;
99
+ const isJson = typeof value === 'string' && isJsonSpecifier(value);
100
+ const jsonImport = isJson ? `${source} with { type: "json" }` : source;
101
+ imports.push(`import ${jsonImport};\n`);
83
102
  transforms.push({
84
103
  start: stmt.start,
85
104
  end: stmt.end,
@@ -95,7 +114,9 @@ const lowerCjsRequireToImports = (program, code, shadowed) => {
95
114
  return {
96
115
  transforms,
97
116
  imports,
98
- needsCreateRequire
117
+ hoisted,
118
+ needsCreateRequire,
119
+ needsInteropHelper
99
120
  };
100
121
  };
101
122
  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';
@@ -136,6 +157,20 @@ const hasTopLevelAwait = program => {
136
157
  walkNode(program, false);
137
158
  return found;
138
159
  };
160
+ const isAsyncContext = ancestors => {
161
+ for (let i = ancestors.length - 1; i >= 0; i -= 1) {
162
+ const node = ancestors[i];
163
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
164
+ return !!node.async;
165
+ }
166
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ // Program scope (top-level) supports await in ESM.
172
+ return true;
173
+ };
139
174
  const lowerEsmToCjs = (program, code, opts, containsTopLevelAwait) => {
140
175
  const live = opts.liveBindings ?? 'strict';
141
176
  const importTransforms = [];
@@ -335,6 +370,33 @@ const format = async (src, ast, opts) => {
335
370
  hasDefaultExportBeenReassigned: false,
336
371
  hasDefaultExportBeenAssigned: false
337
372
  };
373
+ const warned = new Set();
374
+ const emitDiagnostic = diag => {
375
+ if (opts.diagnostics) {
376
+ opts.diagnostics(diag);
377
+ return;
378
+ }
379
+ if (diag.level === 'warning') {
380
+ // eslint-disable-next-line no-console -- used for opt-in diagnostics
381
+ console.warn(diag.message);
382
+ return;
383
+ }
384
+
385
+ // eslint-disable-next-line no-console -- used for opt-in diagnostics
386
+ console.error(diag.message);
387
+ };
388
+ const warnOnce = (codeId, message, loc) => {
389
+ const key = `${codeId}:${loc?.start ?? ''}`;
390
+ if (warned.has(key)) return;
391
+ warned.add(key);
392
+ emitDiagnostic({
393
+ level: 'warning',
394
+ code: codeId,
395
+ message,
396
+ filePath: opts.filePath,
397
+ loc
398
+ });
399
+ };
338
400
  const moduleIdentifiers = await (0, _identifiers.collectModuleIdentifiers)(ast.program);
339
401
  const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
340
402
  if (opts.target === 'module' && opts.transformSyntax) {
@@ -343,13 +405,31 @@ const format = async (src, ast, opts) => {
343
405
  }
344
406
  }
345
407
  const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
408
+ if (opts.target === 'module' && exportTable) {
409
+ const hasExportsVia = [...exportTable.values()].some(entry => entry.via.has('exports'));
410
+ const hasModuleExportsVia = [...exportTable.values()].some(entry => entry.via.has('module.exports'));
411
+ if (hasExportsVia && hasModuleExportsVia) {
412
+ const firstExports = [...exportTable.values()].find(entry => entry.via.has('exports'))?.writes[0];
413
+ const firstModule = [...exportTable.values()].find(entry => entry.via.has('module.exports'))?.writes[0];
414
+ warnOnce('cjs-mixed-exports', 'Both module.exports and exports are assigned in this module; CommonJS shadowing may not match synthesized ESM exports.', {
415
+ start: firstModule?.start ?? 0,
416
+ end: firstExports?.end ?? 0
417
+ });
418
+ }
419
+ }
346
420
  const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
347
421
  const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
422
+ const requireMainStrategy = opts.requireMainStrategy ?? 'import-meta-main';
423
+ let requireMainNeedsRealpath = false;
424
+ let needsRequireResolveHelper = false;
425
+ const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require';
348
426
  const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
349
427
  const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
350
428
  let hoistedImports = [];
429
+ let hoistedStatements = [];
351
430
  let pendingRequireTransforms = [];
352
431
  let needsCreateRequire = false;
432
+ let needsImportInterop = false;
353
433
  let pendingCjsTransforms = null;
354
434
  if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
355
435
  throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
@@ -358,15 +438,25 @@ const format = async (src, ast, opts) => {
358
438
  const {
359
439
  transforms,
360
440
  imports,
361
- needsCreateRequire: reqCreate
441
+ hoisted,
442
+ needsCreateRequire: reqCreate,
443
+ needsInteropHelper: reqInteropHelper
362
444
  } = lowerCjsRequireToImports(ast.program, code, shadowedBindings);
363
445
  pendingRequireTransforms = transforms;
364
446
  hoistedImports = imports;
447
+ hoistedStatements = hoisted;
365
448
  needsCreateRequire = reqCreate;
449
+ needsImportInterop = reqInteropHelper;
366
450
  }
367
451
  await (0, _walk.ancestorWalk)(ast.program, {
368
452
  async enter(node, ancestors) {
369
453
  const parent = ancestors[ancestors.length - 2] ?? null;
454
+ if (shouldRaiseEsm && node.type === 'ReturnStatement' && parent?.type === 'Program') {
455
+ warnOnce('top-level-return', 'Top-level return is not allowed in ESM; the transformed module will fail to parse.', {
456
+ start: node.start,
457
+ end: node.end
458
+ });
459
+ }
370
460
  if (shouldRaiseEsm && node.type === 'BinaryExpression') {
371
461
  const op = node.operator;
372
462
  const isEquality = op === '===' || op === '==' || op === '!==' || op === '!=';
@@ -377,7 +467,11 @@ const format = async (src, ast, opts) => {
377
467
  const rightModule = node.right.type === 'Identifier' && node.right.name === 'module' && !shadowedBindings.has('module');
378
468
  if (leftMain && rightModule || rightMain && leftModule) {
379
469
  const negate = op === '!==' || op === '!=';
380
- code.update(node.start, node.end, negate ? '!import.meta.main' : 'import.meta.main');
470
+ const mainExpr = requireMainStrategy === 'import-meta-main' ? 'import.meta.main' : 'import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href';
471
+ if (requireMainStrategy === 'realpath') {
472
+ requireMainNeedsRealpath = true;
473
+ }
474
+ code.update(node.start, node.end, negate ? `!(${mainExpr})` : mainExpr);
381
475
  return;
382
476
  }
383
477
  }
@@ -399,6 +493,18 @@ const format = async (src, ast, opts) => {
399
493
  const topLevelVarDecl = parent?.type === 'VariableDeclarator' && grandparent?.type === 'VariableDeclaration' && greatGrandparent?.type === 'Program';
400
494
  const hoistableTopLevel = isStatic && (topLevelExprStmt || topLevelVarDecl);
401
495
  if (!isStatic || !hoistableTopLevel) {
496
+ if (nestedRequireStrategy === 'dynamic-import') {
497
+ const asyncCapable = isAsyncContext(ancestors);
498
+ if (asyncCapable) {
499
+ const arg = node.arguments[0];
500
+ const argSrc = arg ? code.slice(arg.start, arg.end) : 'undefined';
501
+ const literalVal = arg?.value;
502
+ const isJson = arg?.type === 'Literal' && typeof literalVal === 'string' && (literalVal.split(/[?#]/)[0] ?? literalVal).endsWith('.json');
503
+ const importTarget = isJson ? `${argSrc} with { type: "json" }` : argSrc;
504
+ code.update(node.start, node.end, `(await import(${importTarget}))`);
505
+ return;
506
+ }
507
+ }
402
508
  needsCreateRequire = true;
403
509
  }
404
510
  }
@@ -460,7 +566,26 @@ const format = async (src, ast, opts) => {
460
566
  (0, _metaProperty.metaProperty)(node, parent, code, opts);
461
567
  }
462
568
  if (node.type === 'MemberExpression') {
463
- (0, _memberExpression.memberExpression)(node, parent, code, opts, shadowedBindings);
569
+ (0, _memberExpression.memberExpression)(node, parent, code, opts, shadowedBindings, {
570
+ onRequireResolve: () => {
571
+ if (shouldRaiseEsm) needsRequireResolveHelper = true;
572
+ },
573
+ requireResolveName: '__requireResolve',
574
+ onDiagnostic: (codeId, message, loc) => {
575
+ if (shouldRaiseEsm) warnOnce(codeId, message, loc);
576
+ }
577
+ });
578
+ }
579
+ if (shouldRaiseEsm && node.type === 'ThisExpression') {
580
+ const bindsThis = ancestor => {
581
+ return ancestor.type === 'FunctionDeclaration' || ancestor.type === 'FunctionExpression' || ancestor.type === 'ClassDeclaration' || ancestor.type === 'ClassExpression';
582
+ };
583
+ const bindingAncestor = ancestors.find(ancestor => bindsThis(ancestor));
584
+ const isTopLevel = !bindingAncestor;
585
+ if (isTopLevel) {
586
+ code.update(node.start, node.end, _exports.exportsRename);
587
+ return;
588
+ }
464
589
  }
465
590
  if ((0, _identifier2.isIdentifierName)(node)) {
466
591
  (0, _identifier.identifier)({
@@ -507,6 +632,15 @@ const format = async (src, ast, opts) => {
507
632
  const safe = /^[0-9]/.test(sanitized) ? `_${sanitized}` : sanitized;
508
633
  return `__export_${safe}`;
509
634
  };
635
+ for (const [key, entry] of exportTable) {
636
+ if (entry.reassignments.length) {
637
+ const loc = entry.reassignments[0];
638
+ warnOnce(`cjs-export-reassignment:${key}`, `Export '${key}' is reassigned after export; ESM live bindings may change consumer behavior.`, {
639
+ start: loc.start,
640
+ end: loc.end
641
+ });
642
+ }
643
+ }
510
644
  const lines = [];
511
645
  const defaultEntry = exportTable.get('default');
512
646
  if (defaultEntry) {
@@ -515,6 +649,9 @@ const format = async (src, ast, opts) => {
515
649
  }
516
650
  for (const [key, entry] of exportTable) {
517
651
  if (key === 'default') continue;
652
+ if (!isValidExportName(key)) {
653
+ warnOnce(`cjs-string-export:${key}`, `Synthesized string-literal export '${key}'. Some tooling may require bracket access to use it.`);
654
+ }
518
655
  if (entry.fromIdentifier) {
519
656
  lines.push(`export { ${entry.fromIdentifier} as ${asExportName(key)} };`);
520
657
  } else {
@@ -529,14 +666,36 @@ const format = async (src, ast, opts) => {
529
666
  }
530
667
  if (shouldRaiseEsm && opts.transformSyntax) {
531
668
  const importPrelude = [];
532
- if (needsCreateRequire) {
669
+ if (needsCreateRequire || needsRequireResolveHelper) {
533
670
  importPrelude.push('import { createRequire } from "node:module";\n');
534
671
  }
672
+ if (needsRequireResolveHelper) {
673
+ importPrelude.push('import { fileURLToPath } from "node:url";\n');
674
+ }
675
+ if (requireMainNeedsRealpath) {
676
+ importPrelude.push('import { realpathSync } from "node:fs";\n');
677
+ importPrelude.push('import { pathToFileURL } from "node:url";\n');
678
+ }
535
679
  if (hoistedImports.length) {
536
680
  importPrelude.push(...hoistedImports);
537
681
  }
682
+ const setupPrelude = [];
683
+ if (needsImportInterop) {
684
+ setupPrelude.push(requireInteropHelper);
685
+ }
686
+ if (hoistedStatements.length) {
687
+ setupPrelude.push(...hoistedStatements);
688
+ }
538
689
  const requireInit = needsCreateRequire ? 'const require = createRequire(import.meta.url);\n' : '';
539
- const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${requireInit}let ${_exports.exportsRename} = {};
690
+ const requireResolveInit = needsRequireResolveHelper ? needsCreateRequire ? `const __requireResolve = (id, parent) => {
691
+ const resolved = require.resolve(id, parent);
692
+ return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
693
+ };\n` : `const __requireResolve = (id, parent) => {
694
+ const req = createRequire(parent ?? import.meta.url);
695
+ const resolved = req.resolve(id, parent);
696
+ return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
697
+ };\n` : '';
698
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}let ${_exports.exportsRename} = {};
540
699
  void import.meta.filename;
541
700
  `;
542
701
  code.prepend(prelude);
@@ -25,7 +25,7 @@ const identifier = ({
25
25
  }
26
26
  switch (name) {
27
27
  case '__filename':
28
- code.update(start, end, 'import.meta.url');
28
+ code.update(start, end, 'import.meta.filename');
29
29
  break;
30
30
  case '__dirname':
31
31
  code.update(start, end, 'import.meta.dirname');
@@ -5,7 +5,7 @@ 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, shadowed) => {
8
+ const memberExpression = (node, parent, src, options, shadowed, extras) => {
9
9
  if (options.target === 'module') {
10
10
  if (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name)) {
11
11
  return;
@@ -32,21 +32,39 @@ const memberExpression = (node, parent, src, options, shadowed) => {
32
32
  src.update(start, end, 'import.meta.main');
33
33
  break;
34
34
  case 'resolve':
35
- src.update(start, end, 'import.meta.resolve');
35
+ extras?.onRequireResolve?.();
36
+ src.update(start, end, extras?.requireResolveName ?? 'import.meta.resolve');
36
37
  break;
37
38
  case 'cache':
38
39
  /**
39
40
  * Can of worms here. ¯\_(ツ)_/¯
40
41
  * @see https://github.com/nodejs/help/issues/2806
41
42
  */
43
+ extras?.onDiagnostic?.('legacy-require-cache', 'Access to require.cache is not supported when raising to ESM; behavior may differ.', {
44
+ start,
45
+ end
46
+ });
42
47
  src.update(start, end, '{}');
43
48
  break;
49
+ case 'extensions':
50
+ extras?.onDiagnostic?.('legacy-require-extensions', 'Access to require.extensions is not supported when raising to ESM; use loaders instead.', {
51
+ start,
52
+ end
53
+ });
54
+ break;
44
55
  }
45
56
  }
46
57
  if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'require') {
47
58
  if (!shadowed?.has('module')) {
48
59
  src.update(node.start, node.end, 'require');
49
60
  }
61
+ return;
62
+ }
63
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && (node.property.name === 'parent' || node.property.name === 'children')) {
64
+ extras?.onDiagnostic?.(`legacy-module-${node.property.name}`, `Access to module.${node.property.name} may not behave the same in ESM; consider loaders or explicit wiring instead.`, {
65
+ start: node.start,
66
+ end: node.end
67
+ });
50
68
  }
51
69
  }
52
70
  };
@@ -30,6 +30,31 @@ const getScopeContext = program => {
30
30
  scopeCache.set(program, context);
31
31
  return context;
32
32
  };
33
+ const isInBindingPattern = (pattern, target) => {
34
+ if (pattern === target) return true;
35
+ switch (pattern.type) {
36
+ case 'Identifier':
37
+ return pattern === target;
38
+ case 'AssignmentPattern':
39
+ return isInBindingPattern(pattern.left, target);
40
+ case 'RestElement':
41
+ return isInBindingPattern(pattern.argument, target);
42
+ case 'ObjectPattern':
43
+ return (pattern.properties ?? []).some(prop => {
44
+ if (prop.type === 'Property') {
45
+ return isInBindingPattern(prop.value, target);
46
+ }
47
+ if (prop.type === 'RestElement') {
48
+ return isInBindingPattern(prop.argument, target);
49
+ }
50
+ return false;
51
+ });
52
+ case 'ArrayPattern':
53
+ return (pattern.elements ?? []).some(elem => elem && isInBindingPattern(elem, target));
54
+ default:
55
+ return false;
56
+ }
57
+ };
33
58
 
34
59
  /**
35
60
  * All methods receive the full set of ancestors, which
@@ -48,6 +73,7 @@ const identifier = exports.identifier = {
48
73
  isModuleScope(ancestors, includeImports = false) {
49
74
  const node = ancestors[ancestors.length - 1];
50
75
  const parent = ancestors[ancestors.length - 2];
76
+ const grandParent = ancestors[ancestors.length - 3];
51
77
  const program = ancestors[0];
52
78
  if (!identifier.isNamed(node) || identifier.isMetaProperty(ancestors) || parent.type === 'LabeledStatement' || parent.type === 'BreakStatement' || parent.type === 'ContinueStatement') {
53
79
  return false;
@@ -56,7 +82,9 @@ const identifier = exports.identifier = {
56
82
  return includeImports && parent.local.name === node.name;
57
83
  }
58
84
  if (parent.type === 'Property' && parent.key === node && !parent.computed) {
59
- return false;
85
+ if (grandParent?.type !== 'ObjectPattern') {
86
+ return false;
87
+ }
60
88
  }
61
89
  if (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) {
62
90
  return false;
@@ -78,8 +106,17 @@ const identifier = exports.identifier = {
78
106
  },
79
107
  isDeclaration(ancestors) {
80
108
  const node = ancestors[ancestors.length - 1];
81
- const parent = ancestors[ancestors.length - 2];
82
- return (parent.type === 'VariableDeclarator' || parent.type === 'FunctionDeclaration' || parent.type === 'ClassDeclaration') && parent.id === node;
109
+ // Walk outwards to find a declarator that binds the node
110
+ for (let i = ancestors.length - 2; i >= 0; i--) {
111
+ const parent = ancestors[i];
112
+ if (parent.type === 'VariableDeclarator') {
113
+ return parent.id === node || isInBindingPattern(parent.id, node);
114
+ }
115
+ if (parent.type === 'FunctionDeclaration' || parent.type === 'ClassDeclaration') {
116
+ return parent.id === node;
117
+ }
118
+ }
119
+ return false;
83
120
  },
84
121
  isClassOrFuncDeclarationId(ancestors) {
85
122
  const node = ancestors[ancestors.length - 1];
@@ -88,12 +125,16 @@ const identifier = exports.identifier = {
88
125
  },
89
126
  isVarDeclarationInGlobalScope(ancestors) {
90
127
  const node = ancestors[ancestors.length - 1];
91
- const parent = ancestors[ancestors.length - 2];
92
- const grandParent = ancestors[ancestors.length - 3];
93
128
  const varBoundScopes = ['ClassDeclaration', 'ClassExpression', 'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'];
94
- return parent.type === 'VariableDeclarator' && parent.id === node && grandParent.type === 'VariableDeclaration' && grandParent.kind === 'var' && ancestors.every(ancestor => {
95
- return !varBoundScopes.includes(ancestor.type);
129
+ const declaratorIndex = ancestors.findIndex(ancestor => {
130
+ return ancestor.type === 'VariableDeclarator' && (ancestor === node || isInBindingPattern(ancestor.id, node));
96
131
  });
132
+ if (declaratorIndex === -1) return false;
133
+ const declarator = ancestors[declaratorIndex];
134
+ const declaration = ancestors[declaratorIndex - 1];
135
+ return declaration?.type === 'VariableDeclaration' && declaration.kind === 'var' && ancestors.every(ancestor => {
136
+ return !varBoundScopes.includes(ancestor.type);
137
+ }) && (declarator.id === node || isInBindingPattern(declarator.id, node));
97
138
  },
98
139
  isIife(ancestors) {
99
140
  const parent = ancestors[ancestors.length - 2];
@@ -1,4 +1,13 @@
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, shadowed?: Set<string>) => void;
4
+ type MemberExpressionExtras = {
5
+ onRequireResolve?: () => void;
6
+ requireResolveName?: string;
7
+ onDiagnostic?: (code: string, message: string, loc?: {
8
+ start: number;
9
+ end: number;
10
+ }) => void;
11
+ };
12
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
13
+ export {};