@scriptc/compiler 0.0.7 → 0.0.9

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.
Files changed (94) hide show
  1. package/ambient/scriptc-node-fallback.d.ts +13 -0
  2. package/dist/backend/cc.js +14 -0
  3. package/dist/backend/cc.js.map +1 -1
  4. package/dist/backend/emission/emit-exprs.js +155 -6
  5. package/dist/backend/emission/emit-exprs.js.map +1 -1
  6. package/dist/backend/emission/emit-island.js +9 -1
  7. package/dist/backend/emission/emit-island.js.map +1 -1
  8. package/dist/backend/emission/emit-stmts.js +3 -1
  9. package/dist/backend/emission/emit-stmts.js.map +1 -1
  10. package/dist/backend/emission/emit-types.d.ts +3 -0
  11. package/dist/backend/emission/emit-types.js +12 -0
  12. package/dist/backend/emission/emit-types.js.map +1 -1
  13. package/dist/backend/emission/emit-walkers.d.ts +7 -5
  14. package/dist/backend/emission/emit-walkers.js +45 -18
  15. package/dist/backend/emission/emit-walkers.js.map +1 -1
  16. package/dist/backend/emission/emitter.d.ts +1 -1
  17. package/dist/backend/emission/emitter.js +35 -10
  18. package/dist/backend/emission/emitter.js.map +1 -1
  19. package/dist/backend/emission/may-throw.js +4 -3
  20. package/dist/backend/emission/may-throw.js.map +1 -1
  21. package/dist/backend/llvm/emitter.js +300 -18
  22. package/dist/backend/llvm/emitter.js.map +1 -1
  23. package/dist/diagnostics/diagnostic.d.ts +98 -11
  24. package/dist/diagnostics/diagnostic.js +185 -25
  25. package/dist/diagnostics/diagnostic.js.map +1 -1
  26. package/dist/frontend/lib-contract.d.ts +24 -6
  27. package/dist/frontend/lib-contract.js +125 -44
  28. package/dist/frontend/lib-contract.js.map +1 -1
  29. package/dist/frontend/lowering/lower-assert.js +16 -0
  30. package/dist/frontend/lowering/lower-assert.js.map +1 -1
  31. package/dist/frontend/lowering/lower-builtins.d.ts +11 -0
  32. package/dist/frontend/lowering/lower-builtins.js +127 -4
  33. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  34. package/dist/frontend/lowering/lower-calls.d.ts +77 -18
  35. package/dist/frontend/lowering/lower-calls.js +978 -44
  36. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  37. package/dist/frontend/lowering/lower-classes.d.ts +14 -1
  38. package/dist/frontend/lowering/lower-classes.js +112 -16
  39. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  40. package/dist/frontend/lowering/lower-containers.d.ts +4 -2
  41. package/dist/frontend/lowering/lower-containers.js +236 -44
  42. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  43. package/dist/frontend/lowering/lower-emitter.js +55 -1
  44. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  45. package/dist/frontend/lowering/lower-exprs.d.ts +26 -3
  46. package/dist/frontend/lowering/lower-exprs.js +507 -47
  47. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  48. package/dist/frontend/lowering/lower-island.d.ts +7 -0
  49. package/dist/frontend/lowering/lower-island.js +71 -10
  50. package/dist/frontend/lowering/lower-island.js.map +1 -1
  51. package/dist/frontend/lowering/lower-mixins.d.ts +1 -1
  52. package/dist/frontend/lowering/lower-mixins.js +1 -1
  53. package/dist/frontend/lowering/lower-modules.js +95 -4
  54. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  55. package/dist/frontend/lowering/lower-stmts.js +866 -56
  56. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  57. package/dist/frontend/lowering/lower-stream.js +28 -2
  58. package/dist/frontend/lowering/lower-stream.js.map +1 -1
  59. package/dist/frontend/lowering/lowerer.d.ts +85 -17
  60. package/dist/frontend/lowering/lowerer.js +459 -37
  61. package/dist/frontend/lowering/lowerer.js.map +1 -1
  62. package/dist/frontend/lowering/surfaces.js +20 -3
  63. package/dist/frontend/lowering/surfaces.js.map +1 -1
  64. package/dist/frontend/npm-static-rewrite.d.ts +7 -2
  65. package/dist/frontend/npm-static-rewrite.js +566 -138
  66. package/dist/frontend/npm-static-rewrite.js.map +1 -1
  67. package/dist/frontend/npm-static.js +11 -2
  68. package/dist/frontend/npm-static.js.map +1 -1
  69. package/dist/frontend/program.js +5 -5
  70. package/dist/frontend/program.js.map +1 -1
  71. package/dist/frontend/shared.d.ts +6 -1
  72. package/dist/frontend/shared.js +46 -1
  73. package/dist/frontend/shared.js.map +1 -1
  74. package/dist/frontend/types.d.ts +28 -1
  75. package/dist/frontend/types.js +304 -10
  76. package/dist/frontend/types.js.map +1 -1
  77. package/dist/index.d.ts +2 -1
  78. package/dist/index.js +181 -26
  79. package/dist/index.js.map +1 -1
  80. package/dist/ir/nodes.d.ts +159 -9
  81. package/dist/ir/nodes.js +76 -6
  82. package/dist/ir/nodes.js.map +1 -1
  83. package/dist/ir/validate.js +93 -14
  84. package/dist/ir/validate.js.map +1 -1
  85. package/dist/library/profile.d.ts +17 -3
  86. package/dist/library/profile.js +38 -9
  87. package/dist/library/profile.js.map +1 -1
  88. package/dist/library/sidecar.js +162 -43
  89. package/dist/library/sidecar.js.map +1 -1
  90. package/dist/library/trap-teaching.d.ts +11 -0
  91. package/dist/library/trap-teaching.js +49 -0
  92. package/dist/library/trap-teaching.js.map +1 -0
  93. package/package.json +2 -2
  94. package/surface-manifest.json +46 -4
