@knighted/module 1.0.0-rc.5 → 1.0.0-rc.6

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
@@ -99,7 +99,7 @@ invoked directly by node
99
99
  type ModuleOptions = {
100
100
  target: 'module' | 'commonjs'
101
101
  sourceType?: 'auto' | 'module' | 'commonjs'
102
- transformSyntax?: boolean
102
+ transformSyntax?: boolean | 'globals-only'
103
103
  liveBindings?: 'strict' | 'loose' | 'off'
104
104
  appendJsExtension?: 'off' | 'relative-only' | 'all'
105
105
  appendDirectoryIndex?: string | false
@@ -127,7 +127,7 @@ type ModuleOptions = {
127
127
  Behavior notes (defaults in parentheses)
128
128
 
129
129
  - `target` (`commonjs`): output module system.
130
- - `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass.
130
+ - `transformSyntax` (true): enable/disable the ESM↔CJS lowering pass; set to `'globals-only'` to rewrite module globals (`import.meta.*`, `__dirname`, `__filename`, `require.main` shims) while leaving import/export syntax untouched. In `'globals-only'`, no helpers are injected (e.g., `__requireResolve`), `require.resolve` rewrites to `import.meta.resolve`, and `idiomaticExports` is skipped. See [globals-only](#globals-only-scope).
131
131
  - `liveBindings` (`strict`): getter-based live bindings, or snapshot (`loose`/`off`).
132
132
  - `appendJsExtension` (`relative-only` when targeting ESM): append `.js` to relative specifiers; never touches bare specifiers.
133
133
  - `appendDirectoryIndex` (`index.js`): when a relative specifier ends with a slash, append this index filename (set `false` to disable).
@@ -151,6 +151,13 @@ See [docs/esm-to-cjs.md](docs/esm-to-cjs.md) for deeper notes on live bindings,
151
151
  > [!NOTE]
152
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`.
153
153
 
154
+ ### Globals-only scope
155
+
156
+ - Rewrites module globals (`import.meta.*`, `__dirname`, `__filename`, `require.main` shims) for the target side.
157
+ - Optional specifier rewrites still run (`rewriteSpecifier`, `appendJsExtension`, `appendDirectoryIndex`).
158
+ - Leaves imports/exports and interop untouched (no export bag, no idiomaticExports, no live-binding synthesis, no helpers like `__requireResolve`).
159
+ - CJS→ESM: `require.resolve` maps to `import.meta.resolve` (URL return, ESM resolver) and may differ from CJS resolution. ESM→CJS: `import.meta` maps to CJS globals; no import lowering.
160
+
154
161
  ### Diagnostics callback example
155
162
 
156
163
  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):
@@ -183,7 +190,7 @@ console.log(diagnostics)
183
190
 
184
191
  ## Pre-`tsc` transforms for TypeScript diagnostics
185
192
 
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.
193
+ 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. For a specifier + globals-only pass that leaves import/export syntax for `tsc`, set `transformSyntax: 'globals-only'`.
187
194
 
188
195
  Minimal flow:
189
196
 
@@ -16,6 +16,36 @@ 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
18
  const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
19
+ const expressionHasRequireCall = (node, shadowed) => {
20
+ let found = false;
21
+ const walkNode = n => {
22
+ if (!n || found) return;
23
+ if (n.type === 'CallExpression' && n.callee?.type === 'Identifier' && n.callee.name === 'require' && !shadowed.has('require')) {
24
+ found = true;
25
+ return;
26
+ }
27
+ if (n.type === 'CallExpression' && n.callee?.type === 'MemberExpression' && n.callee.object?.type === 'Identifier' && n.callee.object.name === 'require' && !shadowed.has('require')) {
28
+ found = true;
29
+ return;
30
+ }
31
+ const keys = Object.keys(n);
32
+ for (const key of keys) {
33
+ const value = n[key];
34
+ if (!value) continue;
35
+ if (Array.isArray(value)) {
36
+ for (const item of value) {
37
+ if (item && typeof item === 'object') walkNode(item);
38
+ if (found) return;
39
+ }
40
+ } else if (value && typeof value === 'object') {
41
+ walkNode(value);
42
+ if (found) return;
43
+ }
44
+ }
45
+ };
46
+ walkNode(node);
47
+ return found;
48
+ };
19
49
  const exportAssignment = (name, expr, live) => {
20
50
  const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
21
51
  if (live === 'strict') {
@@ -397,14 +427,20 @@ const format = async (src, ast, opts) => {
397
427
  loc
398
428
  });
399
429
  };
430
+ const transformMode = opts.transformSyntax;
431
+ const fullTransform = transformMode === true;
400
432
  const moduleIdentifiers = await (0, _identifiers.collectModuleIdentifiers)(ast.program);
401
433
  const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
402
- if (opts.target === 'module' && opts.transformSyntax) {
434
+ if (opts.target === 'module' && fullTransform) {
403
435
  if (shadowedBindings.has('module') || shadowedBindings.has('exports')) {
404
436
  throw new Error('Cannot transform to ESM: module or exports is shadowed in module scope.');
405
437
  }
406
438
  }
407
439
  const exportTable = opts.target === 'module' ? await (0, _exports.collectCjsExports)(ast.program) : null;
440
+ const idiomaticMode = opts.target === 'module' && fullTransform ? opts.idiomaticExports ?? 'safe' : 'off';
441
+ let useExportsBag = fullTransform;
442
+ let idiomaticPlan = null;
443
+ let idiomaticFallbackReason;
408
444
  if (opts.target === 'module' && exportTable) {
409
445
  const hasExportsVia = [...exportTable.values()].some(entry => entry.via.has('exports'));
410
446
  const hasModuleExportsVia = [...exportTable.values()].some(entry => entry.via.has('module.exports'));
@@ -416,15 +452,163 @@ const format = async (src, ast, opts) => {
416
452
  end: firstExports?.end ?? 0
417
453
  });
418
454
  }
455
+ const reservedExports = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
456
+ const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name) && !reservedExports.has(name);
457
+ const isAllowedRhs = node => {
458
+ return node.type === 'Identifier' || node.type === 'Literal' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'ClassExpression';
459
+ };
460
+ const buildIdiomaticPlan = () => {
461
+ if (idiomaticMode === 'off') return {
462
+ ok: false,
463
+ reason: 'disabled'
464
+ };
465
+ const entries = [...exportTable.values()];
466
+ if (!entries.length) return {
467
+ ok: false,
468
+ reason: 'no-exports'
469
+ };
470
+ if (exportTable.hasUnsupportedExportWrite) {
471
+ return {
472
+ ok: false,
473
+ reason: 'unsupported-left'
474
+ };
475
+ }
476
+ const viaSet = new Set();
477
+ for (const entry of entries) {
478
+ entry.via.forEach(v => viaSet.add(v));
479
+ if (entry.hasGetter) return {
480
+ ok: false,
481
+ reason: 'getter-present'
482
+ };
483
+ if (entry.reassignments.length) return {
484
+ ok: false,
485
+ reason: 'reassignment'
486
+ };
487
+ if (entry.hasNonTopLevelWrite) return {
488
+ ok: false,
489
+ reason: 'non-top-level'
490
+ };
491
+ if (entry.writes.length !== 1) return {
492
+ ok: false,
493
+ reason: 'multiple-writes'
494
+ };
495
+ if (!isValidExportName(entry.key)) return {
496
+ ok: false,
497
+ reason: 'non-identifier-key'
498
+ };
499
+ }
500
+ if (viaSet.size > 1) return {
501
+ ok: false,
502
+ reason: 'mixed-exports'
503
+ };
504
+ const replacements = [];
505
+ const exportsOut = [];
506
+ const seen = new Set();
507
+ const requireShadowed = shadowedBindings;
508
+ const rhsSourceFor = node => {
509
+ const raw = code.slice(node.start, node.end);
510
+ return raw.replace(/\b__dirname\b/g, 'import.meta.dirname').replace(/\b__filename\b/g, 'import.meta.filename');
511
+ };
512
+ for (const entry of entries) {
513
+ const write = entry.writes[0];
514
+ if (write.type !== 'AssignmentExpression') {
515
+ return {
516
+ ok: false,
517
+ reason: 'unsupported-write-kind'
518
+ };
519
+ }
520
+ const left = write.left;
521
+ if (left.type !== 'MemberExpression' || left.computed || left.property.type !== 'Identifier') {
522
+ return {
523
+ ok: false,
524
+ reason: 'unsupported-left'
525
+ };
526
+ }
527
+ const base = left.object;
528
+ const propName = left.property.name;
529
+ const baseIsExports = base.type === 'Identifier' && base.name === 'exports';
530
+ const baseIsModuleExports = base.type === 'MemberExpression' && base.object.type === 'Identifier' && base.object.name === 'module' && base.property.type === 'Identifier' && base.property.name === 'exports';
531
+ if (!baseIsExports && !baseIsModuleExports) {
532
+ return {
533
+ ok: false,
534
+ reason: 'unsupported-base'
535
+ };
536
+ }
537
+ const rhs = write.right;
538
+ if (!isAllowedRhs(rhs)) return {
539
+ ok: false,
540
+ reason: 'unsupported-rhs'
541
+ };
542
+ if (expressionHasRequireCall(rhs, requireShadowed)) {
543
+ return {
544
+ ok: false,
545
+ reason: 'rhs-require'
546
+ };
547
+ }
548
+ const rhsSrc = rhsSourceFor(rhs);
549
+ if (propName === 'exports' && baseIsModuleExports) {
550
+ // module.exports = ... handles default
551
+ if (seen.has('default')) return {
552
+ ok: false,
553
+ reason: 'duplicate-default'
554
+ };
555
+ seen.add('default');
556
+ exportsOut.push(`export default ${rhsSrc};`);
557
+ } else {
558
+ if (seen.has(propName)) return {
559
+ ok: false,
560
+ reason: 'duplicate-key'
561
+ };
562
+ seen.add(propName);
563
+ if (rhs.type === 'Identifier') {
564
+ const rhsId = rhsSourceFor(rhs);
565
+ if (rhsId === rhs.name) {
566
+ exportsOut.push(`export { ${rhsId} as ${propName} };`);
567
+ } else {
568
+ exportsOut.push(`export const ${propName} = ${rhsId};`);
569
+ }
570
+ } else {
571
+ exportsOut.push(`export const ${propName} = ${rhsSrc};`);
572
+ }
573
+ }
574
+ replacements.push({
575
+ start: write.start,
576
+ end: write.end
577
+ });
578
+ }
579
+ if (!seen.size) return {
580
+ ok: false,
581
+ reason: 'no-seen'
582
+ };
583
+ return {
584
+ ok: true,
585
+ plan: {
586
+ replacements,
587
+ exports: exportsOut
588
+ }
589
+ };
590
+ };
591
+ if (idiomaticMode !== 'off') {
592
+ const res = buildIdiomaticPlan();
593
+ if (res.ok && res.plan) {
594
+ useExportsBag = false;
595
+ idiomaticPlan = res.plan;
596
+ } else if (res.reason) {
597
+ idiomaticFallbackReason = res.reason;
598
+ }
599
+ }
419
600
  }
