@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 +65 -0
- package/dist/cjs/format.cjs +173 -14
- package/dist/cjs/formatters/identifier.cjs +1 -1
- package/dist/cjs/formatters/memberExpression.cjs +20 -2
- package/dist/cjs/helpers/identifier.cjs +48 -7
- package/dist/cjs/memberExpression.d.cts +10 -1
- package/dist/cjs/module.cjs +154 -16
- package/dist/cjs/specifier.cjs +1 -1
- package/dist/cjs/types.d.cts +38 -0
- package/dist/cjs/utils/exports.cjs +41 -18
- package/dist/cjs/utils/identifiers.cjs +34 -16
- package/dist/format.js +173 -14
- package/dist/formatters/identifier.js +1 -1
- package/dist/formatters/memberExpression.js +20 -2
- package/dist/helpers/identifier.js +48 -7
- package/dist/memberExpression.d.cts +6 -1
- package/dist/memberExpression.d.ts +10 -1
- package/dist/module.js +157 -16
- package/dist/specifier.js +1 -1
- package/dist/src/formatters/memberExpression.d.ts +10 -1
- package/dist/src/types.d.ts +38 -0
- package/dist/types.d.cts +24 -0
- package/dist/types.d.ts +38 -0
- package/dist/utils/exports.js +41 -18
- package/dist/utils/identifiers.js +34 -16
- package/package.json +1 -1
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.
|
package/dist/cjs/format.cjs
CHANGED
|
@@ -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
|
|
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 ${
|
|
55
|
-
|
|
56
|
-
|
|
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 ${
|
|
59
|
-
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
95
|
-
return
|
|
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
|
-
|
|
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 {};
|