@@ -55,6 +55,17 @@
55
55
  * statement positions (visible only to load-order-sensitive side
56
56
  * effects). Node's __esModule marker survives on the rewritten object.
57
57
  *
58
+ * THE IMPORT-SIDE TWIN: the same pass erases esbuild's __toESM interop
59
+ * wrapper around EXTERNAL dependencies (`var import_x =
60
+ * __toESM(require("x"))`, the `, 1` node-mode variant, inline
61
+ * member-accessed forms) — see the section header at planToEsmInterop.
62
+ * The wrapper pads down to the bare require it wraps and module-valued
63
+ * `.default` accesses pad down to their binding, so member accesses model
64
+ * on the required package's canonical table through the ordinary require
65
+ * machinery. Interop the recognizers cannot finish answers `{ degrade }`:
66
+ * the caller reports the package as an offender (naming the construct)
67
+ * and it serves from the island — never a failed build.
68
+ *
58
69
  * Everything here is SYNTACTIC (typescript5 world, the cjs-lexer's
59
70
  * sanctioned island): the chase decides only what TEXT to emit — the
60
71
  * checker then infers the types from the emitted spellings. A file whose
@@ -62,9 +73,10 @@
62
73
  * untouched (null): its import-site errors stay, and the frontend's
63
74
  * offender attribution degrades the PACKAGE to the island with a note. */
64
75
  import { existsSync, readFileSync, statSync } from "node:fs";
65
- import { dirname, resolve as resolvePath } from "node:path";
76
+ import { dirname, join, resolve as resolvePath } from "node:path";
66
77
  import ts from "typescript5";
67
78
  import { cjsLexedExportsOf, cjsLexerVisibleNames } from "./cjs-lexer.js";
79
+ import { resolveExports } from "./npm.js";
68
80
  /** True when `e` is exactly the `exports` identifier. */
