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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,12 @@ Node.js utility for transforming a JavaScript or TypeScript file from an ES modu
9
9
  - ES module ➡️ CommonJS
10
10
  - CommonJS ➡️ ES module
11
11
 
12
+ Highlights
13
+
14
+ - Defaults to safe CommonJS output: strict live bindings, import.meta shims, and specifier preservation.
15
+ - Opt into stricter/looser behaviors: live binding enforcement, import.meta.main gating, and top-level await strategies.
16
+ - Can optionally rewrite relative specifiers and write transformed output to disk.
17
+
12
18
  > [!IMPORTANT]
13
19
  > All parsing logic is applied under the assumption the code is in [strict mode](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode) which [modules run under by default](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_classic_scripts).
14
20
 
@@ -18,9 +24,15 @@ By default `@knighted/module` transforms the one-to-one [differences between ES
18
24
 
19
25
  - Node >= 20.11.0
20
26
 
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install @knighted/module
31
+ ```
32
+
21
33
  ## Example
22
34
 
23
- Given an ES module
35
+ Given an ES module:
24
36
 
25
37
  **file.js**
26
38
 
@@ -40,7 +52,7 @@ const detectCalledFromCli = async path => {
40
52
  detectCalledFromCli(argv[1])
41
53
  ```
42
54
 
43
- You can transform it to the equivalent CommonJS module
55
+ Transform it to CommonJS:
44
56
 
45
57
  ```js
46
58
  import { transform } from '@knighted/module'
@@ -51,7 +63,7 @@ await transform('./file.js', {
51
63
  })
52
64
  ```
53
65
 
54
- Which produces
66
+ Which produces:
55
67
 
56
68
  **file.cjs**
57
69
 
@@ -99,6 +111,7 @@ type ModuleOptions = {
99
111
  | ((value: string) => string | null | undefined)
100
112
  dirFilename?: 'inject' | 'preserve' | 'error'
101
113
  importMeta?: 'preserve' | 'shim' | 'error'
114
+ importMetaMain?: 'shim' | 'warn' | 'error'
102
115
  requireSource?: 'builtin' | 'create-require'
103
116
  cjsDefault?: 'module-exports' | 'auto' | 'none'
104
117
  topLevelAwait?: 'error' | 'wrap' | 'preserve'
@@ -107,7 +120,24 @@ type ModuleOptions = {
107
120
  }
108
121
  ```
109
122
 
123
+ Behavior notes (defaults in parentheses)
124
+
125
+ - `target` (`commonjs`): output module system.
126
+ - `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass.
127
+ - `liveBindings` (`strict`): getter-based live bindings, or snapshot (`loose`/`off`).
128
+ - `dirFilename` (`inject`): inject `__dirname`/`__filename`, preserve existing, or throw.
129
+ - `importMeta` (`shim`): rewrite `import.meta.*` to CommonJS equivalents.
130
+ - `importMetaMain` (`shim`): gate `import.meta.main` with shimming/warning/error when Node support is too old.
131
+ - `topLevelAwait` (`error`): throw, wrap, or preserve when TLA appears in CommonJS output.
132
+ - `rewriteSpecifier` (off): rewrite relative specifiers to a chosen extension or via a callback.
133
+ - `requireSource` (`builtin`): whether `require` comes from Node or `createRequire`.
134
+ - `cjsDefault` (`auto`): bundler-style default interop vs direct `module.exports`.
135
+ - `out`/`inPlace`: write the transformed code to a file; otherwise the function returns the transformed string only.
136
+
137
+ See [docs/esm-to-cjs.md](docs/esm-to-cjs.md) for deeper notes on live bindings, interop helpers, top-level await behavior, and `import.meta.main` handling.
138
+
110
139
  ## Roadmap
111
140
 
112
141
  - Remove `@knighted/specifier` and avoid double parsing.
113
- - Flesh out live-binding and top-level await handling.
142
+ - Emit source maps and clearer diagnostics for transform choices.
143
+ - Broaden fixtures covering live-binding and top-level await edge cases across Node versions.
@@ -15,6 +15,240 @@ var _identifiers = require("#utils/identifiers.js");
15
15
  var _identifier2 = require("#helpers/identifier.js");
16
16
  var _walk = require("#walk");
17
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
+ const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
19
+ const exportAssignment = (name, expr, live) => {
20
+ const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
21
+ if (live === 'strict') {
22
+ const key = JSON.stringify(name);
23
+ return `Object.defineProperty(exports, ${key}, { enumerable: true, get: () => ${expr} });`;
24
+ }
25
+ return `exports${prop} = ${expr};`;
26
+ };
27
+ const defaultInteropName = '__interopDefault';
28
+ const interopHelper = `const ${defaultInteropName} = mod => (mod && mod.__esModule ? mod.default : mod);\n`;
29
+ const hasTopLevelAwait = program => {
30
+ let found = false;
31
+ const walkNode = (node, inFunction) => {
32
+ if (found) return;
33
+ switch (node.type) {
34
+ case 'FunctionDeclaration':
35
+ case 'FunctionExpression':
36
+ case 'ArrowFunctionExpression':
37
+ case 'ClassDeclaration':
38
+ case 'ClassExpression':
39
+ inFunction = true;
40
+ break;
41
+ }
42
+ if (!inFunction && node.type === 'AwaitExpression') {
43
+ found = true;
44
+ return;
45
+ }
46
+ const keys = Object.keys(node);
47
+ for (const key of keys) {
48
+ const value = node[key];
49
+ if (!value) continue;
50
+ if (Array.isArray(value)) {
51
+ for (const item of value) {
52
+ if (item && typeof item === 'object') {
53
+ walkNode(item, inFunction);
54
+ if (found) return;
55
+ }
56
+ }
57
+ } else if (value && typeof value === 'object') {
58
+ walkNode(value, inFunction);
59
+ if (found) return;
60
+ }
61
+ }
62
+ };
63
+ walkNode(program, false);
64
+ return found;
65
+ };
66
+ const lowerEsmToCjs = (program, code, opts, containsTopLevelAwait) => {
67
+ const live = opts.liveBindings ?? 'strict';
68
+ const importTransforms = [];
69
+ const exportTransforms = [];
70
+ let needsInterop = false;
71
+ let importIndex = 0;
72
+ for (const node of program.body) {
73
+ if (node.type === 'ImportDeclaration') {
74
+ const srcLiteral = code.slice(node.source.start, node.source.end);
75
+ const specifiers = node.specifiers ?? [];
76
+ const defaultSpec = specifiers.find(s => s.type === 'ImportDefaultSpecifier');
77
+ const namespaceSpec = specifiers.find(s => s.type === 'ImportNamespaceSpecifier');
78
+ const namedSpecs = specifiers.filter(s => s.type === 'ImportSpecifier');
79
+
80
+ // Side-effect import
81
+ if (!specifiers.length) {
82
+ importTransforms.push({
83
+ start: node.start,
84
+ end: node.end,
85
+ code: `require(${srcLiteral});\n`,
86
+ needsInterop: false
87
+ });
88
+ continue;
89
+ }
90
+ const modIdent = `__mod${importIndex++}`;
91
+ const lines = [];
92
+ lines.push(`const ${modIdent} = require(${srcLiteral});`);
93
+ if (namespaceSpec) {
94
+ lines.push(`const ${namespaceSpec.local.name} = ${modIdent};`);
95
+ }
96
+ if (defaultSpec) {
97
+ let init = modIdent;
98
+ switch (opts.cjsDefault) {
99
+ case 'module-exports':
100
+ init = modIdent;
101
+ break;
102
+ case 'none':
103
+ init = `${modIdent}.default`;
104
+ break;
105
+ case 'auto':
106
+ default:
107
+ init = `${defaultInteropName}(${modIdent})`;
108
+ needsInterop = true;
109
+ break;
110
+ }
111
+ lines.push(`const ${defaultSpec.local.name} = ${init};`);
112
+ }
113
+ if (namedSpecs.length) {
114
+ const pairs = namedSpecs.map(s => {
115
+ const imported = s.imported.name;
116
+ const local = s.local.name;
117
+ return imported === local ? imported : `${imported}: ${local}`;
118
+ });
119
+ lines.push(`const { ${pairs.join(', ')} } = ${modIdent};`);
120
+ }
121
+ importTransforms.push({
122
+ start: node.start,
123
+ end: node.end,
124
+ code: `${lines.join('\n')}\n`,
125
+ needsInterop
126
+ });
127
+ }
128
+ if (node.type === 'ExportNamedDeclaration') {
129
+ // Handle declaration exports
130
+ if (node.declaration) {
131
+ const decl = node.declaration;
132
+ const declSrc = code.slice(decl.start, decl.end);
133
+ const exportedNames = [];
134
+ if (decl.type === 'VariableDeclaration') {
135
+ for (const d of decl.declarations) {
136
+ if (d.id.type === 'Identifier') {
137
+ exportedNames.push(d.id.name);
138
+ }
139
+ }
140
+ } else if (decl.id?.type === 'Identifier') {
141
+ exportedNames.push(decl.id.name);
142
+ }
143
+ const exportLines = exportedNames.map(name => exportAssignment(name, name, live));
144
+ exportTransforms.push({
145
+ start: node.start,
146
+ end: node.end,
147
+ code: `${declSrc}\n${exportLines.join('\n')}\n`
148
+ });
149
+ continue;
150
+ }
151
+
152
+ // Handle re-export or local specifiers
153
+ if (node.specifiers?.length) {
154
+ if (node.source) {
155
+ const srcLiteral = code.slice(node.source.start, node.source.end);
156
+ const modIdent = `__mod${importIndex++}`;
157
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
158
+ for (const spec of node.specifiers) {
159
+ if (spec.type !== 'ExportSpecifier') continue;
160
+ const exported = spec.exported.name;
161
+ const imported = spec.local.name;
162
+ let rhs = `${modIdent}.${imported}`;
163
+ if (imported === 'default') {
164
+ rhs = `${defaultInteropName}(${modIdent})`;
165
+ needsInterop = true;
166
+ }
167
+ lines.push(exportAssignment(exported, rhs, live));
168
+ }
169
+ exportTransforms.push({
170
+ start: node.start,
171
+ end: node.end,
172
+ code: `${lines.join('\n')}\n`,
173
+ needsInterop
174
+ });
175
+ } else {
176
+ const lines = [];
177
+ for (const spec of node.specifiers) {
178
+ if (spec.type !== 'ExportSpecifier') continue;
179
+ const exported = spec.exported.name;
180
+ const local = spec.local.name;
181
+ lines.push(exportAssignment(exported, local, live));
182
+ }
183
+ exportTransforms.push({
184
+ start: node.start,
185
+ end: node.end,
186
+ code: `${lines.join('\n')}\n`
187
+ });
188
+ }
189
+ }
190
+ }
191
+ if (node.type === 'ExportDefaultDeclaration') {
192
+ const decl = node.declaration;
193
+ const useExportsObject = containsTopLevelAwait && opts.topLevelAwait !== 'error';
194
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
195
+ if (decl.id?.name) {
196
+ const declSrc = code.slice(decl.start, decl.end);
197
+ const assign = useExportsObject ? `exports.default = ${decl.id.name};` : `module.exports = ${decl.id.name};`;
198
+ exportTransforms.push({
199
+ start: node.start,
200
+ end: node.end,
201
+ code: `${declSrc}\n${assign}\n`
202
+ });
203
+ } else {
204
+ const declSrc = code.slice(decl.start, decl.end);
205
+ const assign = useExportsObject ? `exports.default = ${declSrc};` : `module.exports = ${declSrc};`;
206
+ exportTransforms.push({
207
+ start: node.start,
208
+ end: node.end,
209
+ code: `${assign}\n`
210
+ });
211
+ }
212
+ } else {
213
+ const exprSrc = code.slice(decl.start, decl.end);
214
+ const assign = useExportsObject ? `exports.default = ${exprSrc};` : `module.exports = ${exprSrc};`;
215
+ exportTransforms.push({
216
+ start: node.start,
217
+ end: node.end,
218
+ code: `${assign}\n`
219
+ });
220
+ }
221
+ }
222
+ if (node.type === 'ExportAllDeclaration') {
223
+ const srcLiteral = code.slice(node.source.start, node.source.end);
224
+ if (node.exported) {
225
+ const exported = node.exported.name;
226
+ const modIdent = `__mod${importIndex++}`;
227
+ const lines = [`const ${modIdent} = require(${srcLiteral});`, exportAssignment(exported, modIdent, live)];
228
+ exportTransforms.push({
229
+ start: node.start,
230
+ end: node.end,
231
+ code: `${lines.join('\n')}\n`
232
+ });
233
+ } else {
234
+ const modIdent = `__mod${importIndex++}`;
235
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
236
+ const loop = `for (const k in ${modIdent}) {\n if (k === 'default') continue;\n if (!Object.prototype.hasOwnProperty.call(${modIdent}, k)) continue;\n Object.defineProperty(exports, k, { enumerable: true, get: () => ${modIdent}[k] });\n}`;
237
+ lines.push(loop);
238
+ exportTransforms.push({
239
+ start: node.start,
240
+ end: node.end,
241
+ code: `${lines.join('\n')}\n`
242
+ });
243
+ }
244
+ }
245
+ }
246
+ return {
247
+ importTransforms,
248
+ exportTransforms,
249
+ needsInterop
250
+ };
251
+ };
18
252
  /**
19
253
  * Node added support for import.meta.main.
20
254
  * Added in: v24.2.0, v22.18.0
@@ -30,6 +264,13 @@ const format = async (src, ast, opts) => {
30
264
  };
31
265
  const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
32
266
  await (0, _identifiers.collectModuleIdentifiers)(ast.program);
267
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
268
+ const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
269
+ const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
270
+ let pendingCjsTransforms = null;
271
+ if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
272
+ throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
273
+ }
33
274
  if (opts.target === 'module' && opts.transformSyntax) {
34
275
  /**
35
276
  * Prepare ESM output by renaming `exports` to `__exports` and seeding an
@@ -114,6 +355,25 @@ void import.meta.filename;
114
355
  }
115
356
  }
116
357
  });
358
+ if (shouldLowerCjs) {
359
+ const {
360
+ importTransforms,
361
+ exportTransforms,
362
+ needsInterop
363
+ } = lowerEsmToCjs(ast.program, code, opts, containsTopLevelAwait);
364
+ pendingCjsTransforms = {
365
+ transforms: [...importTransforms, ...exportTransforms].sort((a, b) => a.start - b.start),
366
+ needsInterop
367
+ };
368
+ }
369
+ if (pendingCjsTransforms) {
370
+ for (const t of pendingCjsTransforms.transforms) {
371
+ code.overwrite(t.start, t.end, t.code);
372
+ }
373
+ if (pendingCjsTransforms.needsInterop) {
374
+ code.prepend(`${interopHelper}exports.__esModule = true;\n`);
375
+ }
376
+ }
117
377
  if (opts.target === 'module' && opts.transformSyntax && exportTable) {
118
378
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
119
379
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
@@ -143,6 +403,16 @@ void import.meta.filename;
143
403
  code.append(`\n${lines.join('\n')}\n`);
144
404
  }
145
405
  }
406
+ if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
407
+ const body = code.toString();
408
+ if (opts.topLevelAwait === 'wrap') {
409
+ const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
410
+ const setPromise = `const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== 'object' && type !== 'function') return;\n target.__tla = __tla;\n};\n`;
411
+ const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
412
+ return `${tlaPromise}${setPromise}${attach}`;
413
+ }
414
+ return `;(async () => {\n${body}\n})();\n`;
415
+ }
146
416
  return code.toString();
147
417
  };
148
418
  exports.format = format;
@@ -4,6 +4,19 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.metaProperty = void 0;
7
+ const importMetaMainSupport = '(() => { const [__nmaj, __nmin] = process.versions.node.split(".").map(n => parseInt(n, 10) || 0); return (__nmaj > 24 || (__nmaj === 24 && __nmin >= 2) || (__nmaj === 22 && __nmin >= 18)); })()';
8
+ const importMetaMainShim = 'process.argv[1] === __filename';
9
+ const importMetaMainExpr = mode => {
10
+ switch (mode) {
11
+ case 'warn':
12
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (console.warn("import.meta.main is not supported before Node 22.18/24.2; falling back to shim."), ${importMetaMainShim}))`;
13
+ case 'error':
14
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (() => { throw new Error("import.meta.main is not supported before Node 22.18/24.2"); })())`;
15
+ case 'shim':
16
+ default:
17
+ return importMetaMainShim;
18
+ }
19
+ };
7
20
  const metaProperty = (node, parent, src, options) => {
8
21
  if (options.target === 'commonjs') {
9
22
  if (parent?.type !== 'MemberExpression') {
@@ -27,10 +40,15 @@ const metaProperty = (node, parent, src, options) => {
27
40
  break;
28
41
  case 'resolve':
29
42
  /**
30
- * Should this be `require('node:url').pathToFileURL(require.resolve(<parsed specifier>)).href`?
43
+ * Map to require.resolve intentionally: matches CJS resolution semantics.
44
+ * Wrapping in pathToFileURL(...) would change the return shape (URL string)
45
+ * without truly emulating ESM import.meta.resolve rules.
31
46
  */
32
47
  src.update(parent.start, parent.end, 'require.resolve');
33
48
  break;
49
+ case 'main':
50
+ src.update(parent.start, parent.end, importMetaMainExpr(options.importMetaMain));
51
+ break;
34
52
  }