420
- const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
601
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && fullTransform;
421
602
  const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
603
+ if (idiomaticFallbackReason && idiomaticMode !== 'off') {
604
+ warnOnce('idiomatic-exports-fallback', `Idiomatic exports disabled for this file: ${idiomaticFallbackReason}. Falling back to helper exports.`);
605
+ }
422
606
  const requireMainStrategy = opts.requireMainStrategy ?? 'import-meta-main';
423
607
  let requireMainNeedsRealpath = false;
424
608
  let needsRequireResolveHelper = false;
425
609
  const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require';
426
- const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
427
- const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
610
+ const shouldLowerCjs = opts.target === 'commonjs' && fullTransform;
611
+ const shouldRaiseEsm = opts.target === 'module' && fullTransform;
428
612
  let hoistedImports = [];
429
613
  let hoistedStatements = [];
430
614
  let pendingRequireTransforms = [];
@@ -574,7 +758,7 @@ const format = async (src, ast, opts) => {
574
758
  onDiagnostic: (codeId, message, loc) => {
575
759
  if (shouldRaiseEsm) warnOnce(codeId, message, loc);
576
760
  }
577
- });
761
+ }, useExportsBag, fullTransform);
578
762
  }
579
763
  if (shouldRaiseEsm && node.type === 'ThisExpression') {
580
764
  const bindsThis = ancestor => {
@@ -594,7 +778,8 @@ const format = async (src, ast, opts) => {
594
778
  code,
595
779
  opts,
596
780
  meta: exportsMeta,
597
- shadowed: shadowedBindings
781
+ shadowed: shadowedBindings,
782
+ useExportsBag
598
783
  });
599
784
  }
600
785
  }
@@ -604,6 +789,20 @@ const format = async (src, ast, opts) => {
604
789
  code.overwrite(t.start, t.end, t.code);
605
790
  }
606
791
  }