69
81
  function isExportsIdent(e) {
70
82
  return ts.isIdentifier(e) && e.text === "exports";
@@ -211,14 +223,10 @@ function definePropertyExportOf(call) {
211
223
  const esModuleStamp = name === "__esModule" && value !== null && value.kind === ts.SyntaxKind.TrueKeyword;
212
224
  return { name, value, esModuleStamp };
213
225
  }
214
- /** Resolves a RELATIVE CJS require target with Node's file-first probes
215
- * (the dist-tree subset: exact, .js/.cjs, /index.js, package.json "main"
216
- * for directories). Bare specifiers answer null — their names ride the
217
- * spread entry and the program resolver. */
218
- function resolveRelativeCjs(fromFile, spec) {
219
- if (!spec.startsWith("./") && !spec.startsWith("../"))
220
- return null;
221
- const base = resolvePath(dirname(fromFile), spec);
226
+ /** Node's file-first CJS probes over an ABSOLUTE base (the dist-tree
227
+ * subset: exact, .js/.cjs, /index.js, package.json "main" for
228
+ * directories). */
229
+ function resolveCjsBase(base) {
222
230
  const candidates = [base, `${base}.js`, `${base}.cjs`];
223
231
  for (const c of candidates) {
224
232
  try {
@@ -252,6 +260,118 @@ function resolveRelativeCjs(fromFile, spec) {
252
260
  }
253
261
  return null;
254
262
  }
263
+ /** Resolves a RELATIVE CJS require target (resolveCjsBase over the
264
+ * importer-anchored path). Bare specifiers answer null — their names ride
265
+ * the spread entry and the program resolver. */
266
+ function resolveRelativeCjs(fromFile, spec) {
267
+ if (!spec.startsWith("./") && !spec.startsWith("../"))
268
+ return null;
269
+ return resolveCjsBase(resolvePath(dirname(fromFile), spec));
270
+ }
271
+ /** Resolves a BARE require target the way Node's CJS resolution would (the
272
+ * __toESM default-shape probe's subset): node_modules/<name> walked up from
273
+ * the requiring file, "exports" honored with the require condition, then
274
+ * main/index. Best-effort — null keeps the caller conservative. */
275
+ function resolveBareRequireCjs(fromFile, spec) {
276
+ if (spec.startsWith("#") || spec.startsWith("node:"))
277
+ return null;
278
+ const parts = spec.split("/");
279
+ const name = spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
280
+ const subparts = spec.startsWith("@") ? parts.slice(2) : parts.slice(1);
281
+ const subpath = subparts.length > 0 ? `./${subparts.join("/")}` : ".";
282
+ for (let dir = dirname(fromFile);;) {
283
+ const pkgDir = join(dir, "node_modules", name);
284
+ try {
285
+ if (existsSync(pkgDir) && statSync(pkgDir).isDirectory()) {
286
+ const pkgPath = join(pkgDir, "package.json");
287
+ let exports;
288
+ try {
289
+ exports = existsSync(pkgPath)
290
+ ? JSON.parse(readFileSync(pkgPath, "utf8")).exports
291
+ : undefined;
292
+ }
293
+ catch {
294
+ return null;
295
+ }
296
+ if (exports !== undefined) {
297
+ const target = resolveExports(exports, subpath, "require");
298
+ return target === null ? null : resolveCjsBase(join(pkgDir, target));
299
+ }
300
+ return resolveCjsBase(subpath === "." ? pkgDir : join(pkgDir, subpath));
301
+ }
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ const parent = dirname(dir);
307
+ if (parent === dir)
308
+ return null;
309
+ dir = parent;
310
+ }
311
+ }
312
+ /** Whether `require(spec)` from `fromFile` answers an object whose
313
+ * `__esModule` is TRUTHY at runtime under Node — the branch esbuild's
314
+ * __toESM takes to decide what `default` binds. True for the transpiled-ESM
315
+ * stamps (the lexer-visible `exports.__esModule =` / defineProperty forms,
316
+ * esbuild's own `module.exports = __toCommonJS(…)` where the stamp hides
317
+ * from the lexer inside the helper) and for real ES-module targets
318
+ * (require(esm) marks the namespace for interop); `module.exports =
319
+ * require(…)` forwarding chains follow. False (the CJS answer: `default`
320
+ * IS module.exports) everywhere else, unresolvable targets included —
321
+ * their require throws before `default` matters. */
322
+ function requireTargetEsModuleStamped(fromFile, spec, depth = 0) {
323
+ if (depth > 8)
324
+ return false;
325
+ const file = spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/")
326
+ ? resolveCjsBase(spec.startsWith("/") ? spec : resolvePath(dirname(fromFile), spec))
327
+ : resolveBareRequireCjs(fromFile, spec);
328
+ if (file === null)
329
+ return false;
330
+ if (file.endsWith(".mjs"))
331
+ return true;
332
+ if (file.endsWith(".json"))
333
+ return false;
334
+ if (file.endsWith(".js")) {
335
+ // the nearest package.json "type" decides the .js format
336
+ for (let dir = dirname(file);;) {
337
+ const pkgPath = join(dir, "package.json");
338
+ if (existsSync(pkgPath)) {
339
+ try {
340
+ if (JSON.parse(readFileSync(pkgPath, "utf8")).type === "module")
341
+ return true;
342
+ }
343
+ catch {
344
+ /* unreadable — treat as CJS */
345
+ }
346
+ break;
347
+ }
348
+ const parent = dirname(dir);
349
+ if (parent === dir)
350
+ break;
351
+ dir = parent;
352
+ }
353
+ }
354
+ let src;
355
+ try {
356
+ src = readFileSync(file, "utf8");
357
+ }
358
+ catch {
359
+ return false;
360
+ }
361
+ try {
362
+ if (cjsLexedExportsOf(src, file).exports.has("__esModule"))
363
+ return true;
364
+ }
365
+ catch {
366
+ /* unlexable — keep probing */
367
+ }
368
+ if (src.includes("__toCommonJS("))
369
+ return true;
370
+ const fwd = /module\.exports\s*=\s*require\(\s*["']([^"']+)["']\s*\)/.exec(src);
371
+ if (fwd !== null)
372
+ return requireTargetEsModuleStamped(file, fwd[1], depth + 1);
373
+ return false;
374
+ }
255
375
  /** The Node-visible named-export set of a star-re-export TARGET file:
256
376
  * its recursive lexer-visible names (relative edges only — exactly what
257
377
  * the emitted member entries must enumerate). */
@@ -275,13 +395,272 @@ function padSpan(text, start, end) {
275
395
  function isPlainName(name) {
276
396
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
277
397
  }
398
+ /** Structural recognition of esbuild's vendored __toESM helper:
399
+ * `var __toESM = (mod, isNodeMode, target) => (target = …, __copyProps(
400
+ * isNodeMode || !mod || !mod.__esModule
401
+ * ? __defProp(target, "default", { value: mod, enumerable: true })
402
+ * : target,
403
+ * mod))`
404
+ * — an arrow over (mod, isNodeMode[, target]) whose result is a
405
+ * __copyProps(…, mod) call copying onto a conditional that tests
406
+ * isNodeMode/__esModule and defines "default" with value mod. Comments and
407
+ * formatting are free; the SHAPE is the contract. */
408
+ function recognizedToEsmDecl(stmt) {
409
+ if (!ts.isVariableStatement(stmt))
410
+ return false;
411
+ const decls = stmt.declarationList.declarations;
412
+ if (decls.length !== 1)
413
+ return false;
414
+ const d = decls[0];
415
+ if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined)
416
+ return false;
417
+ let init = d.initializer;
418
+ while (ts.isParenthesizedExpression(init))
419
+ init = init.expression;
420
+ if (!ts.isArrowFunction(init))
421
+ return false;
422
+ if (init.parameters.length < 2 || init.parameters.length > 3)
423
+ return false;
424
+ const p0 = init.parameters[0];
425
+ const p1 = init.parameters[1];
426
+ if (!ts.isIdentifier(p0.name) || !ts.isIdentifier(p1.name))
427
+ return false;
428
+ const mod = p0.name.text;
429
+ const isNodeMode = p1.name.text;
430
+ if (ts.isBlock(init.body))
431
+ return false;
432
+ let expr = init.body;
433
+ while (ts.isParenthesizedExpression(expr))
434
+ expr = expr.expression;
435
+ // the rightmost comma operand carries the result
436
+ while (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.CommaToken) {
437
+ expr = expr.right;
438
+ }
439
+ while (ts.isParenthesizedExpression(expr))
440
+ expr = expr.expression;
441
+ if (!ts.isCallExpression(expr) || !ts.isIdentifier(expr.expression) || expr.expression.text !== "__copyProps")
442
+ return false;
443
+ if (expr.arguments.length < 2)
444
+ return false;
445
+ const from = expr.arguments[1];
446
+ if (!ts.isIdentifier(from) || from.text !== mod)
447
+ return false;
448
+ let to = expr.arguments[0];
449
+ while (ts.isParenthesizedExpression(to))
450
+ to = to.expression;
451
+ if (!ts.isConditionalExpression(to))
452
+ return false;
453
+ const condText = to.condition.getText();
454
+ if (!condText.includes(isNodeMode) || !condText.includes("__esModule"))
455
+ return false;
456
+ let stamp = to.whenTrue;
457
+ while (ts.isParenthesizedExpression(stamp))
458
+ stamp = stamp.expression;
459
+ if (!ts.isCallExpression(stamp) || !ts.isIdentifier(stamp.expression) || stamp.expression.text !== "__defProp")
460
+ return false;
461
+ if (stamp.arguments.length !== 3)
462
+ return false;
463
+ const nameArg = stamp.arguments[1];
464
+ if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default")
465
+ return false;
466
+ const desc = stamp.arguments[2];
467
+ if (!ts.isObjectLiteralExpression(desc))
468
+ return false;
469
+ return desc.properties.some((p) => ts.isPropertyAssignment(p) &&
470
+ ts.isIdentifier(p.name) &&
471
+ p.name.text === "value" &&
472
+ ts.isIdentifier(p.initializer) &&
473
+ p.initializer.text === mod);
474
+ }
475
+ const TO_ESM_SHAPE_DEGRADE = "its shipped JS calls the __toESM bundler-interop helper but spells it in a shape the " +
476
+ "recognizer cannot verify — the package serves from the island instead";
477
+ const TO_ESM_ESCAPE_DEGRADE = "a __toESM(require(…)) bundler-interop object escapes into a use the static rewrite cannot " +
478
+ "follow (only module-scope bindings and direct member reads compile) — the package serves " +
479
+ "from the island instead";
480
+ const TO_ESM_ARG_DEGRADE = "it calls the __toESM bundler-interop helper on something other than require(…) of a string " +
481
+ "literal — the package serves from the island instead";
482
+ const TO_ESM_MIXED_DEGRADE = "its bundler-emitted export surface cannot be respelled around its __toESM interop imports " +
483
+ "— the package serves from the island instead";
484
+ /** The interop-erasure plan for one CJS file (see the section header). */
485
+ function planToEsmInterop(sf) {
486
+ const pads = [];
487
+ const moduleBindings = new Set();
488
+ const plan = { pads, moduleBindings, degrade: null };
489
+ const fail = (reason) => ({ pads: [], moduleBindings: new Set(), degrade: reason });
490
+ const declStmts = sf.statements.filter((s) => ts.isVariableStatement(s) &&
491
+ s.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "__toESM"));
492
+ const refs = [];
493
+ const calls = [];
494
+ const collect = (n) => {
495
+ if (ts.isIdentifier(n) && n.text === "__toESM")
496
+ refs.push(n);
497
+ if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM")
498
+ calls.push(n);
499
+ ts.forEachChild(n, collect);
500
+ };
501
+ collect(sf);
502
+ if (calls.length === 0 && refs.length === 0)
503
+ return plan;
504
+ // Every reference must be the declarator's own name or a call's callee —
505
+ // a helper that escapes as a VALUE is outside the recognized shape.
506
+ const callees = new Set(calls.map((c) => c.expression));
507
+ const declNames = new Set();
508
+ for (const s of declStmts) {
509
+ for (const d of s.declarationList.declarations) {
510
+ if (ts.isIdentifier(d.name) && d.name.text === "__toESM")
511
+ declNames.add(d.name);
512
+ }
513
+ }
514
+ if (refs.some((r) => !callees.has(r) && !declNames.has(r)))
515
+ return fail(TO_ESM_ESCAPE_DEGRADE);
516
+ if (calls.length === 0)
517
+ return plan; // declared but never called — dead helper
518
+ if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]))
519
+ return fail(TO_ESM_SHAPE_DEGRADE);
520
+ /** Module-scope interop bindings: name → whether `.default` IS the
521
+ * module (pad the access) rather than a member read of its `default`. */
522
+ const bindings = new Map();
523
+ for (const call of calls) {
524
+ const arg0 = call.arguments[0];
525
+ const spec = arg0 !== undefined ? bareRequireSpecOf(arg0) : null;
526
+ if (spec === null || call.arguments.length > 2)
527
+ return fail(TO_ESM_ARG_DEGRADE);
528
+ let isNodeMode = false;
529
+ if (call.arguments.length === 2) {
530
+ const modeArg = call.arguments[1];
531
+ if (ts.isNumericLiteral(modeArg))
532
+ isNodeMode = Number(modeArg.text) !== 0;
533
+ else if (modeArg.kind === ts.SyntaxKind.TrueKeyword)
534
+ isNodeMode = true;
535
+ else if (modeArg.kind === ts.SyntaxKind.FalseKeyword)
536
+ isNodeMode = false;
537
+ else
538
+ return fail(TO_ESM_ARG_DEGRADE);
539
+ }
540
+ const defaultIsModule = isNodeMode || !requireTargetEsModuleStamped(sf.fileName, spec);
541
+ // classify the call's use: a variable binding, or an immediate member
542
+ // read; anything else escapes.
543
+ let child = call;
544
+ let parent = call.parent;
545
+ while (ts.isParenthesizedExpression(parent)) {
546
+ child = parent;
547
+ parent = parent.parent;
548
+ }
549
+ if (ts.isVariableDeclaration(parent) && parent.initializer === child && ts.isIdentifier(parent.name)) {
550
+ if (bindings.has(parent.name.text))
551
+ return fail(TO_ESM_ESCAPE_DEGRADE);
552
+ bindings.set(parent.name.text, defaultIsModule);
553
+ if (defaultIsModule)
554
+ moduleBindings.add(parent.name.text);
555
+ }
556
+ else if (ts.isPropertyAccessExpression(parent) && parent.expression === child && ts.isIdentifier(parent.name)) {
557
+ if (parent.name.text === "default" && defaultIsModule) {
558
+ if (!readOnlyAccess(parent))
559
+ return fail(TO_ESM_ESCAPE_DEGRADE);
560
+ pads.push({ start: child.getEnd(), end: parent.getEnd() });
561
+ }
562
+ }
563
+ else {
564
+ return fail(TO_ESM_ESCAPE_DEGRADE);
565
+ }
566
+ // the wrapper itself: callee + '(' down, and everything after the
567
+ // require argument (`, 1` included) — the bare require survives at its
568
+ // original offsets.
569
+ pads.push({ start: call.getStart(sf), end: arg0.getStart(sf) });
570
+ pads.push({ start: arg0.getEnd(), end: call.getEnd() });
571
+ }
572
+ if (bindings.size > 0) {
573
+ // Binding safety: the erased binding must be declared exactly once and
574
+ // never written — the `.default` mapping below matches receivers by
575
+ // NAME (syntactic, like the rest of this file), so a shadow or a
576
+ // reassignment would silently change what a padded access reads.
577
+ const declCount = new Map();
578
+ const bump = (name) => {
579
+ if (bindings.has(name))
580
+ declCount.set(name, (declCount.get(name) ?? 0) + 1);
581
+ };
582
+ let violated = false;
583
+ const scan = (n) => {
584
+ if (violated)
585
+ return;
586
+ if ((ts.isVariableDeclaration(n) || ts.isBindingElement(n) || ts.isParameter(n)) &&
587
+ ts.isIdentifier(n.name)) {
588
+ bump(n.name.text);
589
+ }
590
+ else if ((ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) && n.name !== undefined) {
591
+ bump(n.name.text);
592
+ }
593
+ else if (ts.isBinaryExpression(n) &&
594
+ n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
595
+ n.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
596
+ ts.isIdentifier(n.left) &&
597
+ bindings.has(n.left.text)) {
598
+ violated = true;
599
+ }
600
+ else if ((ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
601
+ (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken) &&
602
+ ts.isIdentifier(n.operand) &&
603
+ bindings.has(n.operand.text)) {
604
+ violated = true;
605
+ }
606
+ ts.forEachChild(n, scan);
607
+ };
608
+ scan(sf);
609
+ if (violated || [...bindings.keys()].some((name) => (declCount.get(name) ?? 0) !== 1)) {
610
+ return fail(TO_ESM_ESCAPE_DEGRADE);
611
+ }
612
+ // `.default` reads on a module-valued binding pad down to the binding.
613
+ const padDefaults = (n) => {
614
+ if (ts.isPropertyAccessExpression(n) &&
615
+ ts.isIdentifier(n.expression) &&
616
+ bindings.get(n.expression.text) === true &&
617
+ ts.isIdentifier(n.name) &&
618
+ n.name.text === "default") {
619
+ if (!readOnlyAccess(n)) {
620
+ violated = true;
621
+ return;
622
+ }
623
+ pads.push({ start: n.expression.getEnd(), end: n.getEnd() });
624
+ }
625
+ ts.forEachChild(n, padDefaults);
626
+ };
627
+ padDefaults(sf);
628
+ if (violated)
629
+ return fail(TO_ESM_ESCAPE_DEGRADE);
630
+ }
631
+ return plan;
632
+ }
633
+ /** True when a padded-away `.default` access is a plain READ — an
634
+ * assignment target or delete operand cannot lose its member spelling. */
635
+ function readOnlyAccess(access) {
636
+ const p = access.parent;
637
+ if (p !== undefined && ts.isDeleteExpression(p))
638
+ return false;
639
+ if (p !== undefined &&
640
+ ts.isBinaryExpression(p) &&
641
+ p.left === access &&
642
+ p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
643
+ p.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
644
+ return false;
645
+ }
646
+ if (p !== undefined &&
647
+ (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
648
+ (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)) {
649
+ return false;
650
+ }
651
+ return true;
652
+ }
278
653
  const RESERVED_KEYS = new Set(["__proto__"]);
279
654
  /** The rewrite (see the header). Null = not a recognized bundle shape (or
280
- * nothing to fix) — serve the file untouched. */
655
+ * nothing to fix) — serve the file untouched. A `{ degrade }` answer means
656
+ * the file carries a bundler-interop construct the recognizers cannot
657
+ * finish: the caller reports the PACKAGE as an offender with the reason
658
+ * and the fallback loop islands it — never a failed build. */
281
659
  export function rewriteBundlerCjsExports(source, filePath) {
282
660
  // Cheap gates before any parse: CJS bundle plumbing leaves textual
283
661
  // fingerprints; files without any are never candidates.
284
662
  if (!source.includes("__toCommonJS") &&
663
+ !source.includes("__toESM") &&
285
664
  !source.includes("__exportStar") &&
286
665
  !source.includes("__reExport") &&
287
666
  !(source.includes("Object.defineProperty(exports") && source.includes("get"))) {
@@ -290,11 +669,29 @@ export function rewriteBundlerCjsExports(source, filePath) {
290
669
  // parents ON: the helper sweep excludes declarator-name self references
291
670
  // and the chase reads expression text through the tree
292
671
  const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
293
- // ESM syntax → not CJS; leave alone.
672
+ // ESM syntax → not CJS; leave alone — except when the ES module CALLS
673
+ // the __toESM interop helper (esbuild's ESM output around __require of
674
+ // an external): no static story respells that, and served untouched the
675
+ // helper's `var __create = Object.create;` chain fences at MODULE LOAD,
676
+ // so the honest answer is the per-package degrade.
294
677
  for (const stmt of sf.statements) {
295
- if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt) || ts.isExportAssignment(stmt))
296
- return null;
678
+ if (ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt) || ts.isExportAssignment(stmt)) {
679
+ return source.includes("__toESM(")
680
+ ? {
681
+ degrade: "its ES-module dist routes an external dependency through the __toESM " +
682
+ "bundler-interop helper, which has no static story in ESM output — the " +
683
+ "package serves from the island instead",
684
+ }
685
+ : null;
686
+ }
297
687
  }
688
+ // The __toESM interop pass (see the section header): wrapper call sites
689
+ // pad down to the bare require they wrap, module-valued `.default`
690
+ // accesses pad down to their binding, and a file whose interop deviates
691
+ // beyond recognition degrades the package.
692
+ const toEsm = planToEsmInterop(sf);
693
+ if (toEsm.degrade !== null)
694
+ return { degrade: toEsm.degrade };
298
695
  const neutralize = [];
299
696
  const exportTables = [];
300
697
  const definePropEntries = [];
@@ -428,145 +825,162 @@ export function rewriteBundlerCjsExports(source, filePath) {
428
825
  continue;
429
826
  }
430
827
  }
431
- if (!sawBundleShape)
432
- return null;
433
828
  const lexed = cjsLexedExportsOf(source, filePath);
434
- if (lexed.exports.size === 0 && lexed.reexports.length === 0)
435
- return null;
436
- /* Star specs Node actually honors are the lexer's SURVIVING reexports
437
- * (annotation spreads for esbuild bundles, star-pattern calls for tsc
438
- * output). The statements walked above contribute load order; specs the
439
- * lexer dropped (cleared by a later module.exports=) drop here too. */
440
- const lexerSpecs = new Set(lexed.reexports);
441
- const starSpecs = starSpecsInOrder.filter((s) => lexerSpecs.has(s));
442
- for (const s of lexed.reexports) {
443
- if (!starSpecs.includes(s))
444
- starSpecs.push(s);
445
- }
446
- /* The entry list, first-wins per name: own getter-table entries, then
447
- * defineProperty entries, then surviving top-level member exports, then
448
- * (for names still uncovered) star enumerations in spec order, then the
449
- * any fallback for every remaining lexer-visible name. */
829
+ let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0);
830
+ const starSpecs = [];
450
831
  const entries = new Map();