35
53
  }
36
54
  }
@@ -18,6 +18,7 @@ const defaultOptions = {
18
18
  rewriteSpecifier: undefined,
19
19
  dirFilename: 'inject',
20
20
  importMeta: 'shim',
21
+ importMetaMain: 'shim',
21
22
  requireSource: 'builtin',
22
23
  cjsDefault: 'auto',
23
24
  topLevelAwait: 'error',
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/dist/format.js CHANGED
@@ -8,7 +8,240 @@ import { exportsRename, collectCjsExports } from '#utils/exports.js';
8
8
  import { collectModuleIdentifiers } from '#utils/identifiers.js';
9
9
  import { isIdentifierName } from '#helpers/identifier.js';
10
10
  import { ancestorWalk } from '#walk';
11
+ const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
12
+ const exportAssignment = (name, expr, live) => {
13
+ const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
14
+ if (live === 'strict') {
15
+ const key = JSON.stringify(name);
16
+ return `Object.defineProperty(exports, ${key}, { enumerable: true, get: () => ${expr} });`;
17
+ }
18
+ return `exports${prop} = ${expr};`;
19
+ };
20
+ const defaultInteropName = '__interopDefault';
21
+ const interopHelper = `const ${defaultInteropName} = mod => (mod && mod.__esModule ? mod.default : mod);\n`;
22
+ const hasTopLevelAwait = program => {
23
+ let found = false;
24
+ const walkNode = (node, inFunction) => {
25
+ if (found) return;
26
+ switch (node.type) {
27
+ case 'FunctionDeclaration':
28
+ case 'FunctionExpression':
29
+ case 'ArrowFunctionExpression':
30
+ case 'ClassDeclaration':
31
+ case 'ClassExpression':
32
+ inFunction = true;
33
+ break;
34
+ }
35
+ if (!inFunction && node.type === 'AwaitExpression') {
36
+ found = true;
37
+ return;
38
+ }
39
+ const keys = Object.keys(node);
40
+ for (const key of keys) {
41
+ const value = node[key];
42
+ if (!value) continue;
43
+ if (Array.isArray(value)) {
44
+ for (const item of value) {
45
+ if (item && typeof item === 'object') {
46
+ walkNode(item, inFunction);
47
+ if (found) return;
48
+ }
49
+ }
50
+ } else if (value && typeof value === 'object') {
51
+ walkNode(value, inFunction);
52
+ if (found) return;
53
+ }
54
+ }
55
+ };
56
+ walkNode(program, false);
57
+ return found;
58
+ };
59
+ const lowerEsmToCjs = (program, code, opts, containsTopLevelAwait) => {
60
+ const live = opts.liveBindings ?? 'strict';
61
+ const importTransforms = [];
62
+ const exportTransforms = [];
63
+ let needsInterop = false;
64
+ let importIndex = 0;
65
+ for (const node of program.body) {
66
+ if (node.type === 'ImportDeclaration') {
67
+ const srcLiteral = code.slice(node.source.start, node.source.end);
68
+ const specifiers = node.specifiers ?? [];
69
+ const defaultSpec = specifiers.find(s => s.type === 'ImportDefaultSpecifier');
70
+ const namespaceSpec = specifiers.find(s => s.type === 'ImportNamespaceSpecifier');
71
+ const namedSpecs = specifiers.filter(s => s.type === 'ImportSpecifier');
11
72
 
73
+ // Side-effect import
74
+ if (!specifiers.length) {
75
+ importTransforms.push({
76
+ start: node.start,
77
+ end: node.end,
78
+ code: `require(${srcLiteral});\n`,
79
+ needsInterop: false
80
+ });
81
+ continue;
82
+ }
83
+ const modIdent = `__mod${importIndex++}`;
84
+ const lines = [];
85
+ lines.push(`const ${modIdent} = require(${srcLiteral});`);
86
+ if (namespaceSpec) {
87
+ lines.push(`const ${namespaceSpec.local.name} = ${modIdent};`);
88
+ }
89
+ if (defaultSpec) {
90
+ let init = modIdent;
91
+ switch (opts.cjsDefault) {
92
+ case 'module-exports':
93
+ init = modIdent;
94
+ break;
95
+ case 'none':
96
+ init = `${modIdent}.default`;
97
+ break;
98
+ case 'auto':
99
+ default:
100
+ init = `${defaultInteropName}(${modIdent})`;
101
+ needsInterop = true;
102
+ break;
103
+ }
104
+ lines.push(`const ${defaultSpec.local.name} = ${init};`);
105
+ }
106
+ if (namedSpecs.length) {
107
+ const pairs = namedSpecs.map(s => {
108
+ const imported = s.imported.name;
109
+ const local = s.local.name;
110
+ return imported === local ? imported : `${imported}: ${local}`;
111
+ });
112
+ lines.push(`const { ${pairs.join(', ')} } = ${modIdent};`);
113
+ }
114
+ importTransforms.push({
115
+ start: node.start,
116
+ end: node.end,
117
+ code: `${lines.join('\n')}\n`,
118
+ needsInterop
119
+ });
120
+ }
121
+ if (node.type === 'ExportNamedDeclaration') {
122
+ // Handle declaration exports
123
+ if (node.declaration) {
124
+ const decl = node.declaration;
125
+ const declSrc = code.slice(decl.start, decl.end);
126
+ const exportedNames = [];
127
+ if (decl.type === 'VariableDeclaration') {
128
+ for (const d of decl.declarations) {
129
+ if (d.id.type === 'Identifier') {
130
+ exportedNames.push(d.id.name);
131
+ }
132
+ }
133
+ } else if (decl.id?.type === 'Identifier') {
134
+ exportedNames.push(decl.id.name);
135
+ }
136
+ const exportLines = exportedNames.map(name => exportAssignment(name, name, live));
137
+ exportTransforms.push({
138
+ start: node.start,
139
+ end: node.end,
140
+ code: `${declSrc}\n${exportLines.join('\n')}\n`
141
+ });
142
+ continue;
143
+ }
144
+
145
+ // Handle re-export or local specifiers
146
+ if (node.specifiers?.length) {
147
+ if (node.source) {
148
+ const srcLiteral = code.slice(node.source.start, node.source.end);
149
+ const modIdent = `__mod${importIndex++}`;
150
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
151
+ for (const spec of node.specifiers) {
152
+ if (spec.type !== 'ExportSpecifier') continue;
153
+ const exported = spec.exported.name;
154
+ const imported = spec.local.name;
155
+ let rhs = `${modIdent}.${imported}`;
156
+ if (imported === 'default') {
157
+ rhs = `${defaultInteropName}(${modIdent})`;
158
+ needsInterop = true;
159
+ }
160
+ lines.push(exportAssignment(exported, rhs, live));
161
+ }
162
+ exportTransforms.push({
163
+ start: node.start,
164
+ end: node.end,
165
+ code: `${lines.join('\n')}\n`,
166
+ needsInterop
167
+ });
168
+ } else {
169
+ const lines = [];
170
+ for (const spec of node.specifiers) {
171
+ if (spec.type !== 'ExportSpecifier') continue;
172
+ const exported = spec.exported.name;
173
+ const local = spec.local.name;
174
+ lines.push(exportAssignment(exported, local, live));
175
+ }
176
+ exportTransforms.push({
177
+ start: node.start,
178
+ end: node.end,
179
+ code: `${lines.join('\n')}\n`
180
+ });
181
+ }
182
+ }
183
+ }
184
+ if (node.type === 'ExportDefaultDeclaration') {
185
+ const decl = node.declaration;
186
+ const useExportsObject = containsTopLevelAwait && opts.topLevelAwait !== 'error';
187
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
188
+ if (decl.id?.name) {
189
+ const declSrc = code.slice(decl.start, decl.end);
190
+ const assign = useExportsObject ? `exports.default = ${decl.id.name};` : `module.exports = ${decl.id.name};`;
191
+ exportTransforms.push({
192
+ start: node.start,
193
+ end: node.end,
194
+ code: `${declSrc}\n${assign}\n`
195
+ });
196
+ } else {
197
+ const declSrc = code.slice(decl.start, decl.end);
198
+ const assign = useExportsObject ? `exports.default = ${declSrc};` : `module.exports = ${declSrc};`;
199
+ exportTransforms.push({
200
+ start: node.start,
201
+ end: node.end,
202
+ code: `${assign}\n`
203
+ });
204
+ }
205
+ } else {
206
+ const exprSrc = code.slice(decl.start, decl.end);
207
+ const assign = useExportsObject ? `exports.default = ${exprSrc};` : `module.exports = ${exprSrc};`;
208
+ exportTransforms.push({
209
+ start: node.start,
210
+ end: node.end,
211
+ code: `${assign}\n`
212
+ });
213
+ }
214
+ }
215
+ if (node.type === 'ExportAllDeclaration') {
216
+ const srcLiteral = code.slice(node.source.start, node.source.end);
217
+ if (node.exported) {
218
+ const exported = node.exported.name;
219
+ const modIdent = `__mod${importIndex++}`;
220
+ const lines = [`const ${modIdent} = require(${srcLiteral});`, exportAssignment(exported, modIdent, live)];
221
+ exportTransforms.push({
222
+ start: node.start,
223
+ end: node.end,
224
+ code: `${lines.join('\n')}\n`
225
+ });
226
+ } else {
227
+ const modIdent = `__mod${importIndex++}`;
228
+ const lines = [`const ${modIdent} = require(${srcLiteral});`];
229
+ const loop = `for (const k in ${modIdent}) {\n if (k === 'default') continue;\n if (!Object.prototype.hasOwnProperty.call(${modIdent}, k)) continue;\n Object.defineProperty(exports, k, { enumerable: true, get: () => ${modIdent}[k] });\n}`;
230
+ lines.push(loop);
231
+ exportTransforms.push({
232
+ start: node.start,
233
+ end: node.end,
234
+ code: `${lines.join('\n')}\n`
235
+ });
236
+ }
237
+ }
238
+ }
239
+ return {
240
+ importTransforms,
241
+ exportTransforms,
242
+ needsInterop
243
+ };
244
+ };
12
245
  /**
13
246
  * Node added support for import.meta.main.
14
247
  * Added in: v24.2.0, v22.18.0
@@ -24,6 +257,13 @@ const format = async (src, ast, opts) => {
24
257
  };
25
258
  const exportTable = opts.target === 'module' ? await collectCjsExports(ast.program) : null;
26
259
  await collectModuleIdentifiers(ast.program);
260
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
261
+ const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
262
+ const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
263
+ let pendingCjsTransforms = null;
264
+ if (shouldLowerCjs && opts.topLevelAwait === 'error' && containsTopLevelAwait) {
265
+ throw new Error('Top-level await is not supported when targeting CommonJS (set topLevelAwait to "wrap" or "preserve" to override).');
266
+ }
27
267
  if (opts.target === 'module' && opts.transformSyntax) {
28
268
  /**
29
269
  * Prepare ESM output by renaming `exports` to `__exports` and seeding an
@@ -108,6 +348,25 @@ void import.meta.filename;
108
348
  }
109
349
  }
110
350
  });
351
+ if (shouldLowerCjs) {
352
+ const {
353
+ importTransforms,
354
+ exportTransforms,
355
+ needsInterop
356
+ } = lowerEsmToCjs(ast.program, code, opts, containsTopLevelAwait);
357
+ pendingCjsTransforms = {
358
+ transforms: [...importTransforms, ...exportTransforms].sort((a, b) => a.start - b.start),
359
+ needsInterop
360
+ };
361
+ }
362
+ if (pendingCjsTransforms) {
363
+ for (const t of pendingCjsTransforms.transforms) {
364
+ code.overwrite(t.start, t.end, t.code);
365
+ }
366
+ if (pendingCjsTransforms.needsInterop) {
367
+ code.prepend(`${interopHelper}exports.__esModule = true;\n`);
368
+ }
369
+ }
111
370
  if (opts.target === 'module' && opts.transformSyntax && exportTable) {
112
371
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
113
372
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
@@ -137,6 +396,16 @@ void import.meta.filename;
137
396
  code.append(`\n${lines.join('\n')}\n`);
138
397
  }
139
398
  }
399
+ if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
400
+ const body = code.toString();
401
+ if (opts.topLevelAwait === 'wrap') {
402
+ const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
403
+ const setPromise = `const __setTla = target => {\n if (!target) return;\n const type = typeof target;\n if (type !== 'object' && type !== 'function') return;\n target.__tla = __tla;\n};\n`;
404
+ const attach = `__setTla(module.exports);\n__tla.then(resolved => __setTla(resolved), err => { throw err; });\n`;
405
+ return `${tlaPromise}${setPromise}${attach}`;
406
+ }
407
+ return `;(async () => {\n${body}\n})();\n`;
408
+ }
140
409
  return code.toString();
141
410
  };
142
411
  export { format };
@@ -1,3 +1,16 @@
1
+ const importMetaMainSupport = '(() => { const [__nmaj, __nmin] = process.versions.node.split(".").map(n => parseInt(n, 10) || 0); return (__nmaj > 24 || (__nmaj === 24 && __nmin >= 2) || (__nmaj === 22 && __nmin >= 18)); })()';
2
+ const importMetaMainShim = 'process.argv[1] === __filename';
3
+ const importMetaMainExpr = mode => {
4
+ switch (mode) {
5
+ case 'warn':
6
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (console.warn("import.meta.main is not supported before Node 22.18/24.2; falling back to shim."), ${importMetaMainShim}))`;
7
+ case 'error':
8
+ return `(${importMetaMainSupport} ? ${importMetaMainShim} : (() => { throw new Error("import.meta.main is not supported before Node 22.18/24.2"); })())`;
9
+ case 'shim':
10
+ default:
11
+ return importMetaMainShim;
12
+ }
13
+ };
1
14
  export const metaProperty = (node, parent, src, options) => {
2
15
  if (options.target === 'commonjs') {
3
16
  if (parent?.type !== 'MemberExpression') {
@@ -21,10 +34,15 @@ export const metaProperty = (node, parent, src, options) => {
21
34
  break;
22
35
  case 'resolve':
23
36
  /**
24
- * Should this be `require('node:url').pathToFileURL(require.resolve(<parsed specifier>)).href`?
37
+ * Map to require.resolve intentionally: matches CJS resolution semantics.
38
+ * Wrapping in pathToFileURL(...) would change the return shape (URL string)
39
+ * without truly emulating ESM import.meta.resolve rules.
25
40
  */