792
+ if (!useExportsBag && idiomaticPlan) {
793
+ if (idiomaticPlan.exports.length === idiomaticPlan.replacements.length) {
794
+ idiomaticPlan.replacements.forEach((rep, idx) => {
795
+ code.overwrite(rep.start, rep.end, idiomaticPlan.exports[idx]);
796
+ });
797
+ } else {
798
+ for (const rep of idiomaticPlan.replacements) {
799
+ code.overwrite(rep.start, rep.end, ';');
800
+ }
801
+ if (idiomaticPlan.exports.length) {
802
+ code.append(`\n${idiomaticPlan.exports.join('\n')}\n`);
803
+ }
804
+ }
805
+ }
607
806
  if (shouldLowerCjs) {
608
807
  const {
609
808
  importTransforms,
@@ -623,7 +822,7 @@ const format = async (src, ast, opts) => {
623
822
  code.prepend(`${interopHelper}exports.__esModule = true;\n`);
624
823
  }
625
824
  }
626
- if (opts.target === 'module' && opts.transformSyntax && exportTable) {
825
+ if (useExportsBag && opts.target === 'module' && fullTransform && exportTable) {
627
826
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
628
827
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
629
828
  const accessProp = name => isValidExportName(name) ? `${_exports.exportsRename}.${name}` : `${_exports.exportsRename}[${JSON.stringify(name)}]`;
@@ -683,7 +882,7 @@ const format = async (src, ast, opts) => {
683
882
  code.append(`\n${lines.join('\n')}\n`);
684
883
  }
685
884
  }
686
- if (shouldRaiseEsm && opts.transformSyntax) {
885
+ if (shouldRaiseEsm && fullTransform) {
687
886
  const importPrelude = [];
688
887
  if (needsCreateRequire || needsRequireResolveHelper) {
689
888
  importPrelude.push('import { createRequire } from "node:module";\n');
@@ -714,12 +913,14 @@ const format = async (src, ast, opts) => {
714
913
  const resolved = req.resolve(id, parent);
715
914
  return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
716
915
  };\n` : '';
717
- const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}let ${_exports.exportsRename} = {};
718
- void import.meta.filename;
916
+ const exportsBagInit = useExportsBag ? `let ${_exports.exportsRename} = {};
917
+ ` : '';
918
+ const modulePrelude = '';
919
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}void import.meta.filename;
719
920
  `;
720
921
  code.prepend(prelude);
721
922
  }
722
- if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
923
+ if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) {
723
924
  const body = code.toString();
724
925
  if (opts.topLevelAwait === 'wrap') {
725
926
  const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
@@ -12,7 +12,8 @@ const identifier = ({
12
12
  code,
13
13
  opts,
14
14
  meta,
15
- shadowed
15
+ shadowed,
16
+ useExportsBag = true
16
17
  }) => {
17
18
  if (opts.target === 'module') {
18
19
  const {
@@ -33,7 +34,7 @@ const identifier = ({
33
34
  case 'exports':
34
35
  {
35
36
  const parent = ancestors[ancestors.length - 2];
36
- if (opts.transformSyntax) {
37
+ if (opts.transformSyntax && useExportsBag) {
37
38
  if (parent.type === 'AssignmentExpression' && parent.left === node) {
38
39
  // The code is reassigning `exports` to something else.
39
40
 
@@ -5,15 +5,33 @@ 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, extras) => {
8
+ const memberExpression = (node, parent, src, options, shadowed, extras, useExportsBag = true, rewriteExports = true) => {
9
9
  if (options.target === 'module') {
10
- if (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name)) {
11
- return;
10
+ if (rewriteExports && !useExportsBag) {
11
+ if (parent?.type === 'MemberExpression' && parent.object === node && parent.property.type === 'Identifier') {
12
+ const baseIsExportsIdent = node.object.type === 'Identifier' && node.object.name === 'exports';
13
+ const baseIsModuleExports = node.object.type === 'Identifier' && node.object.name === 'module' && node.property.type === 'Identifier' && node.property.name === 'exports';
14
+ if (baseIsExportsIdent || baseIsModuleExports) {
15
+ src.update(parent.start, parent.end, parent.property.name);
16
+ return;
17
+ }
18
+ }
19
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
20
+ src.update(node.start, node.end, 'undefined');
21
+ return;
22
+ }
12
23
  }
13
- if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
14
- src.update(node.start, node.end, _exports.exportsRename);
24
+ if (rewriteExports && (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name))) {
15
25
  return;
16
26
  }
27
+ if (rewriteExports) {
28
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
29
+ if (useExportsBag) {
30
+ src.update(node.start, node.end, _exports.exportsRename);
31
+ }
32
+ return;
33
+ }
34
+ }
17
35
  if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'require') {
18
36
  const {
19
37
  start,
@@ -32,6 +50,10 @@ const memberExpression = (node, parent, src, options, shadowed, extras) => {
32
50
  src.update(start, end, 'import.meta.main');
33
51
  break;
34
52
  case 'resolve':
53
+ if (options.transformSyntax !== true) {
54
+ src.update(start, end, 'import.meta.resolve');
55
+ return;
56
+ }
35
57
  extras?.onRequireResolve?.();
36
58
  src.update(start, end, extras?.requireResolveName ?? 'import.meta.resolve');
37
59
  break;
@@ -9,5 +9,5 @@ type MemberExpressionExtras = {
9
9
  end: number;
10
10
  }) => void;
11
11
  };
12
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
12
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras, useExportsBag?: boolean, rewriteExports?: boolean) => void;
13
13
  export {};
@@ -160,6 +160,7 @@ const defaultOptions = {
160
160
  requireSource: 'builtin',
161
161
  nestedRequireStrategy: 'create-require',
162
162
  cjsDefault: 'auto',
163
+ idiomaticExports: 'safe',
163
164
  topLevelAwait: 'error',
164
165
  out: undefined,
165
166
  inPlace: false
@@ -6,8 +6,13 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
- /** Enable syntax transforms beyond parsing. */
10
- transformSyntax?: boolean;
9
+ /**
10
+ * Enable syntax transforms beyond parsing.
11
+ * - true: full CJS↔ESM lowering/raising
12
+ * - 'globals-only': rewrite module-global differences (import.meta, __dirname/filename, require.main shims) while leaving import/export shapes untouched
13
+ * - false/undefined: no syntax transforms
14
+ */
15
+ transformSyntax?: boolean | 'globals-only';
11
16
  /** How to emit live bindings for ESM exports. */
12
17
  liveBindings?: 'strict' | 'loose' | 'off';
13
18
  /** Rewrite import specifiers (e.g. add extensions). */
@@ -32,6 +37,8 @@ export type ModuleOptions = {
32
37
  nestedRequireStrategy?: 'create-require' | 'dynamic-import';
33
38
  /** Default interop style for CommonJS default imports. */
34
39
  cjsDefault?: 'module-exports' | 'auto' | 'none';
40
+ /** Emit idiomatic exports when raising CJS to ESM. */
41
+ idiomaticExports?: 'off' | 'safe' | 'aggressive';
35
42
  /** Handling for top-level await constructs. */
36
43
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
37
44
  /** Optional diagnostics sink for warnings/errors emitted during transform. */
@@ -67,6 +74,7 @@ export type CjsExport = {
67
74
  via: Set<'exports' | 'module.exports'>;
68
75
  reassignments: SpannedNode[];
69
76
  hasGetter?: boolean;
77
+ hasNonTopLevelWrite?: boolean;
70
78
  };
71
79
  export type IdentMeta = {
72
80
  declare: SpannedNode[];
@@ -69,6 +69,12 @@ const collectCjsExports = async ast => {
69
69
  const localToExport = new Map();
70
70
  const aliases = new Map();
71
71
  const literals = new Map();
72
+ let hasUnsupportedExportWrite = false;
73
+ const isTopLevelWrite = ancestors => {
74
+ const parent = ancestors[ancestors.length - 2];
75
+ const grandparent = ancestors[ancestors.length - 3];
76
+ return grandparent?.type === 'Program' && (parent?.type === 'ExpressionStatement' || parent?.type === 'VariableDeclaration');
77
+ };
72
78
  const addExport = (ref, node, rhs, options) => {
73
79
  const entry = exportsMap.get(ref.key) ?? {
74
80
  key: ref.key,
@@ -81,6 +87,9 @@ const collectCjsExports = async ast => {
81
87
  if (options?.hasGetter) {
82
88
  entry.hasGetter = true;
83
89
  }
90
+ if (options?.topLevel === false) {
91
+ entry.hasNonTopLevelWrite = true;
92
+ }
84
93
  if (rhs) {
85
94
  entry.fromIdentifier ??= rhs.name;
86
95
  const set = localToExport.get(rhs.name) ?? new Set();
@@ -110,9 +119,15 @@ const collectCjsExports = async ast => {
110
119
  const target = resolveExportTarget(node.left, aliases, literals, ancestors);
111
120
  if (target) {
112
121
  const rhsIdent = node.right.type === 'Identifier' ? node.right : undefined;
113
- addExport(target, node, rhsIdent);
122
+ const topLevel = isTopLevelWrite(ancestors);
123
+ addExport(target, node, rhsIdent, {
124
+ topLevel
125
+ });
114
126
  return;
115
127
  }
128
+ if (node.left.type === 'MemberExpression' && resolveBase(node.left.object, aliases, ancestors)) {
129
+ hasUnsupportedExportWrite = true;
130
+ }
116
131
  if (node.left.type === 'Identifier') {
117
132
  const keys = localToExport.get(node.left.name);
118
133
  if (keys) {
@@ -129,7 +144,12 @@ const collectCjsExports = async ast => {
129
144
  const findExportRefs = pattern => {
130
145
  if (pattern.type === 'MemberExpression') {
131
146
  const ref = resolveExportTarget(pattern, aliases, literals, ancestors);
132
- if (ref) addExport(ref, node);
147
+ if (ref) {
148
+ const topLevel = isTopLevelWrite(ancestors);
149
+ addExport(ref, node, undefined, {
150
+ topLevel
151
+ });
152
+ }
133
153
  return;
134
154
  }
135
155
  if (pattern.type === 'ObjectPattern') {
@@ -171,10 +191,13 @@ const collectCjsExports = async ast => {
171
191
  if (prop.value.type === 'Identifier') {
172
192
  rhsIdent = prop.value;
173
193
  }
194
+ const topLevel = isTopLevelWrite(ancestors);
174
195
  addExport({
175
196
  key: keyName,
176
197
  via: ref
177
- }, node, rhsIdent);
198
+ }, node, rhsIdent, {
199
+ topLevel
200
+ });
178
201
  }
179
202
  }
180
203
  }
@@ -203,11 +226,13 @@ const collectCjsExports = async ast => {
203
226
  // Setter-only doesn’t create a readable export; ignore beyond marking write
204
227
  }
205
228
  }
229
+ const topLevel = isTopLevelWrite(ancestors);
206
230
  addExport({
207
231
  key: keyName,
208
232
  via: target
209
233
  }, node, rhsIdent, {
210
- hasGetter
234
+ hasGetter,
235
+ topLevel
211
236
  });
212
237
  }
213
238
 
@@ -234,17 +259,20 @@ const collectCjsExports = async ast => {
234
259
  hasGetter = true;
235
260
  }
236
261
  }
262
+ const topLevel = isTopLevelWrite(ancestors);
237
263
  addExport({
238
264
  key: keyName,
239
265
  via: target
240
266
  }, node, rhsIdent, {
241
- hasGetter
267
+ hasGetter,
268
+ topLevel
242
269
  });
243
270
  }
244
271
  }
245
272
  }
246
273
  }
247
274
  });
275
+ exportsMap.hasUnsupportedExportWrite = hasUnsupportedExportWrite;
248
276
  return exportsMap;
249
277
  };
250
278
  exports.collectCjsExports = collectCjsExports;
package/dist/format.js CHANGED
@@ -9,6 +9,36 @@ import { collectModuleIdentifiers } from '#utils/identifiers.js';
9
9
  import { isIdentifierName } from '#helpers/identifier.js';
10
10
  import { ancestorWalk } from '#walk';
11
11
  const isValidIdent = name => /^[$A-Z_a-z][$\w]*$/.test(name);
12
+ const expressionHasRequireCall = (node, shadowed) => {
13
+ let found = false;
14
+ const walkNode = n => {
15
+ if (!n || found) return;
16
+ if (n.type === 'CallExpression' && n.callee?.type === 'Identifier' && n.callee.name === 'require' && !shadowed.has('require')) {
17
+ found = true;
18
+ return;
19
+ }
20
+ if (n.type === 'CallExpression' && n.callee?.type === 'MemberExpression' && n.callee.object?.type === 'Identifier' && n.callee.object.name === 'require' && !shadowed.has('require')) {
21
+ found = true;
22
+ return;
23
+ }
24
+ const keys = Object.keys(n);
25
+ for (const key of keys) {
26
+ const value = n[key];
27
+ if (!value) continue;
28
+ if (Array.isArray(value)) {
29
+ for (const item of value) {
30
+ if (item && typeof item === 'object') walkNode(item);
31
+ if (found) return;
32
+ }
33
+ } else if (value && typeof value === 'object') {
34
+ walkNode(value);
35
+ if (found) return;
36
+ }
37
+ }
38
+ };
39
+ walkNode(node);
40
+ return found;
41
+ };
12
42
  const exportAssignment = (name, expr, live) => {
13
43
  const prop = isValidIdent(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
14
44
  if (live === 'strict') {
@@ -390,14 +420,20 @@ const format = async (src, ast, opts) => {
390
420
  loc
391
421
  });
392
422
  };
423
+ const transformMode = opts.transformSyntax;
424
+ const fullTransform = transformMode === true;
393
425
  const moduleIdentifiers = await collectModuleIdentifiers(ast.program);
394
426
  const shadowedBindings = new Set([...moduleIdentifiers.entries()].filter(([, meta]) => meta.declare.length > 0).map(([name]) => name));
395
- if (opts.target === 'module' && opts.transformSyntax) {
427
+ if (opts.target === 'module' && fullTransform) {
396
428
  if (shadowedBindings.has('module') || shadowedBindings.has('exports')) {
397
429
  throw new Error('Cannot transform to ESM: module or exports is shadowed in module scope.');
398
430
  }
399
431
  }
400
432
  const exportTable = opts.target === 'module' ? await collectCjsExports(ast.program) : null;
433
+ const idiomaticMode = opts.target === 'module' && fullTransform ? opts.idiomaticExports ?? 'safe' : 'off';
434
+ let useExportsBag = fullTransform;
435
+ let idiomaticPlan = null;
436
+ let idiomaticFallbackReason;
401
437
  if (opts.target === 'module' && exportTable) {
402
438
  const hasExportsVia = [...exportTable.values()].some(entry => entry.via.has('exports'));
403
439
  const hasModuleExportsVia = [...exportTable.values()].some(entry => entry.via.has('module.exports'));
@@ -409,15 +445,163 @@ const format = async (src, ast, opts) => {
409
445
  end: firstExports?.end ?? 0
410
446
  });
411
447
  }
448
+ const reservedExports = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']);
449
+ const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name) && !reservedExports.has(name);
450
+ const isAllowedRhs = node => {
451
+ return node.type === 'Identifier' || node.type === 'Literal' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'ClassExpression';
452
+ };
453
+ const buildIdiomaticPlan = () => {
454
+ if (idiomaticMode === 'off') return {
455
+ ok: false,
456
+ reason: 'disabled'
457
+ };
458
+ const entries = [...exportTable.values()];
459
+ if (!entries.length) return {
460
+ ok: false,
461
+ reason: 'no-exports'
462
+ };
463
+ if (exportTable.hasUnsupportedExportWrite) {
464
+ return {
465
+ ok: false,
466
+ reason: 'unsupported-left'
467
+ };
468
+ }
469
+ const viaSet = new Set();
470
+ for (const entry of entries) {
471
+ entry.via.forEach(v => viaSet.add(v));
472
+ if (entry.hasGetter) return {
473
+ ok: false,
474
+ reason: 'getter-present'
475
+ };
476
+ if (entry.reassignments.length) return {
477
+ ok: false,
478
+ reason: 'reassignment'
479
+ };
480
+ if (entry.hasNonTopLevelWrite) return {
481
+ ok: false,
482
+ reason: 'non-top-level'
483
+ };
484
+ if (entry.writes.length !== 1) return {
485
+ ok: false,
486
+ reason: 'multiple-writes'
487
+ };
488
+ if (!isValidExportName(entry.key)) return {
489
+ ok: false,
490
+ reason: 'non-identifier-key'
491
+ };
492
+ }
493
+ if (viaSet.size > 1) return {
494
+ ok: false,
495
+ reason: 'mixed-exports'
496
+ };
497
+ const replacements = [];
498
+ const exportsOut = [];
499
+ const seen = new Set();
500
+ const requireShadowed = shadowedBindings;
501
+ const rhsSourceFor = node => {
502
+ const raw = code.slice(node.start, node.end);
503
+ return raw.replace(/\b__dirname\b/g, 'import.meta.dirname').replace(/\b__filename\b/g, 'import.meta.filename');
504
+ };
505
+ for (const entry of entries) {
506
+ const write = entry.writes[0];
507
+ if (write.type !== 'AssignmentExpression') {
508
+ return {
509
+ ok: false,
510
+ reason: 'unsupported-write-kind'
511
+ };
512
+ }
513
+ const left = write.left;
514
+ if (left.type !== 'MemberExpression' || left.computed || left.property.type !== 'Identifier') {
515
+ return {
516
+ ok: false,
517
+ reason: 'unsupported-left'
518
+ };
519
+ }
520
+ const base = left.object;
521
+ const propName = left.property.name;
522
+ const baseIsExports = base.type === 'Identifier' && base.name === 'exports';
523
+ const baseIsModuleExports = base.type === 'MemberExpression' && base.object.type === 'Identifier' && base.object.name === 'module' && base.property.type === 'Identifier' && base.property.name === 'exports';
524
+ if (!baseIsExports && !baseIsModuleExports) {
525
+ return {
526
+ ok: false,
527
+ reason: 'unsupported-base'
528
+ };
529
+ }
530
+ const rhs = write.right;
531
+ if (!isAllowedRhs(rhs)) return {
532
+ ok: false,
533
+ reason: 'unsupported-rhs'
534
+ };
535
+ if (expressionHasRequireCall(rhs, requireShadowed)) {
536
+ return {
537
+ ok: false,
538
+ reason: 'rhs-require'
539
+ };
540
+ }
541
+ const rhsSrc = rhsSourceFor(rhs);
542
+ if (propName === 'exports' && baseIsModuleExports) {
543
+ // module.exports = ... handles default
544
+ if (seen.has('default')) return {
545
+ ok: false,
546
+ reason: 'duplicate-default'
547
+ };
548
+ seen.add('default');
549
+ exportsOut.push(`export default ${rhsSrc};`);
550
+ } else {
551
+ if (seen.has(propName)) return {
552
+ ok: false,
553
+ reason: 'duplicate-key'
554
+ };
555
+ seen.add(propName);
556
+ if (rhs.type === 'Identifier') {
557
+ const rhsId = rhsSourceFor(rhs);
558
+ if (rhsId === rhs.name) {
559
+ exportsOut.push(`export { ${rhsId} as ${propName} };`);
560
+ } else {
561
+ exportsOut.push(`export const ${propName} = ${rhsId};`);
562
+ }
563
+ } else {
564
+ exportsOut.push(`export const ${propName} = ${rhsSrc};`);
565
+ }
566
+ }
567
+ replacements.push({
568
+ start: write.start,
569
+ end: write.end
570
+ });
571
+ }
572
+ if (!seen.size) return {
573
+ ok: false,
574
+ reason: 'no-seen'
575
+ };
576
+ return {
577
+ ok: true,
578
+ plan: {
579
+ replacements,
580
+ exports: exportsOut
581
+ }
582
+ };
583
+ };
584
+ if (idiomaticMode !== 'off') {
585
+ const res = buildIdiomaticPlan();
586
+ if (res.ok && res.plan) {
587
+ useExportsBag = false;
588
+ idiomaticPlan = res.plan;
589
+ } else if (res.reason) {
590
+ idiomaticFallbackReason = res.reason;
591
+ }
592
+ }
412
593
  }
413
- const shouldCheckTopLevelAwait = opts.target === 'commonjs' && opts.transformSyntax;
594
+ const shouldCheckTopLevelAwait = opts.target === 'commonjs' && fullTransform;
414
595
  const containsTopLevelAwait = shouldCheckTopLevelAwait ? hasTopLevelAwait(ast.program) : false;
596
+ if (idiomaticFallbackReason && idiomaticMode !== 'off') {
597
+ warnOnce('idiomatic-exports-fallback', `Idiomatic exports disabled for this file: ${idiomaticFallbackReason}. Falling back to helper exports.`);
598
+ }
415
599
  const requireMainStrategy = opts.requireMainStrategy ?? 'import-meta-main';
416
600
  let requireMainNeedsRealpath = false;
417
601
  let needsRequireResolveHelper = false;
418
602
  const nestedRequireStrategy = opts.nestedRequireStrategy ?? 'create-require';
419
- const shouldLowerCjs = opts.target === 'commonjs' && opts.transformSyntax;
420
- const shouldRaiseEsm = opts.target === 'module' && opts.transformSyntax;
603
+ const shouldLowerCjs = opts.target === 'commonjs' && fullTransform;
604
+ const shouldRaiseEsm = opts.target === 'module' && fullTransform;
421
605
  let hoistedImports = [];
422
606
  let hoistedStatements = [];
423
607
  let pendingRequireTransforms = [];
@@ -567,7 +751,7 @@ const format = async (src, ast, opts) => {
567
751
  onDiagnostic: (codeId, message, loc) => {
568
752
  if (shouldRaiseEsm) warnOnce(codeId, message, loc);
569
753
  }
570
- });
754
+ }, useExportsBag, fullTransform);
571
755
  }
572
756
  if (shouldRaiseEsm && node.type === 'ThisExpression') {
573
757
  const bindsThis = ancestor => {
@@ -587,7 +771,8 @@ const format = async (src, ast, opts) => {
587
771
  code,
588
772
  opts,
589
773
  meta: exportsMeta,
590
- shadowed: shadowedBindings
774
+ shadowed: shadowedBindings,
775
+ useExportsBag
591
776
  });
592
777
  }
593
778
  }
@@ -597,6 +782,20 @@ const format = async (src, ast, opts) => {
597
782
  code.overwrite(t.start, t.end, t.code);
598
783
  }
599
784
  }
785
+ if (!useExportsBag && idiomaticPlan) {
786
+ if (idiomaticPlan.exports.length === idiomaticPlan.replacements.length) {
787
+ idiomaticPlan.replacements.forEach((rep, idx) => {
788
+ code.overwrite(rep.start, rep.end, idiomaticPlan.exports[idx]);
789
+ });
790
+ } else {
791
+ for (const rep of idiomaticPlan.replacements) {
792
+ code.overwrite(rep.start, rep.end, ';');
793
+ }
794
+ if (idiomaticPlan.exports.length) {
795
+ code.append(`\n${idiomaticPlan.exports.join('\n')}\n`);
796
+ }
797
+ }
798
+ }
600
799
  if (shouldLowerCjs) {
601
800
  const {
602
801
  importTransforms,
@@ -616,7 +815,7 @@ const format = async (src, ast, opts) => {
616
815
  code.prepend(`${interopHelper}exports.__esModule = true;\n`);
617
816
  }
618
817
  }
619
- if (opts.target === 'module' && opts.transformSyntax && exportTable) {
818
+ if (useExportsBag && opts.target === 'module' && fullTransform && exportTable) {
620
819
  const isValidExportName = name => /^[$A-Z_a-z][$\w]*$/.test(name);
621
820
  const asExportName = name => isValidExportName(name) ? name : JSON.stringify(name);
622
821
  const accessProp = name => isValidExportName(name) ? `${exportsRename}.${name}` : `${exportsRename}[${JSON.stringify(name)}]`;
@@ -676,7 +875,7 @@ const format = async (src, ast, opts) => {
676
875
  code.append(`\n${lines.join('\n')}\n`);
677
876
  }
678
877
  }
679
- if (shouldRaiseEsm && opts.transformSyntax) {
878
+ if (shouldRaiseEsm && fullTransform) {
680
879
  const importPrelude = [];
681
880
  if (needsCreateRequire || needsRequireResolveHelper) {
682
881
  importPrelude.push('import { createRequire } from "node:module";\n');
@@ -707,12 +906,14 @@ const format = async (src, ast, opts) => {
707
906
  const resolved = req.resolve(id, parent);
708
907
  return resolved.startsWith("file://") ? fileURLToPath(resolved) : resolved;
709
908
  };\n` : '';
710
- const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}let ${exportsRename} = {};
711
- void import.meta.filename;
909
+ const exportsBagInit = useExportsBag ? `let ${exportsRename} = {};
910
+ ` : '';
911
+ const modulePrelude = '';
912
+ const prelude = `${importPrelude.join('')}${importPrelude.length ? '\n' : ''}${setupPrelude.join('')}${setupPrelude.length ? '\n' : ''}${requireInit}${requireResolveInit}${exportsBagInit}${modulePrelude}void import.meta.filename;
712
913
  `;
713
914
  code.prepend(prelude);
714
915
  }
715
- if (opts.target === 'commonjs' && opts.transformSyntax && containsTopLevelAwait) {
916
+ if (opts.target === 'commonjs' && fullTransform && containsTopLevelAwait) {
716
917
  const body = code.toString();
717
918
  if (opts.topLevelAwait === 'wrap') {
718
919
  const tlaPromise = `const __tla = (async () => {\n${body}\nreturn module.exports;\n})();\n`;
@@ -6,7 +6,8 @@ export const identifier = ({
6
6
  code,
7
7
  opts,
8
8
  meta,
9
- shadowed
9
+ shadowed,
10
+ useExportsBag = true
10
11
  }) => {
11
12
  if (opts.target === 'module') {
12
13
  const {
@@ -27,7 +28,7 @@ export const identifier = ({
27
28
  case 'exports':
28
29
  {
29
30
  const parent = ancestors[ancestors.length - 2];
30
- if (opts.transformSyntax) {
31
+ if (opts.transformSyntax && useExportsBag) {
31
32
  if (parent.type === 'AssignmentExpression' && parent.left === node) {
32
33
  // The code is reassigning `exports` to something else.
33
34
 
@@ -1,13 +1,31 @@
1
1
  import { exportsRename } from '#utils/exports.js';
2
- export const memberExpression = (node, parent, src, options, shadowed, extras) => {
2
+ export const memberExpression = (node, parent, src, options, shadowed, extras, useExportsBag = true, rewriteExports = true) => {
3
3
  if (options.target === 'module') {
4
- if (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name)) {
5
- return;
4
+ if (rewriteExports && !useExportsBag) {
5
+ if (parent?.type === 'MemberExpression' && parent.object === node && parent.property.type === 'Identifier') {
6
+ const baseIsExportsIdent = node.object.type === 'Identifier' && node.object.name === 'exports';
7
+ const baseIsModuleExports = node.object.type === 'Identifier' && node.object.name === 'module' && node.property.type === 'Identifier' && node.property.name === 'exports';
8
+ if (baseIsExportsIdent || baseIsModuleExports) {
9
+ src.update(parent.start, parent.end, parent.property.name);
10
+ return;
11
+ }
12
+ }
13
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
14
+ src.update(node.start, node.end, 'undefined');
15
+ return;
16
+ }
6
17
  }
7
- if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
8
- src.update(node.start, node.end, exportsRename);
18
+ if (rewriteExports && (node.object.type === 'Identifier' && shadowed?.has(node.object.name) || node.property.type === 'Identifier' && shadowed?.has(node.property.name))) {
9
19
  return;
10
20
  }
21
+ if (rewriteExports) {
22
+ if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'module' && node.property.name === 'exports') {
23
+ if (useExportsBag) {
24
+ src.update(node.start, node.end, exportsRename);
25
+ }
26
+ return;
27
+ }
28
+ }
11
29
  if (node.object.type === 'Identifier' && node.property.type === 'Identifier' && node.object.name === 'require') {
12
30
  const {
13
31
  start,
@@ -26,6 +44,10 @@ export const memberExpression = (node, parent, src, options, shadowed, extras) =
26
44
  src.update(start, end, 'import.meta.main');
27
45
  break;
28
46
  case 'resolve':
47
+ if (options.transformSyntax !== true) {
48
+ src.update(start, end, 'import.meta.resolve');
49
+ return;
50
+ }
29
51
  extras?.onRequireResolve?.();
30
52
  src.update(start, end, extras?.requireResolveName ?? 'import.meta.resolve');
31
53
  break;
@@ -9,5 +9,5 @@ type MemberExpressionExtras = {
9
9
  end: number;
10
10
  }) => void;
11
11
  };
12
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
12
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras, useExportsBag?: boolean, rewriteExports?: boolean) => void;
13
13
  export {};
@@ -9,5 +9,5 @@ type MemberExpressionExtras = {
9
9
  end: number;
10
10
  }) => void;
11
11
  };
12
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
12
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras, useExportsBag?: boolean, rewriteExports?: boolean) => void;
13
13
  export {};
package/dist/module.js CHANGED
@@ -157,6 +157,7 @@ const defaultOptions = {
157
157
  requireSource: 'builtin',
158
158
  nestedRequireStrategy: 'create-require',
159
159
  cjsDefault: 'auto',
160
+ idiomaticExports: 'safe',
160
161
  topLevelAwait: 'error',
161
162
  out: undefined,
162
163
  inPlace: false
@@ -8,6 +8,7 @@ type IdentifierArg = {
8
8
  opts: FormatterOptions;
9
9
  meta: ExportsMeta;
10
10
  shadowed?: Set<string>;
11
+ useExportsBag?: boolean;
11
12
  };
12
- export declare const identifier: ({ node, ancestors, code, opts, meta, shadowed, }: IdentifierArg) => void;
13
+ export declare const identifier: ({ node, ancestors, code, opts, meta, shadowed, useExportsBag, }: IdentifierArg) => void;
13
14
  export {};
@@ -9,5 +9,5 @@ type MemberExpressionExtras = {
9
9
  end: number;
10
10
  }) => void;
11
11
  };
12
- export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras) => void;
12
+ export declare const memberExpression: (node: MemberExpression, parent: Node | null, src: MagicString, options: FormatterOptions, shadowed?: Set<string>, extras?: MemberExpressionExtras, useExportsBag?: boolean, rewriteExports?: boolean) => void;
13
13
  export {};
@@ -6,8 +6,13 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
- /** Enable syntax transforms beyond parsing. */
10
- transformSyntax?: boolean;
9
+ /**
10
+ * Enable syntax transforms beyond parsing.
11
+ * - true: full CJS↔ESM lowering/raising
12
+ * - 'globals-only': rewrite module-global differences (import.meta, __dirname/filename, require.main shims) while leaving import/export shapes untouched
13
+ * - false/undefined: no syntax transforms
14
+ */
15
+ transformSyntax?: boolean | 'globals-only';
11
16
  /** How to emit live bindings for ESM exports. */
12
17
  liveBindings?: 'strict' | 'loose' | 'off';
13
18
  /** Rewrite import specifiers (e.g. add extensions). */
@@ -32,6 +37,8 @@ export type ModuleOptions = {
32
37
  nestedRequireStrategy?: 'create-require' | 'dynamic-import';
33
38
  /** Default interop style for CommonJS default imports. */
34
39
  cjsDefault?: 'module-exports' | 'auto' | 'none';
40
+ /** Emit idiomatic exports when raising CJS to ESM. */
41
+ idiomaticExports?: 'off' | 'safe' | 'aggressive';
35
42
  /** Handling for top-level await constructs. */
36
43
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
37
44
  /** Optional diagnostics sink for warnings/errors emitted during transform. */
@@ -67,6 +74,7 @@ export type CjsExport = {
67
74
  via: Set<'exports' | 'module.exports'>;
68
75
  reassignments: SpannedNode[];
69
76
  hasGetter?: boolean;
77
+ hasNonTopLevelWrite?: boolean;
70
78
  };
71
79
  export type IdentMeta = {
72
80
  declare: SpannedNode[];
package/dist/types.d.cts CHANGED
@@ -6,8 +6,13 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
- /** Enable syntax transforms beyond parsing. */
10
- transformSyntax?: boolean;
9
+ /**
10
+ * Enable syntax transforms beyond parsing.
11
+ * - true: full CJS↔ESM lowering/raising
12
+ * - 'globals-only': rewrite module-global differences (import.meta, __dirname/filename, require.main shims) while leaving import/export shapes untouched
13
+ * - false/undefined: no syntax transforms
14
+ */
15
+ transformSyntax?: boolean | 'globals-only';
11
16
  /** How to emit live bindings for ESM exports. */
12
17
  liveBindings?: 'strict' | 'loose' | 'off';
13
18
  /** Rewrite import specifiers (e.g. add extensions). */
@@ -32,6 +37,8 @@ export type ModuleOptions = {
32
37
  nestedRequireStrategy?: 'create-require' | 'dynamic-import';
33
38
  /** Default interop style for CommonJS default imports. */
34
39
  cjsDefault?: 'module-exports' | 'auto' | 'none';
40
+ /** Emit idiomatic exports when raising CJS to ESM. */
41
+ idiomaticExports?: 'off' | 'safe' | 'aggressive';
35
42
  /** Handling for top-level await constructs. */
36
43
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
37
44
  /** Optional diagnostics sink for warnings/errors emitted during transform. */
@@ -67,6 +74,7 @@ export type CjsExport = {
67
74
  via: Set<'exports' | 'module.exports'>;
68
75
  reassignments: SpannedNode[];
69
76
  hasGetter?: boolean;
77
+ hasNonTopLevelWrite?: boolean;
70
78
  };
71
79
  export type IdentMeta = {
72
80
  declare: SpannedNode[];
package/dist/types.d.ts CHANGED
@@ -6,8 +6,13 @@ export type ModuleOptions = {
6
6
  target: 'module' | 'commonjs';
7
7
  /** Explicit source type; auto infers from file extension. */
8
8
  sourceType?: 'auto' | 'module' | 'commonjs';
9
- /** Enable syntax transforms beyond parsing. */
10
- transformSyntax?: boolean;
9
+ /**
10
+ * Enable syntax transforms beyond parsing.
11
+ * - true: full CJS↔ESM lowering/raising
12
+ * - 'globals-only': rewrite module-global differences (import.meta, __dirname/filename, require.main shims) while leaving import/export shapes untouched
13
+ * - false/undefined: no syntax transforms
14
+ */
15
+ transformSyntax?: boolean | 'globals-only';
11
16
  /** How to emit live bindings for ESM exports. */
12
17
  liveBindings?: 'strict' | 'loose' | 'off';
13
18
  /** Rewrite import specifiers (e.g. add extensions). */
@@ -32,6 +37,8 @@ export type ModuleOptions = {
32
37
  nestedRequireStrategy?: 'create-require' | 'dynamic-import';
33
38
  /** Default interop style for CommonJS default imports. */
34
39
  cjsDefault?: 'module-exports' | 'auto' | 'none';
40
+ /** Emit idiomatic exports when raising CJS to ESM. */
41
+ idiomaticExports?: 'off' | 'safe' | 'aggressive';
35
42
  /** Handling for top-level await constructs. */
36
43
  topLevelAwait?: 'error' | 'wrap' | 'preserve';
37
44
  /** Optional diagnostics sink for warnings/errors emitted during transform. */
@@ -67,6 +74,7 @@ export type CjsExport = {
67
74
  via: Set<'exports' | 'module.exports'>;
68
75
  reassignments: SpannedNode[];
69
76
  hasGetter?: boolean;
77
+ hasNonTopLevelWrite?: boolean;
70
78
  };
71
79
  export type IdentMeta = {
72
80
  declare: SpannedNode[];
@@ -63,6 +63,12 @@ const collectCjsExports = async ast => {
63
63
  const localToExport = new Map();
64
64
  const aliases = new Map();
65
65
  const literals = new Map();
66
+ let hasUnsupportedExportWrite = false;
67
+ const isTopLevelWrite = ancestors => {
68
+ const parent = ancestors[ancestors.length - 2];
69
+ const grandparent = ancestors[ancestors.length - 3];
70
+ return grandparent?.type === 'Program' && (parent?.type === 'ExpressionStatement' || parent?.type === 'VariableDeclaration');
71
+ };
66
72
  const addExport = (ref, node, rhs, options) => {
67
73
  const entry = exportsMap.get(ref.key) ?? {
68
74
  key: ref.key,
@@ -75,6 +81,9 @@ const collectCjsExports = async ast => {
75
81
  if (options?.hasGetter) {
76
82
  entry.hasGetter = true;
77
83
  }
84
+ if (options?.topLevel === false) {
85
+ entry.hasNonTopLevelWrite = true;
86
+ }
78
87
  if (rhs) {
79
88
  entry.fromIdentifier ??= rhs.name;
80
89
  const set = localToExport.get(rhs.name) ?? new Set();
@@ -104,9 +113,15 @@ const collectCjsExports = async ast => {
104
113
  const target = resolveExportTarget(node.left, aliases, literals, ancestors);
105
114
  if (target) {
106
115
  const rhsIdent = node.right.type === 'Identifier' ? node.right : undefined;
107
- addExport(target, node, rhsIdent);
116
+ const topLevel = isTopLevelWrite(ancestors);
117
+ addExport(target, node, rhsIdent, {
118
+ topLevel
119
+ });
108
120
  return;
109
121
  }
122
+ if (node.left.type === 'MemberExpression' && resolveBase(node.left.object, aliases, ancestors)) {
123
+ hasUnsupportedExportWrite = true;
124
+ }
110
125
  if (node.left.type === 'Identifier') {
111
126
  const keys = localToExport.get(node.left.name);
112
127
  if (keys) {
@@ -123,7 +138,12 @@ const collectCjsExports = async ast => {
123
138
  const findExportRefs = pattern => {
124
139
  if (pattern.type === 'MemberExpression') {
125
140
  const ref = resolveExportTarget(pattern, aliases, literals, ancestors);
126
- if (ref) addExport(ref, node);
141
+ if (ref) {
142
+ const topLevel = isTopLevelWrite(ancestors);
143
+ addExport(ref, node, undefined, {
144
+ topLevel
145
+ });
146
+ }
127
147
  return;
128
148
  }
129
149
  if (pattern.type === 'ObjectPattern') {
@@ -165,10 +185,13 @@ const collectCjsExports = async ast => {
165
185
  if (prop.value.type === 'Identifier') {
166
186
  rhsIdent = prop.value;
167
187
  }
188
+ const topLevel = isTopLevelWrite(ancestors);
168
189
  addExport({
169
190
  key: keyName,
170
191
  via: ref
171
- }, node, rhsIdent);
192
+ }, node, rhsIdent, {
193
+ topLevel
194
+ });
172
195
  }
173
196
  }
174
197
  }
@@ -197,11 +220,13 @@ const collectCjsExports = async ast => {
197
220
  // Setter-only doesn’t create a readable export; ignore beyond marking write
198
221
  }
199
222
  }
223
+ const topLevel = isTopLevelWrite(ancestors);
200
224
  addExport({
201
225
  key: keyName,
202
226
  via: target
203
227
  }, node, rhsIdent, {
204
- hasGetter
228
+ hasGetter,
229
+ topLevel
205
230
  });
206
231
  }
207
232
 
@@ -228,17 +253,20 @@ const collectCjsExports = async ast => {
228
253
  hasGetter = true;
229
254
  }
230
255
  }
256
+ const topLevel = isTopLevelWrite(ancestors);
231
257
  addExport({
232
258
  key: keyName,
233
259
  via: target
234
260
  }, node, rhsIdent, {
235
- hasGetter
261
+ hasGetter,
262
+ topLevel
236
263
  });
237
264
  }
238
265
  }
239
266
  }
240
267
  }
241
268
  });
269
+ exportsMap.hasUnsupportedExportWrite = hasUnsupportedExportWrite;
242
270
  return exportsMap;
243
271
  };
244
272
  export { exportsRename, requireMainRgx, collectCjsExports };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knighted/module",
3
- "version": "1.0.0-rc.5",
3
+ "version": "1.0.0-rc.6",
4
4
  "description": "Bidirectional transform for ES modules and CommonJS.",
5
5
  "type": "module",
6
6
  "main": "dist/module.js",