451
- const claim = (name, spec) => {
452
- if (name === "__esModule" || RESERVED_KEYS.has(name))
453
- return;
454
- if (!entries.has(name))
455
- entries.set(name, spec);
456
- };
457
- const classify = (name, value) => {
458
- const chased = value !== null ? chaseValue(value) : null;
459
- if (chased === null)
460
- return { name, kind: "any" };
461
- if (chased.kind === "ident")
462
- return { name, kind: "ident", value: chased.text };
463
- return { name, kind: "hoist", value: chased.text };
464
- };
465
- // Own names: the getter table(s) — the one feeding module.exports first.
466
- const tablesOrdered = [
467
- ...exportTables.filter((t) => t.target === esbuildMainTarget),
468
- ...exportTables.filter((t) => t.target !== esbuildMainTarget),
469
- ];
470
- for (const t of tablesOrdered) {
471
- for (const [name, value] of t.entries)
472
- claim(name, classify(name, value));
473
- }
474
- for (const d of definePropEntries)
475
- claim(d.name, classify(d.name, d.value));
476
- /* Top-level member exports. Node's export-object identity rules decide
477
- * each statement's fate against the ORIGINAL table (esbuildMainTarget's
478
- * assignment): a member attached BEFORE it was discarded (the table
479
- * replaced the object), a bare `exports.N =` AFTER it wrote the stale
480
- * object — both keep only their lexer-visible NAME (value undefined,
481
- * exactly Node's link answer: the any fallback below). A surviving
482
- * member (`module.exports.N =` after the table, or any spelling when no
483
- * table exists) re-emits its VALUE as a canonical entry — identifier
484
- * RHS only; any other RHS would need a second evaluation, so the file
485
- * is left alone (the offender path owns it). Every member statement is
486
- * then NEUTRALIZED: the canonical tail table replaces the object, and
487
- * a stray earlier attach would meet the lowering's discard fence. */
488
- for (const m of memberStmts) {
489
- let cur = m.finalRhs;
490
- while (ts.isParenthesizedExpression(cur))
491
- cur = cur.expression;
492
- const isVoidInit = cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
493
- const discarded = tablePos !== null && (m.pos < tablePos || m.viaBareExports);
494
- if (!isVoidInit) {
495
- // Only side-effect-free RHS shapes may be neutralized (identifiers,
496
- // scalar literals, ident-rooted member chains — chaseValue's set);
497
- // anything else keeps the file untouched (the offender path owns
498
- // it: neutralizing would drop an evaluation, keeping it meets the
499
- // lowering's discard fence at load).
500
- const chased = chaseValue(cur);
832
+ const starMembers = [];
833
+ const requireConsts = [];
834
+ if (emitTable) {
835
+ /* Star specs Node actually honors are the lexer's SURVIVING reexports
836
+ * (annotation spreads for esbuild bundles, star-pattern calls for tsc
837
+ * output). The statements walked above contribute load order; specs the
838
+ * lexer dropped (cleared by a later module.exports=) drop here too. */
839
+ const lexerSpecs = new Set(lexed.reexports);
840
+ starSpecs.push(...starSpecsInOrder.filter((s) => lexerSpecs.has(s)));
841
+ for (const s of lexed.reexports) {
842
+ if (!starSpecs.includes(s))
843
+ starSpecs.push(s);
844
+ }
845
+ /* The entry list, first-wins per name: own getter-table entries, then
846
+ * defineProperty entries, then surviving top-level member exports, then
847
+ * (for names still uncovered) star enumerations in spec order, then the
848
+ * any fallback for every remaining lexer-visible name. */
849
+ const claim = (name, spec) => {
850
+ if (name === "__esModule" || RESERVED_KEYS.has(name))
851
+ return;
852
+ if (!entries.has(name))
853
+ entries.set(name, spec);
854
+ };
855
+ const classify = (name, value) => {
856
+ const chased = value !== null ? chaseValue(value) : null;
501
857
  if (chased === null)
502
- return null;
503
- if (!discarded) {
504
- for (const name of m.names)
505
- claim(name, { name, kind: chased.kind, value: chased.text });
858
+ return { name, kind: "any" };
859
+ if (chased.kind === "ident")
860
+ return { name, kind: "ident", value: chased.text };
861
+ return { name, kind: "hoist", value: chased.text };
862
+ };
863
+ // Own names: the getter table(s) — the one feeding module.exports first.
864
+ const tablesOrdered = [
865
+ ...exportTables.filter((t) => t.target === esbuildMainTarget),
866
+ ...exportTables.filter((t) => t.target !== esbuildMainTarget),
867
+ ];
868
+ for (const t of tablesOrdered) {
869
+ for (const [name, value] of t.entries)
870
+ claim(name, classify(name, value));
871
+ }
872
+ for (const d of definePropEntries)
873
+ claim(d.name, classify(d.name, d.value));
874
+ /* Top-level member exports. Node's export-object identity rules decide
875
+ * each statement's fate against the ORIGINAL table (esbuildMainTarget's
876
+ * assignment): a member attached BEFORE it was discarded (the table
877
+ * replaced the object), a bare `exports.N =` AFTER it wrote the stale
878
+ * object — both keep only their lexer-visible NAME (value undefined,
879
+ * exactly Node's link answer: the any fallback below). A surviving
880
+ * member (`module.exports.N =` after the table, or any spelling when no
881
+ * table exists) re-emits its VALUE as a canonical entry — identifier
882
+ * RHS only; any other RHS would need a second evaluation, so the file
883
+ * is left alone (the offender path owns it — and when the file also
884
+ * carries erased interop wrappers, "untouched" is not on the menu:
885
+ * the live helper chain would fence at module load, so the package
886
+ * degrades with the note instead). Every member statement is then
887
+ * NEUTRALIZED: the canonical tail table replaces the object, and a
888
+ * stray earlier attach would meet the lowering's discard fence. */
889
+ for (const m of memberStmts) {
890
+ let cur = m.finalRhs;
891
+ while (ts.isParenthesizedExpression(cur))
892
+ cur = cur.expression;
893
+ const isVoidInit = cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
894
+ const discarded = tablePos !== null && (m.pos < tablePos || m.viaBareExports);
895
+ if (!isVoidInit) {
896
+ // Only side-effect-free RHS shapes may be neutralized (identifiers,
897
+ // scalar literals, ident-rooted member chains — chaseValue's set);
898
+ // anything else keeps the file untouched (the offender path owns
899
+ // it: neutralizing would drop an evaluation, keeping it meets the
900
+ // lowering's discard fence at load).
901
+ const chased = chaseValue(cur);
902
+ if (chased === null) {
903
+ return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
904
+ }
905
+ if (!discarded) {
906
+ for (const name of m.names)
907
+ claim(name, { name, kind: chased.kind, value: chased.text });
908
+ }
909
+ // discarded members keep only their lexer-visible NAME (the any
910
+ // fallback below) — value undefined, exactly Node's link answer
506
911
  }
507
- // discarded members keep only their lexer-visible NAME (the any
508
- // fallback below) — value undefined, exactly Node's link answer
912
+ neutralize.push({ start: m.start, end: m.end });
509
913
  }
510
- neutralize.push({ start: m.start, end: m.end });
511
- }
512
- /* Direct-vs-hoist for ident entries: only bindings declared by a
513
- * top-level const/function/class may be spelled directly (alias
514
- * plumbing needs an immutable target — a `var`/`let` in the table would
515
- * fence the whole statement). Everything else snapshots via a tail
516
- * const. */
517
- const immutableTopLevel = new Set();
518
- for (const stmt of sf.statements) {
519
- if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined)
520
- immutableTopLevel.add(stmt.name.text);
521
- else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined)
522
- immutableTopLevel.add(stmt.name.text);
523
- else if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
524
- for (const d of stmt.declarationList.declarations) {
525
- if (ts.isIdentifier(d.name))
526
- immutableTopLevel.add(d.name.text);
914
+ /* Direct-vs-hoist for ident entries: only bindings declared by a
915
+ * top-level const/function/class may be spelled directly (alias
916
+ * plumbing needs an immutable target — a `var`/`let` in the table would
917
+ * fence the whole statement). Everything else snapshots via a tail
918
+ * const. */
919
+ const immutableTopLevel = new Set();
920
+ for (const stmt of sf.statements) {
921
+ if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined)
922
+ immutableTopLevel.add(stmt.name.text);
923
+ else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined)
924
+ immutableTopLevel.add(stmt.name.text);
925
+ else if (ts.isVariableStatement(stmt) && (stmt.declarationList.flags & ts.NodeFlags.Const) !== 0) {
926
+ for (const d of stmt.declarationList.declarations) {
927
+ if (ts.isIdentifier(d.name))
928
+ immutableTopLevel.add(d.name.text);
929
+ }
527
930
  }
528
931
  }
932
+ for (const spec of entries.values()) {
933
+ if (spec.kind === "ident" && !immutableTopLevel.has(spec.value))
934
+ spec.kind = "hoist";
935
+ }
936
+ // Star enumerations for names the own entries left uncovered.
937
+ starSpecs.forEach((spec, i) => {
938
+ const target = resolveRelativeCjs(filePath, spec);
939
+ if (target === null)
940
+ return; // bare/unresolved: the spread entry carries it
941
+ const names = starTargetNames(target);
942
+ let refVar = null;
943
+ for (const n of names) {
944
+ if (n === "default" || n === "__esModule" || entries.has(n) || RESERVED_KEYS.has(n))
945
+ continue;
946
+ if (starMembers.some((m) => m.name === n))
947
+ continue; // earlier spec wins
948
+ if (refVar === null) {
949
+ refVar = `__scriptc_r${i}`;
950
+ requireConsts.push(`const ${refVar} = require(${JSON.stringify(spec)});`);
951
+ }
952
+ starMembers.push({ name: n, ref: refVar });
953
+ }
954
+ });
955
+ // The any fallback: every lexer-visible name nothing above covered.
956
+ for (const n of lexed.exports)
957
+ claim(n, { name: n, kind: "any" });
958
+ emitTable = entries.size > 0 || starSpecs.length > 0;
529
959
  }
