@knighted/module 1.0.0-rc.2 → 1.0.0-rc.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
@@ -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,38 @@ 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
+ ## Pre-`tsc` transforms for TypeScript diagnostics
155
+
156
+ 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.
157
+
158
+ Minimal flow:
159
+
160
+ ```js
161
+ import { glob } from 'glob'
162
+ import { transform } from '@knighted/module'
163
+
164
+ const files = await glob('src/**/*.{ts,js,mts,cts}', { ignore: 'node_modules/**' })
165
+
166
+ for (const file of files) {
167
+ await transform(file, {
168
+ target: 'commonjs', // or 'module' when raising CJS → ESM
169
+ inPlace: true,
170
+ transformSyntax: true,
171
+ })
172
+ }
173
+ // then run `tsc`
174
+ ```
175
+
176
+ This pre-`tsc` step removes the flagged globals in the compiled orientation; runtime semantics still match the target build.
177
+
143
178
  ## Roadmap
144
179
 
145
180
  - 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 = [];
@@ -345,11 +380,17 @@ const format = async (src, ast, opts) => {
345
380
  const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
346
381
  const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
347
382
  const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
383
+ const requireMainStrategy = opts.requireMainStrategy ?? 'import-meta-main';
384
+ let requireMainNeedsRealpath = false;
385
+ let needsRequireResolveHelper = false;
386
+ const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require';
348
387
  const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
349
388
  const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
350
389
  let hoistedImports = [];
390
+ let hoistedStatements = [];
351
391
  let pendingRequireTransforms = [];
352
392
  let needsCreateRequire = false;
393
+ let needsImportInterop = false;
353
394
  let pendingCjsTransforms = null;
354
395
  if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
355
396
  throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
@@ -358,11 +399,15 @@ const format = async (src, ast, opts) => {
358
399
  const {
359
400
  transforms,
360
401
  imports,
361
- needsCreateRequire: reqCreate
402
+ hoisted,
403
+ needsCreateRequire: reqCreate,
404
+ needsInteropHelper: reqInteropHelper
362
405
  } = lowerCjsRequireToImports(ast.program, code, shadowedBindings);
363
406
  pendingRequireTransforms = transforms;
364
407
  hoistedImports = imports;
408
+ hoistedStatements = hoisted;
365
409
  needsCreateRequire = reqCreate;
410
+ needsImportInterop = reqInteropHelper;
366
411
  }
367
412
  await (0, _walk.ancestorWalk)(ast.program, {
368
413
  async enter(node, ancestors) {
@@ -377,7 +422,11 @@ const format = async (src, ast, opts) => {
377
422
  const rightModule = node.right.type === 'Identifier' && node.right.name === 'module' && !shadowedBindings.has('module');
378
423
  if (leftMain && rightModule || rightMain && leftModule) {
379
424
  const negate = op === '!==' || op === '!=';
380
- code.update(node.start, node.end, negate ? '!import.meta.main' : 'import.meta.main');
425
+ const mainExpr = requireMainStrategy === 'import-meta-main' ? 'import.meta.main' : 'import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href';
426
+ if (requireMainStrategy === 'realpath') {
427
+ requireMainNeedsRealpath = true;
428
+ }
429
+ code.update(node.start, node.end, negate ? `!(${mainExpr})` : mainExpr);
381
430
  return;
382
431
  }
383
432
  }
@@ -399,6 +448,18 @@ const format = async (src, ast, opts) => {
399
448
  const topLevelVarDecl = parent?.type === 'VariableDeclarator' && grandparent?.type === 'VariableDeclaration' && greatGrandparent?.type === 'Program';
400
449
  const hoistableTopLevel = isStatic && (topLevelExprStmt || topLevelVarDecl);
401
450
  if (!isStatic || !hoistableTopLevel) {
451
+ if (nestedRequireStrategy === 'dynamic-import') {
452
+ const asyncCapable = isAsyncContext(ancestors);
453
+ if (asyncCapable) {
454
+ const arg = node.arguments[0];
455
+ const argSrc = arg ? code.slice(arg.start, arg.end) : 'undefined';
456
+ const literalVal = arg?.value;
457
+ const isJson = arg?.type === 'Literal' && typeof literalVal === 'string' && (literalVal.split(/[?#]/)[0] ?? literalVal).endsWith('.json');
458
+ const importTarget = isJson ? `${argSrc} with { type: "json" }` : argSrc;
459
+ code.update(node.start, node.end, `(await import(${importTarget}))`);
460
+ return;
461
+ }
462
+ }
402
463
  needsCreateRequire = true;
403
464
  }
404
465
  }
@@ -460,7 +521,23 @@ const format = async (src, ast, opts) => {
460
521
  (0, _metaProperty.metaProperty)(node, parent, code, opts);
461
522
  }
462
523
  if (node.type === 'MemberExpression') {
463
- (0, _memberExpression.memberExpression)(node, parent, code, opts, shadowedBindings);
524
+ (0, _memberExpression.memberExpression)(node, parent, code, opts, shadowedBindings, {
525
+ onRequireResolve: () => {
526
+ if (shouldRaiseEsm) needsRequireResolveHelper = true;
527
+ },
528
+ requireResolveName: '__requireResolve'
529
+ });
530
+ }
531
+ if (shouldRaiseEsm && node.type === 'ThisExpression') {
532
+ const bindsThis = ancestor => {
533
+ return ancestor.type === 'FunctionDeclaration' || ancestor.type === 'FunctionExpression' || ancestor.type === 'ClassDeclaration' || ancestor.type === 'ClassExpression';
534
+ };
535
+ const bindingAncestor = ancestors.find(ancestor => bindsThis(ancestor));
536
+ const isTopLevel = !bindingAncestor;
537
+ if (isTopLevel) {
538
+ code.update(node.start, node.end, _exports.exportsRename);
539
+ return;
540
+ }
464
541
  }
465
542
  if ((0, _identifier2.isIdentifierName)(node)) {
466
543
  (0, _identifier.identifier)({
@@ -529,14 +606,36 @@ const format = async (src, ast, opts) => {
529
606
  }
530
607
  if (shouldRaiseEsm && opts.transformSyntax) {
531
608
  const importPrelude = [];
532
- if (needsCreateRequire) {
609
+ if (needsCreateRequire || needsRequireResolveHelper) {
533
610
  importPrelude.push('import { createRequire } from "node:module";\n');
534
611
  }
612
+ if (needsRequireResolveHelper) {
613
+ importPrelude.push('import { fileURLToPath } from "node:url";\n');
614
+ }
615
+ if (requireMainNeedsRealpath) {
616
+ importPrelude.push('import { realpathSync } from "node:fs";\n');
617
+ importPrelude.push('import { pathToFileURL } from "node:url";\n');
618
+ }
535
619
  if (hoistedImports.length) {
536
620
  importPrelude.push(...hoistedImports);
537
621
  }
622
+ const setupPrelude = [];
623
+ if (needsImportInterop) {
624
+ setupPrelude.push(requireInteropHelper);
625
+ }
626
+ if (hoistedStatements.length) {
627
+ setupPrelude.push(...hoistedStatements);
628
+ }
538
629
  const requireInit = needsCreateRequire ? 'const require = createRequire(import.meta.url);\n' : '';
539
- const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${requireInit}let ${_exports.exportsRename} = {};
630
+ const requireResolveInit = needsRequireResolveHelper ? needsCreateRequire ? `const __requireResolve = (id, parent) => {
631
+ const resolved = require.resolve(id, parent);
632
+ return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
633
+ };\n` : `const __requireResolve = (id, parent) => {
634
+ const req = createRequire(parent ?? import.meta.url);
635
+ const resolved = req.resolve(id, parent);
636
+ return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
637
+ };\n` : '';
638
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}let ${_exports.exportsRename} = {};
540
639
  void import.meta.filename;
541
640
  `;
542
641
  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,7 +32,8 @@ 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
  /**
@@ -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,9 @@
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
+ };
8
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
9
+ export {};
@@ -10,16 +10,155 @@ var _specifier = require("./specifier.cjs");
10
10
  var _parse = require("#parse");
11
11
  var _format = require("#format");
12
12
  var _lang = require("#utils/lang.js");
13
+ var _nodeModule = require("node:module");
14
+ var _walk = require("#walk");
15
+ const collapseSpecifier = value => value.replace(/['"`+)\s]|new String\(/g, '');
16
+ const builtinSpecifiers = new Set(_nodeModule.builtinModules.map(mod => mod.startsWith('node:') ? mod.slice(5) : mod).flatMap(mod => {
17
+ const parts = mod.split('/');
18
+ const base = parts[0];
19
+ return parts.length > 1 ? [mod, base] : [mod];
20
+ }));
21
+ const appendExtensionIfNeeded = (spec, mode, dirIndex, value = spec.value) => {
22
+ if (mode === 'off') return;
23
+ if (spec.type === 'TemplateLiteral') {
24
+ const node = spec.node;
25
+ if (node.expressions.length > 0) return;
26
+ } else if (spec.type !== 'StringLiteral') {
27
+ return;
28
+ }
29
+ const collapsed = collapseSpecifier(value);
30
+ const isRelative = /^(?:\.\.?)\//.test(collapsed);
31
+ if (!isRelative) return;
32
+ const base = collapsed.split(/[?#]/)[0];
33
+ if (!base) return;
34
+ if (base.endsWith('/')) {
35
+ if (!dirIndex) return;
36
+ return `${value}${dirIndex}`;
37
+ }
38
+ const lastSegment = base.split('/').pop() ?? '';
39
+ if (lastSegment.includes('.')) return;
40
+ return `${value}.js`;
41
+ };
42
+ const rewriteSpecifierValue = (value, rewriteSpecifier) => {
43
+ if (!rewriteSpecifier) return;
44
+ if (typeof rewriteSpecifier === 'function') {
45
+ return rewriteSpecifier(value) ?? undefined;
46
+ }
47
+ const collapsed = collapseSpecifier(value);
48
+ const relative = /^(?:\.\.?)\//;
49
+ if (relative.test(collapsed)) {
50
+ return value.replace(/(.+)\.(?:m|c)?(?:j|t)s([)'"]*)?$/, `$1${rewriteSpecifier}$2`);
51
+ }
52
+ };
53
+ const normalizeBuiltinSpecifier = value => {
54
+ const collapsed = collapseSpecifier(value);
55
+ if (!collapsed) return;
56
+ const specPart = collapsed.split(/[?#]/)[0] ?? '';
57
+
58
+ // Ignore relative and absolute paths.
59
+ if (/^(?:\.\.?\/|\/)/.test(specPart)) return;
60
+
61
+ // Skip other protocols (e.g., http:, data:) but allow node:.
62
+ if (/^[a-zA-Z][a-zA-Z+.-]*:/.test(specPart) && !specPart.startsWith('node:')) return;
63
+ const bare = specPart.startsWith('node:') ? specPart.slice(5) : specPart;
64
+ const base = bare.split('/')[0] ?? '';
65
+ if (!builtinSpecifiers.has(bare) && !builtinSpecifiers.has(base)) return;
66
+ if (specPart.startsWith('node:')) return;
67
+ const quote = /^['"`]/.exec(value)?.[0] ?? '';
68
+ return quote ? `${quote}node:${value.slice(quote.length)}` : `node:${value}`;
69
+ };
70
+ const fileExists = async candidate => {
71
+ try {
72
+ const s = await (0, _promises.stat)(candidate);
73
+ return s.isFile();
74
+ } catch {
75
+ return false;
76
+ }
77
+ };
78
+ const resolveRequirePath = async (fromFile, spec, dirIndex) => {
79
+ if (!spec.startsWith('./') && !spec.startsWith('../')) return null;
80
+ const base = (0, _nodePath.resolve)((0, _nodePath.dirname)(fromFile), spec);
81
+ const ext = (0, _nodePath.extname)(base);
82
+ const candidates = [];
83
+ if (ext) {
84
+ candidates.push(base);
85
+ } else {
86
+ candidates.push(`${base}.js`, `${base}.cjs`, `${base}.mjs`);
87
+ candidates.push((0, _nodePath.join)(base, dirIndex));
88
+ }
89
+ for (const candidate of candidates) {
90
+ if (await fileExists(candidate)) return candidate;
91
+ }
92
+ return null;
93
+ };
94
+ const collectStaticRequires = async (filePath, dirIndex) => {
95
+ const src = await (0, _promises.readFile)(filePath, 'utf8');
96
+ const ast = (0, _parse.parse)(filePath, src);
97
+ const specs = [];
98
+ await (0, _walk.walk)(ast.program, {
99
+ enter(node) {
100
+ if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require' && node.arguments.length === 1 && node.arguments[0].type === 'Literal' && typeof node.arguments[0].value === 'string') {
101
+ const spec = node.arguments[0].value;
102
+ if (spec.startsWith('./') || spec.startsWith('../')) {
103
+ specs.push(spec);
104
+ }
105
+ }
106
+ }
107
+ });
108
+ const resolved = [];
109
+ for (const spec of specs) {
110
+ const target = await resolveRequirePath(filePath, spec, dirIndex);
111
+ if (target) resolved.push(target);
112
+ }
113
+ return resolved;
114
+ };
115
+ const detectCircularRequireGraph = async (entryFile, mode, dirIndex) => {
116
+ const cache = new Map();
117
+ const visiting = new Set();
118
+ const visited = new Set();
119
+ const dfs = async (file, stack) => {
120
+ if (visiting.has(file)) {
121
+ const cycle = [...stack, file];
122
+ const msg = `Circular require detected: ${cycle.join(' -> ')}`;
123
+ if (mode === 'error') {
124
+ throw new Error(msg);
125
+ }
126
+ // eslint-disable-next-line no-console -- surfaced when cycle detection is warn-only
127
+ console.warn(msg);
128
+ return;
129
+ }
130
+ if (visited.has(file)) return;
131
+ visiting.add(file);
132
+ stack.push(file);
133
+ let deps = cache.get(file);
134
+ if (!deps) {
135
+ deps = await collectStaticRequires(file, dirIndex);
136
+ cache.set(file, deps);
137
+ }
138
+ for (const dep of deps) {
139
+ await dfs(dep, stack);
140
+ }
141
+ stack.pop();
142
+ visiting.delete(file);
143
+ visited.add(file);
144
+ };
145
+ await dfs(entryFile, []);
146
+ };
13
147
  const defaultOptions = {
14
148
  target: 'commonjs',
15
149
  sourceType: 'auto',
16
150
  transformSyntax: true,
17
151
  liveBindings: 'strict',
18
152
  rewriteSpecifier: undefined,
153
+ appendJsExtension: undefined,
154
+ appendDirectoryIndex: 'index.js',
19
155
  dirFilename: 'inject',
20
156
  importMeta: 'shim',
21
157
  importMetaMain: 'shim',
158
+ requireMainStrategy: 'import-meta-main',
159
+ detectCircularRequires: 'off',
22
160
  requireSource: 'builtin',
161
+ nestedRequireStrategy: 'create-require',
23
162
  cjsDefault: 'auto',
24
163
  topLevelAwait: 'error',
25
164
  out: undefined,
@@ -30,28 +169,26 @@ const transform = async (filename, options = defaultOptions) => {
30
169
  ...defaultOptions,
31
170
  ...options
32
171
  };
172
+ const appendMode = options?.appendJsExtension ?? (opts.target === 'module' ? 'relative-only' : 'off');
173
+ const dirIndex = opts.appendDirectoryIndex === undefined ? 'index.js' : opts.appendDirectoryIndex;
174
+ const detectCycles = opts.detectCircularRequires ?? 'off';
33
175
  const file = (0, _nodePath.resolve)(filename);
34
176
  const code = (await (0, _promises.readFile)(file)).toString();
35
177
  const ast = (0, _parse.parse)(filename, code);
36
178
  let source = await (0, _format.format)(code, ast, opts);
37
- if (opts.rewriteSpecifier) {
38
- const code = await _specifier.specifier.updateSrc(source, (0, _lang.getLangFromExt)(filename), ({
39
- value
40
- }) => {
41
- if (typeof opts.rewriteSpecifier === 'function') {
42
- return opts.rewriteSpecifier(value) ?? undefined;
43
- }
44
-
45
- // Collapse any BinaryExpression or NewExpression to test for a relative specifier
46
- const collapsed = value.replace(/['"`+)\s]|new String\(/g, '');
47
- const relative = /^(?:\.|\.\.)\//;
48
- if (relative.test(collapsed)) {
49
- // $2 is for any closing quotation/parens around BE or NE
50
- return value.replace(/(.+)\.(?:m|c)?(?:j|t)s([)'"]*)?$/, `$1${opts.rewriteSpecifier}$2`);
51
- }
179
+ if (opts.rewriteSpecifier || appendMode !== 'off' || dirIndex) {
180
+ const code = await _specifier.specifier.updateSrc(source, (0, _lang.getLangFromExt)(filename), spec => {
181
+ const normalized = normalizeBuiltinSpecifier(spec.value);
182
+ const rewritten = rewriteSpecifierValue(normalized ?? spec.value, opts.rewriteSpecifier);
183
+ const baseValue = rewritten ?? normalized ?? spec.value;
184
+ const appended = appendExtensionIfNeeded(spec, appendMode, dirIndex, baseValue);
185
+ return appended ?? rewritten ?? normalized ?? undefined;
52
186
  });
53
187
  source = code;
54
188
  }
189
+ if (detectCycles !== 'off' && opts.target === 'module' && opts.transformSyntax) {
190
+ await detectCircularRequireGraph(file, detectCycles, dirIndex || 'index.js');
191
+ }
55
192
  const outputPath = opts.inPlace ? file : opts.out ? (0, _nodePath.resolve)(opts.out) : undefined;
56
193
  if (outputPath) {
57
194
  await (0, _promises.writeFile)(outputPath, source);
@@ -128,7 +128,7 @@ const formatSpecifiers = async (src, ast, cb) => {
128
128
  }
129
129
  if (node.type === 'CallExpression') {
130
130
  // Handle require(), require.resolve(), import.meta.resolve()
131
- if (node.callee.type === 'Identifier' && node.callee.name === 'require' || node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'require' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'resolve' || node.callee.type === 'MemberExpression' && node.callee.object.type === 'MetaProperty' && node.callee.object.meta.name === 'import' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'resolve') {
131
+ if (node.callee.type === 'Identifier' && node.callee.name === 'require' || node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'require' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'resolve' || node.callee.type === 'Identifier' && node.callee.name === '__requireResolve' || node.callee.type === 'MemberExpression' && node.callee.object.type === 'MetaProperty' && node.callee.object.meta.name === 'import' && node.callee.property.type === 'Identifier' && node.callee.property.name === 'resolve') {
132
132
  formatExpression(node);
133
133
  }
134
134
  }