26
41
  src.update(parent.start, parent.end, 'require.resolve');
27
42
  break;
43
+ case 'main':
44
+ src.update(parent.start, parent.end, importMetaMainExpr(options.importMetaMain));
45
+ break;
28
46
  }
29
47
  }
30
48
  }
package/dist/module.js CHANGED
@@ -12,6 +12,7 @@ const defaultOptions = {
12
12
  rewriteSpecifier: undefined,
13
13
  dirFilename: 'inject',
14
14
  importMeta: 'shim',
15
+ importMetaMain: 'shim',
15
16
  requireSource: 'builtin',
16
17
  cjsDefault: 'auto',
17
18
  topLevelAwait: 'error',
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export type ModuleOptions = {
8
8
  rewriteSpecifier?: RewriteSpecifier;
9
9
  dirFilename?: 'inject' | 'preserve' | 'error';
10
10
  importMeta?: 'preserve' | 'shim' | 'error';
11
+ importMetaMain?: 'shim' | 'warn' | 'error';
11
12
  requireSource?: 'builtin' | 'create-require';
12
13
  cjsDefault?: 'module-exports' | 'auto' | 'none';
13
14
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/module",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Transforms differences between ES modules and CommonJS.",
5
5
  "type": "module",
6
6
  "main": "dist/module.js",