530
- for (const spec of entries.values()) {
531
- if (spec.kind === "ident" && !immutableTopLevel.has(spec.value))
532
- spec.kind = "hoist";
960
+ /* A file with interop pads but no canonical table still rewrites: the
961
+ * pads erase the __toESM wrappers and the sweep below drops the dead
962
+ * helper chain — but nothing replaces the export plumbing, so none of
963
+ * it may be neutralized (the surviving text keeps exactly its
964
+ * untouched-file behavior). */
965
+ if (!emitTable) {
966
+ if (toEsm.pads.length === 0)
967
+ return null;
968
+ neutralize.length = 0;
533
969
  }
534
- // Star enumerations for names the own entries left uncovered.
535
- const starMembers = [];
536
- const requireConsts = [];
537
- starSpecs.forEach((spec, i) => {
538
- const target = resolveRelativeCjs(filePath, spec);
539
- if (target === null)
540
- return; // bare/unresolved: the spread entry carries it
541
- const names = starTargetNames(target);
542
- let refVar = null;
543
- for (const n of names) {
544
- if (n === "default" || n === "__esModule" || entries.has(n) || RESERVED_KEYS.has(n))
545
- continue;
546
- if (starMembers.some((m) => m.name === n))
547
- continue; // earlier spec wins
548
- if (refVar === null) {
549
- refVar = `__scriptc_r${i}`;
550
- requireConsts.push(`const ${refVar} = require(${JSON.stringify(spec)});`);
551
- }
552
- starMembers.push({ name: n, ref: refVar });
553
- }
554
- });
555
- // The any fallback: every lexer-visible name nothing above covered.
556
- for (const n of lexed.exports)
557
- claim(n, { name: n, kind: "any" });
558
- if (entries.size === 0 && starSpecs.length === 0)
559
- return null;
970
+ // Interop pads join the neutralized set BEFORE the sweep: the erased
971
+ // __toESM call sites read as dead references, so the helper declaration
972
+ // sweeps away and its __create/__getProtoOf chain follows.
973
+ neutralize.push(...toEsm.pads);
560
974
  /* Bundler HELPPER declarations left unused once the plumbing statements
561
975
  * are gone would still fence at module load (`var __defProp =
562
976
  * Object.defineProperty;` — a stdlib generic method as a value has no
563
977
  * lowering, and a top-level declaration's fence throws when init runs
564
978
  * it). Mark-and-sweep the RECOGNIZED helper names: a helper whose every
565
979
  * reference lies inside an already-neutralized span (the plumbing
566
- * calls, or another swept helper's declaration) is neutralized too.
567
- * Helpers live code still calls (__toESM interop wrappers, __commonJS
568
- * chunk factories) stay — their declarations are plain function values
569
- * whose bodies defer per the JS function-poison rule. */
980
+ * calls, the erased __toESM interop wrappers, or another swept helper's
981
+ * declaration) is neutralized too. Helpers live code still calls
982
+ * (__commonJS chunk factories) stay — their declarations are plain
983
+ * function values whose bodies defer per the JS function-poison rule. */
570
984
  {
571
985
  const HELPER_NAMES = new Set([
572
986
  "__create", "__defProp", "__defProps", "__getOwnPropDesc", "__getOwnPropDescs",
@@ -631,6 +1045,8 @@ export function rewriteBundlerCjsExports(source, filePath) {
631
1045
  for (const n of [...neutralize].sort((a, b) => b.start - a.start)) {
632
1046
  text = padSpan(text, n.start, n.end);
633
1047
  }
1048
+ if (!emitTable)
1049
+ return text; // interop pads only — nothing appends
634
1050
  const lines = [""];
635
1051
  const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any");
636
1052
  if (hasAnyFallback) {
@@ -642,6 +1058,18 @@ export function rewriteBundlerCjsExports(source, filePath) {
642
1058
  const hoists = [];
643
1059
  let hoistN = 0;
644
1060
  const keyOf = (name) => (isPlainName(name) ? name : JSON.stringify(name));
1061
+ /* Hoist texts are COPIED off the original source, where a `.default`
1062
+ * access on a module-valued interop binding still carries its member
1063
+ * spelling (the pads erase only source spans) — drop it here exactly
1064
+ * like the pads do, or the tail const would read a member the module
1065
+ * never exports. */
1066
+ const dropInteropDefault = (text) => {
1067
+ let out = text;
1068
+ for (const b of toEsm.moduleBindings) {
1069
+ out = out.replace(new RegExp(`\\b${b}\\s*\\.\\s*default\\b`, "g"), b);
1070
+ }
1071
+ return out;
1072
+ };
645
1073
  const plainParts = [];
646
1074
  for (const spec of entries.values()) {
647
1075
  if (spec.kind === "ident") {
@@ -649,7 +1077,7 @@ export function rewriteBundlerCjsExports(source, filePath) {
649
1077
  }
650
1078
  else if (spec.kind === "hoist") {
651
1079
  const v = `__scriptc_e${hoistN++}`;
652
- hoists.push(`const ${v} = ${spec.value};`);
1080
+ hoists.push(`const ${v} = ${dropInteropDefault(spec.value)};`);
653
1081
  plainParts.push(`${keyOf(spec.name)}: ${v},`);
654
1082
  }
655
1083
  else {