@nimbus-sh/core 0.6.0 → 0.7.0

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 (88) hide show
  1. package/README.md +47 -26
  2. package/dist/_shared/byte-stream.d.ts +5 -4
  3. package/dist/_shared/byte-stream.d.ts.map +1 -1
  4. package/dist/_shared/real-node-imports.d.ts +7 -0
  5. package/dist/_shared/real-node-imports.d.ts.map +1 -1
  6. package/dist/_shared/real-node-imports.js +8 -0
  7. package/dist/_shared/tarball.d.ts +23 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +38 -5
  10. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
  11. package/dist/_shared/vfs-write-ledger.js +22 -16
  12. package/dist/runtime/comment-strip.d.ts +0 -35
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +48 -20
  15. package/dist/runtime/esbuild-service.d.ts +38 -42
  16. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  17. package/dist/runtime/esbuild-service.js +494 -197
  18. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  19. package/dist/runtime/javascript-ast.js +45 -7
  20. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  21. package/dist/runtime/opentui-wasm-backend.js +18 -16
  22. package/dist/runtime/port-registry.d.ts +4 -2
  23. package/dist/runtime/port-registry.d.ts.map +1 -1
  24. package/dist/runtime/port-registry.js +60 -8
  25. package/dist/shell/stdin-adapter.d.ts +11 -0
  26. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  27. package/dist/shell/stdin-adapter.js +59 -0
  28. package/dist/shell/unix-commands.d.ts +2 -1
  29. package/dist/shell/unix-commands.d.ts.map +1 -1
  30. package/dist/shell/unix-commands.js +1078 -50
  31. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  32. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  33. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  34. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  35. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  36. package/dist/substrate/lifo/commands/system/npm.js +26 -43
  37. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  38. package/dist/substrate/lifo/commands/text/head.js +2 -1
  39. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  40. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  41. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  42. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  43. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  44. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  45. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  46. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  47. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  48. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  49. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  50. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  51. package/dist/substrate/lifo/shell/pipe.js +62 -18
  52. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  53. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  55. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  56. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  57. package/dist/vfs/sqlite-vfs.js +106 -23
  58. package/dist/workspace/nimbus-workspace.d.ts +15 -0
  59. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  60. package/dist/workspace/nimbus-workspace.js +20 -3
  61. package/dist/workspace/supervisor-op.d.ts +23 -0
  62. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  63. package/dist/workspace/supervisor-op.js +92 -0
  64. package/package.json +2 -2
  65. package/src/_shared/byte-stream.ts +5 -4
  66. package/src/_shared/real-node-imports.ts +8 -0
  67. package/src/_shared/tarball.ts +56 -5
  68. package/src/_shared/vfs-write-ledger.ts +22 -16
  69. package/src/runtime/comment-strip.ts +42 -23
  70. package/src/runtime/esbuild-service.ts +566 -217
  71. package/src/runtime/javascript-ast.ts +40 -9
  72. package/src/runtime/opentui-wasm-backend.ts +19 -17
  73. package/src/runtime/port-registry.ts +66 -8
  74. package/src/shell/stdin-adapter.ts +58 -0
  75. package/src/shell/unix-commands.ts +1054 -50
  76. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  77. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  78. package/src/substrate/lifo/commands/system/npm.ts +58 -38
  79. package/src/substrate/lifo/commands/text/head.ts +2 -1
  80. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  81. package/src/substrate/lifo/commands/types.ts +8 -5
  82. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  83. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  84. package/src/substrate/lifo/shell/pipe.ts +65 -24
  85. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  86. package/src/vfs/sqlite-vfs.ts +104 -23
  87. package/src/workspace/nimbus-workspace.ts +26 -2
  88. package/src/workspace/supervisor-op.ts +117 -0
@@ -16,10 +16,11 @@
16
16
  * this is acceptable for Phase 3. Phase 4+ can move it to a dedicated
17
17
  * facet once wasm module passing to dynamic workers is stable.
18
18
  */
19
- import { CRED_KERNEL } from './os-contracts.js';
20
19
  import { resolvePackageEntry, resolveExports } from '../_shared/exports-resolver.js';
21
20
  import { normalizeVfsPath, stripLeadingSlashes } from '../vfs/path.js';
22
21
  import { errorText } from '../_shared/error-text.js';
22
+ import { tokenizer, tokTypes } from 'acorn';
23
+ import { literalStringValue, nodeList, nodeName, nodeProp, parseJavaScriptModule, } from './javascript-ast.js';
23
24
  /**
24
25
  * Bundler version tag. BUMP THIS whenever bundling semantics change —
25
26
  * the esbuild plugin's resolver logic, the shared-externals rules, the
@@ -52,7 +53,7 @@ import { errorText } from '../_shared/error-text.js';
52
53
  * rows hold post-rewrite text and must be re-bundled. user_module_
53
54
  * transforms is likewise re-keyed by mount base.
54
55
  */
55
- export const BUNDLER_VERSION = 'v8';
56
+ export const BUNDLER_VERSION = 'v9';
56
57
  // ── Shared-runtime externals ────────────────────────────────────────────
57
58
  /**
58
59
  * Returns the list of specifiers that must be marked `external` when bundling
@@ -139,95 +140,6 @@ export function getSharedRuntimeExternals(specifier) {
139
140
  return true;
140
141
  });
141
142
  }
142
- /**
143
- * Detect top-level await in source. Used by `EsbuildService.transform`
144
- * to decide whether to async-IIFE-wrap CJS-target sources (see
145
- * transform()'s header comment).
146
- *
147
- * Token scan over a comment-and-string-stripped source. Tracks paren
148
- * depth AND function-body depth so a `{` inside a parameter list (a
149
- * default-value object literal — `async function f(opts = {})`) is NOT
150
- * mistaken for the function body. The earlier heuristic lacked the paren
151
- * guard: the `= {}` braces consumed the body-tracking slot, leaving the
152
- * real body untracked so an internal `await` read as top-level. That
153
- * wrongly routed such files (e.g. @bluwy/giget-core's
154
- * download-template.js, reached by create-astro) into the two-pass
155
- * ESM→require rewrite, whose assembled output still carried export/import
156
- * statements → "Cannot use import statement outside a module" at startup.
157
- *
158
- * Arrow bodies are distinguished from arrow expression bodies (the
159
- * `arrowPending` flag): only a `{` immediately following `=>` opens a
160
- * function scope. The idiomatic object-returning arrow `x => ({ ... })`
161
- * has its `{` at parenDepth 1, so it opens no body slot — otherwise the
162
- * leaked slot would be consumed by a later top-level `{ ... }` block,
163
- * making a real top-level `await` inside it read as non-TLA (false
164
- * negative) and routing genuine ESM-with-TLA into the wrong transform.
165
- *
166
- * A regex/token scan (not acorn) is kept here deliberately: pulling
167
- * acorn into the esbuild-service bundle chunk duplicates its ~15 KiB
168
- * Unicode identifier tables.
169
- */
170
- export function hasTopLevelAwait(src) {
171
- if (!src || src.indexOf('await') === -1)
172
- return false;
173
- const stripped = stripCommentsAndStrings(src);
174
- // Tokens: keywords, arrow, and each bracket kind. `parenDepth` rises
175
- // inside `(...)`; a `{` only opens a function body when a `function`/
176
- // `=>` is pending AND we're at parenDepth 0 (past the param list).
177
- const re = /\b(await|function|class)\b|=>|\{|\}|\(|\)/g;
178
- const fnEntryDepths = [];
179
- let depth = 0;
180
- let parenDepth = 0;
181
- let pendingFn = 0;
182
- // An arrow can have either a block body (`=> { ... }`) — which opens a
183
- // function scope — or an expression body (`=> expr`), which does not.
184
- // We can only tell which by looking at the token right after `=>`: a
185
- // block body has `{` as its very next token. `arrowPending` carries
186
- // that "the next `{` (and only an immediately-following `{`) is this
187
- // arrow's body" intent, separately from `pendingFn` so the idiomatic
188
- // `x => ({ ... })` (object-returning expression body, `{` at parenDepth
189
- // 1) does not leak a body slot onto a later top-level block.
190
- let arrowPending = false;
191
- for (let m = re.exec(stripped); m !== null; m = re.exec(stripped)) {
192
- const tok = m[0];
193
- if (arrowPending && tok !== '{')
194
- arrowPending = false;
195
- if (tok === '(') {
196
- parenDepth++;
197
- }
198
- else if (tok === ')') {
199
- if (parenDepth > 0)
200
- parenDepth--;
201
- }
202
- else if (tok === '{') {
203
- depth++;
204
- if (arrowPending) {
205
- fnEntryDepths.push(depth);
206
- arrowPending = false;
207
- }
208
- else if (pendingFn > 0 && parenDepth === 0) {
209
- fnEntryDepths.push(depth);
210
- pendingFn--;
211
- }
212
- }
213
- else if (tok === '}') {
214
- if (fnEntryDepths.length > 0 && fnEntryDepths[fnEntryDepths.length - 1] === depth)
215
- fnEntryDepths.pop();
216
- depth--;
217
- }
218
- else if (tok === 'function' || tok === 'class') {
219
- pendingFn++;
220
- }
221
- else if (tok === '=>') {
222
- arrowPending = true;
223
- }
224
- else if (tok === 'await') {
225
- if (fnEntryDepths.length === 0)
226
- return true;
227
- }
228
- }
229
- return false;
230
- }
231
143
  /**
232
144
  * Cheap heuristic: does the source contain a top-level ESM `import`
233
145
  * statement? Used by `EsbuildService.transform` to detect sources that
@@ -261,10 +173,8 @@ export function hasTopLevelAwait(src) {
261
173
  * EXCLUDED — those are expressions, legal anywhere including IIFE
262
174
  * bodies, and need no rewrite.
263
175
  *
264
- * Operates on the comment-and-string-stripped source so commented-out
265
- * imports and "import" appearing inside string literals don't trigger.
266
- * We reuse the comment/string stripper from `hasTopLevelAwait`'s pass
267
- * — see `stripCommentsAndStrings` below.
176
+ * Operates on comment-and-string-stripped source so commented-out imports
177
+ * and "import" inside string literals do not trigger.
268
178
  */
269
179
  function hasEsmImports(src) {
270
180
  if (!src || src.indexOf('import') === -1)
@@ -289,10 +199,8 @@ function hasEsmExports(src) {
289
199
  * source, replacing each with a single space. The result is byte-aligned
290
200
  * with the input on a per-line basis (newlines are preserved), so error
291
201
  * line numbers from downstream parsers still align with the original.
292
- *
293
- * Shared by `hasTopLevelAwait` (which had this inline) and
294
- * `hasEsmImports`. Pure function — no caching needed for the small
295
- * inputs we see (typical .mjs entry-point: <2KiB).
202
+ * Shared by the import/export classifiers. Pure function — no caching needed
203
+ * for the small entry points that reach the true-TLA fallback.
296
204
  */
297
205
  function stripCommentsAndStrings(src) {
298
206
  let stripped = '';
@@ -304,9 +212,8 @@ function stripCommentsAndStrings(src) {
304
212
  // start-of-file). Pre-fix the stripper had no regex awareness, so
305
213
  // patterns like `var X = /^(?:'…)/` contained an unmatched `'` that
306
214
  // bit it as a string opener that didn't close until many lines
307
- // later — corrupting every downstream classifier (export-scanner,
308
- // hasEsmImports, hasTopLevelAwait). Real-world bite: sv-utils@0.0.3
309
- // index.mjs has dozens of these regex literals.
215
+ // later — corrupting the import/export classifiers. Real-world bite:
216
+ // sv-utils@0.0.3 index.mjs has dozens of these regex literals.
310
217
  let lastNonWsChar = '';
311
218
  const recordOut = (ch) => {
312
219
  if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') {
@@ -559,7 +466,7 @@ function convertEsmImportsToRequire(src) {
559
466
  continue;
560
467
  }
561
468
  // Side-effect import: `import "m";` / `import 'm';`
562
- let m = line.match(/^[ \t]*import\s+["']([^"']+)["']\s*;?\s*$/);
469
+ let m = line.match(/^[ \t]*import\s*["']([^"']+)["']\s*;?\s*$/);
563
470
  if (m) {
564
471
  requires.push(`require(${JSON.stringify(m[1])});`);
565
472
  continue;
@@ -572,14 +479,14 @@ function convertEsmImportsToRequire(src) {
572
479
  // statement survived into the async-IIFE wrap → SyntaxError
573
480
  // "import statement outside module" at facet pre-compile.
574
481
  // Default + namespace: `import x, * as ns from "m";`
575
- m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\*\s+as\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
482
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
576
483
  if (m) {
577
484
  const def = m[1], ns = m[2], mod = m[3];
578
485
  requires.push(`const ${ns} = require(${JSON.stringify(mod)}); const ${def} = ${ns}.__esModule ? ${ns}.default : ${ns};`);
579
486
  continue;
580
487
  }
581
488
  // Default + named: `import x, { a, b as c } from "m";`
582
- m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\{([^}]+)\}\s+from\s+["']([^"']+)["']\s*;?\s*$/);
489
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\{([^}]+)\}\s+from\s*["']([^"']+)["']\s*;?\s*$/);
583
490
  if (m) {
584
491
  const def = m[1], bindings = m[2], mod = m[3];
585
492
  const tmp = `_nimbus_m_${counter++}`;
@@ -593,13 +500,13 @@ function convertEsmImportsToRequire(src) {
593
500
  continue;
594
501
  }
595
502
  // Namespace: `import * as ns from "m";`
596
- m = line.match(/^[ \t]*import\s+\*\s+as\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
503
+ m = line.match(/^[ \t]*import\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
597
504
  if (m) {
598
505
  requires.push(`const ${m[1]} = require(${JSON.stringify(m[2])});`);
599
506
  continue;
600
507
  }
601
508
  // Named only: `import { a, b as c } from "m";`
602
- m = line.match(/^[ \t]*import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']\s*;?\s*$/);
509
+ m = line.match(/^[ \t]*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']\s*;?\s*$/);
603
510
  if (m) {
604
511
  const bindings = m[1], mod = m[2];
605
512
  const named = bindings.split(',').map((b) => {
@@ -612,7 +519,7 @@ function convertEsmImportsToRequire(src) {
612
519
  continue;
613
520
  }
614
521
  // Default only: `import x from "m";`
615
- m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
522
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
616
523
  if (m) {
617
524
  const def = m[1], mod = m[2];
618
525
  requires.push(`const ${def} = (() => { const _m = require(${JSON.stringify(mod)}); return _m && _m.__esModule ? _m.default : _m; })();`);
@@ -801,29 +708,399 @@ function convertEsmImportsToRequire(src) {
801
708
  }
802
709
  return { requires: requires.join('\n'), body: out.join('\n') };
803
710
  }
711
+ function topLevelModuleDeclarationRanges(source) {
712
+ try {
713
+ const tokens = tokenizer(source, {
714
+ ecmaVersion: 'latest',
715
+ sourceType: 'module',
716
+ allowHashBang: true,
717
+ });
718
+ const ranges = [];
719
+ let active = null;
720
+ let braces = 0;
721
+ let parens = 0;
722
+ let brackets = 0;
723
+ const updateDepth = (type) => {
724
+ if (type === tokTypes.braceL || type === tokTypes.dollarBraceL)
725
+ braces++;
726
+ else if (type === tokTypes.braceR)
727
+ braces = Math.max(0, braces - 1);
728
+ else if (type === tokTypes.parenL)
729
+ parens++;
730
+ else if (type === tokTypes.parenR)
731
+ parens = Math.max(0, parens - 1);
732
+ else if (type === tokTypes.bracketL)
733
+ brackets++;
734
+ else if (type === tokTypes.bracketR)
735
+ brackets = Math.max(0, brackets - 1);
736
+ };
737
+ while (true) {
738
+ const token = tokens.getToken();
739
+ const type = token.type;
740
+ if (type === tokTypes.eof)
741
+ return active ? null : ranges;
742
+ if (active) {
743
+ updateDepth(type);
744
+ if (type === tokTypes.semi && braces === 0 && parens === 0 && brackets === 0) {
745
+ ranges.push({ ...active, end: token.end });
746
+ active = null;
747
+ }
748
+ continue;
749
+ }
750
+ const topLevel = braces === 0 && parens === 0 && brackets === 0;
751
+ if (topLevel && type === tokTypes._import) {
752
+ const next = tokens.getToken();
753
+ if (next.type !== tokTypes.parenL && next.type !== tokTypes.dot) {
754
+ active = { start: token.start, kind: 'import' };
755
+ }
756
+ updateDepth(next.type);
757
+ continue;
758
+ }
759
+ if (topLevel && type === tokTypes._export) {
760
+ active = { start: token.start, kind: 'export' };
761
+ continue;
762
+ }
763
+ updateDepth(type);
764
+ }
765
+ }
766
+ catch {
767
+ return null;
768
+ }
769
+ }
770
+ function hasUnscopedAwait(source) {
771
+ try {
772
+ const tokens = tokenizer(source, {
773
+ ecmaVersion: 'latest',
774
+ sourceType: 'module',
775
+ allowHashBang: true,
776
+ });
777
+ const functionBraces = [];
778
+ const functionParenDepths = [];
779
+ const methodParenCandidates = [];
780
+ const arrowExpressions = [];
781
+ let bracketDepth = 0;
782
+ let pendingMethodBody = false;
783
+ let pendingArrowBody = false;
784
+ let pendingFunctionKeyword = false;
785
+ let previous = tokTypes.eof;
786
+ let previousEnd = 0;
787
+ while (true) {
788
+ const token = tokens.getToken();
789
+ const type = token.type;
790
+ if (type === tokTypes.eof)
791
+ return false;
792
+ if (pendingMethodBody && type !== tokTypes.braceL)
793
+ pendingMethodBody = false;
794
+ if (pendingArrowBody && type !== tokTypes.braceL) {
795
+ arrowExpressions.push({
796
+ parens: methodParenCandidates.length,
797
+ braces: functionBraces.length,
798
+ brackets: bracketDepth,
799
+ });
800
+ pendingArrowBody = false;
801
+ }
802
+ if (pendingFunctionKeyword) {
803
+ if (type === tokTypes.colon || type === tokTypes.comma || type === tokTypes.braceR
804
+ || type === tokTypes.parenR || type === tokTypes.bracketR || type === tokTypes.eq)
805
+ functionParenDepths.pop();
806
+ pendingFunctionKeyword = false;
807
+ }
808
+ if (source.slice(previousEnd, token.start).includes('\n')) {
809
+ while (arrowExpressions.length > 0) {
810
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
811
+ if (methodParenCandidates.length !== arrow.parens
812
+ || functionBraces.length !== arrow.braces
813
+ || bracketDepth !== arrow.brackets)
814
+ break;
815
+ arrowExpressions.pop();
816
+ }
817
+ }
818
+ while (arrowExpressions.length > 0) {
819
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
820
+ const delimited = (type === tokTypes.semi || type === tokTypes.comma)
821
+ && methodParenCandidates.length === arrow.parens
822
+ && functionBraces.length === arrow.braces
823
+ && bracketDepth === arrow.brackets;
824
+ const closed = (type === tokTypes.parenR && methodParenCandidates.length === arrow.parens)
825
+ || (type === tokTypes.bracketR && bracketDepth === arrow.brackets)
826
+ || (type === tokTypes.braceR && functionBraces.length === arrow.braces);
827
+ if (!delimited && !closed)
828
+ break;
829
+ arrowExpressions.pop();
830
+ }
831
+ if (type === tokTypes.name
832
+ && source.slice(token.start, token.end) === 'await'
833
+ && !functionBraces.includes(true)
834
+ && arrowExpressions.length === 0)
835
+ return true;
836
+ if (type === tokTypes._function || type === tokTypes._class) {
837
+ if (previous !== tokTypes.dot && previous !== tokTypes.questionDot) {
838
+ functionParenDepths.push(methodParenCandidates.length);
839
+ pendingFunctionKeyword = true;
840
+ }
841
+ }
842
+ else if (type === tokTypes.arrow) {
843
+ pendingArrowBody = true;
844
+ }
845
+ else if (type === tokTypes.parenL) {
846
+ methodParenCandidates.push(functionBraces.length > 0
847
+ && (previous === tokTypes.name || previous === tokTypes.string
848
+ || previous === tokTypes.num || previous === tokTypes.bracketR));
849
+ }
850
+ else if (type === tokTypes.parenR) {
851
+ pendingMethodBody = methodParenCandidates.pop() === true;
852
+ }
853
+ else if (type === tokTypes.bracketL) {
854
+ bracketDepth++;
855
+ }
856
+ else if (type === tokTypes.bracketR) {
857
+ bracketDepth = Math.max(0, bracketDepth - 1);
858
+ }
859
+ else if (type === tokTypes.dollarBraceL) {
860
+ functionBraces.push(false);
861
+ }
862
+ else if (type === tokTypes.braceL) {
863
+ const functionBody = pendingArrowBody
864
+ || pendingMethodBody
865
+ || functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length;
866
+ if (functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length) {
867
+ functionParenDepths.pop();
868
+ }
869
+ functionBraces.push(functionBody);
870
+ pendingArrowBody = false;
871
+ pendingMethodBody = false;
872
+ }
873
+ else if (type === tokTypes.braceR) {
874
+ functionBraces.pop();
875
+ }
876
+ previousEnd = token.end;
877
+ previous = type;
878
+ }
879
+ }
880
+ catch {
881
+ return true;
882
+ }
883
+ }
884
+ function convertBundledModuleDeclarations(snippets) {
885
+ const imports = [];
886
+ const exports = [];
887
+ let importIndex = 0;
888
+ let markedEsm = false;
889
+ for (const snippet of snippets) {
890
+ const bindingList = snippet.match(/^[ \t]*export\s*\{([\s\S]*)\}\s*;?\s*$/);
891
+ if (bindingList && !/\}\s*from\b/.test(snippet)) {
892
+ if (!markedEsm) {
893
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
894
+ markedEsm = true;
895
+ }
896
+ for (const binding of bindingList[1].split(',')) {
897
+ const match = binding.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
898
+ if (!match)
899
+ return null;
900
+ const local = match[1];
901
+ const exported = match[2] || local;
902
+ exports.push(`Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`);
903
+ }
904
+ continue;
905
+ }
906
+ let ast;
907
+ try {
908
+ ast = parseJavaScriptModule(snippet);
909
+ }
910
+ catch {
911
+ return null;
912
+ }
913
+ const body = nodeList(ast, 'body');
914
+ if (body.length !== 1)
915
+ return null;
916
+ const declaration = body[0];
917
+ if (declaration.type === 'ImportDeclaration') {
918
+ const source = literalStringValue(nodeProp(declaration, 'source'));
919
+ if (!source)
920
+ return null;
921
+ const specifiers = nodeList(declaration, 'specifiers');
922
+ if (specifiers.length === 0) {
923
+ imports.push(`module.require(${JSON.stringify(source)});`);
924
+ continue;
925
+ }
926
+ const moduleName = `__nimbus_import_${importIndex++}`;
927
+ imports.push(`const ${moduleName} = module.require(${JSON.stringify(source)});`);
928
+ for (const specifier of specifiers) {
929
+ const local = nodeName(nodeProp(specifier, 'local'));
930
+ if (!local)
931
+ return null;
932
+ if (specifier.type === 'ImportDefaultSpecifier') {
933
+ imports.push(`const ${local} = ${moduleName} && ${moduleName}.__esModule ? ${moduleName}.default : ${moduleName};`);
934
+ }
935
+ else if (specifier.type === 'ImportNamespaceSpecifier') {
936
+ imports.push(`const ${local} = ${moduleName};`);
937
+ }
938
+ else if (specifier.type === 'ImportSpecifier') {
939
+ const imported = nodeName(nodeProp(specifier, 'imported'));
940
+ if (!imported)
941
+ return null;
942
+ imports.push(`const ${local} = ${moduleName}[${JSON.stringify(imported)}];`);
943
+ }
944
+ else {
945
+ return null;
946
+ }
947
+ }
948
+ continue;
949
+ }
950
+ if (declaration.type === 'ExportNamedDeclaration') {
951
+ if (nodeProp(declaration, 'source') || nodeProp(declaration, 'declaration'))
952
+ return null;
953
+ if (!markedEsm) {
954
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
955
+ markedEsm = true;
956
+ }
957
+ for (const specifier of nodeList(declaration, 'specifiers')) {
958
+ const local = nodeName(nodeProp(specifier, 'local'));
959
+ const exported = nodeName(nodeProp(specifier, 'exported'));
960
+ if (!local || !exported)
961
+ return null;
962
+ exports.push(`Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`);
963
+ }
964
+ continue;
965
+ }
966
+ if (declaration.type === 'ExportDefaultDeclaration') {
967
+ const value = nodeProp(declaration, 'declaration');
968
+ if (!value || typeof value.start !== 'number' || typeof value.end !== 'number')
969
+ return null;
970
+ if (value.type === 'FunctionDeclaration' || value.type === 'ClassDeclaration')
971
+ return null;
972
+ if (!markedEsm) {
973
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
974
+ markedEsm = true;
975
+ }
976
+ exports.push(`Object.defineProperty(module.exports, "default", { enumerable: true, value: (${snippet.slice(value.start, value.end)}) });`);
977
+ continue;
978
+ }
979
+ return null;
980
+ }
981
+ return { imports: imports.join('\n'), exports: exports.join('\n') };
982
+ }
983
+ function importMetaEdits(source, absoluteUrl) {
984
+ const edits = [];
985
+ try {
986
+ const tokens = tokenizer(source, {
987
+ ecmaVersion: 'latest',
988
+ sourceType: 'module',
989
+ allowHashBang: true,
990
+ });
991
+ while (true) {
992
+ const start = tokens.getToken();
993
+ if (start.type === tokTypes.eof)
994
+ return edits;
995
+ if (start.type !== tokTypes._import)
996
+ continue;
997
+ const dot1 = tokens.getToken();
998
+ if (dot1.type !== tokTypes.dot)
999
+ continue;
1000
+ const meta = tokens.getToken();
1001
+ if (meta.type !== tokTypes.name || source.slice(meta.start, meta.end) !== 'meta')
1002
+ return null;
1003
+ const dot2 = tokens.getToken();
1004
+ if (dot2.type !== tokTypes.dot)
1005
+ return null;
1006
+ const property = tokens.getToken();
1007
+ if (property.type !== tokTypes.name)
1008
+ return null;
1009
+ const propertyName = source.slice(property.start, property.end);
1010
+ if (propertyName === 'url') {
1011
+ edits.push({ start: start.start, end: property.end, text: JSON.stringify(absoluteUrl) });
1012
+ }
1013
+ else if (propertyName === 'resolve') {
1014
+ edits.push({
1015
+ start: start.start,
1016
+ end: property.end,
1017
+ text: `(specifier => globalThis.__nimbusImportMetaResolve(specifier, ${JSON.stringify(absoluteUrl)}))`,
1018
+ });
1019
+ }
1020
+ else {
1021
+ return null;
1022
+ }
1023
+ }
1024
+ }
1025
+ catch {
1026
+ return null;
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Converts bundler-emitted ESM without constructing an AST or loading
1031
+ * esbuild-wasm. Returns null for module declarations that are not the compact,
1032
+ * semicolon-terminated shapes emitted by current JS bundlers.
1033
+ */
1034
+ export function rewriteBundledEsmToCjs(source, absoluteUrl) {
1035
+ if (hasUnscopedAwait(source))
1036
+ return null;
1037
+ const declarations = topLevelModuleDeclarationRanges(source);
1038
+ if (!declarations || declarations.length === 0)
1039
+ return null;
1040
+ const declarationSnippets = declarations.map(({ start, end }) => source.slice(start, end));
1041
+ for (let i = 0; i < declarations.length; i++) {
1042
+ if (/^[ \t]*export\s+default\b/.test(declarationSnippets[i])
1043
+ && source.slice(declarations[i].end).trim() !== '')
1044
+ return null;
1045
+ }
1046
+ const converted = convertBundledModuleDeclarations(declarationSnippets);
1047
+ if (!converted)
1048
+ return null;
1049
+ const metaEdits = importMetaEdits(source, absoluteUrl);
1050
+ if (!metaEdits)
1051
+ return null;
1052
+ const edits = [
1053
+ ...declarations.map(({ start, end }) => ({ start, end, text: '' })),
1054
+ ...metaEdits.filter((edit) => !declarations.some(({ start, end }) => edit.start >= start && edit.end <= end)),
1055
+ ].sort((a, b) => a.start - b.start);
1056
+ const bodyParts = [];
1057
+ let cursor = 0;
1058
+ for (const edit of edits) {
1059
+ if (edit.start < cursor)
1060
+ return null;
1061
+ bodyParts.push(source.slice(cursor, edit.start), edit.text);
1062
+ cursor = edit.end;
1063
+ }
1064
+ bodyParts.push(source.slice(cursor));
1065
+ const body = bodyParts.join('');
1066
+ return {
1067
+ code: converted.imports + '\n' + body + '\n' + converted.exports,
1068
+ map: '',
1069
+ warnings: [],
1070
+ };
1071
+ }
804
1072
  import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm';
805
1073
  /**
806
1074
  * Cached reference to the esbuild namespace. Populated on first
807
1075
  * `loadEsbuild()` call; nullable until then so module-load code paths
808
1076
  * that never touch bundling can complete without ever evaluating
809
- * `esbuild-wasm/lib/main.js`.
1077
+ * esbuild-wasm's JS at all.
810
1078
  */
811
1079
  let _esbuildMod = null;
812
1080
  let _esbuildLoadPromise = null;
813
1081
  /**
814
- * Lazily load the esbuild-wasm namespace. Safe to call many times;
815
- * concurrent callers share a single in-flight Promise. Throws if the
816
- * CJS main module itself can't run (i.e. the runtime doesn't support
817
- * the require('fs') pattern esbuild-wasm uses) — callers should catch
818
- * and surface a helpful error rather than crash the Worker.
1082
+ * Load the esbuild-wasm namespace. Safe to call many times; concurrent
1083
+ * callers share a single in-flight Promise, and a rejection clears the
1084
+ * cache so a later call can retry.
1085
+ *
1086
+ * Exported so `tests/unit/esbuild-wasm-entrypoint.mjs` can drive the real
1087
+ * specifier under a Node-style resolver. A test that restated the specifier
1088
+ * would grade its own copy of it, and this defect reached production
1089
+ * precisely because nothing graded the resolution.
1090
+ *
1091
+ * The specifier stays a literal: a computed one would defeat the host
1092
+ * bundler's static analysis and leave the module out of the deployed worker.
819
1093
  */
820
- async function loadEsbuild() {
1094
+ export async function loadEsbuild() {
821
1095
  if (_esbuildMod)
822
1096
  return _esbuildMod;
823
1097
  if (_esbuildLoadPromise)
824
1098
  return _esbuildLoadPromise;
825
1099
  _esbuildLoadPromise = (async () => {
826
- const mod = await import('esbuild-wasm');
1100
+ // Deliberately dynamic: a static import would evaluate esbuild-wasm in
1101
+ // every session, including the ones that only serve a shell and never
1102
+ // bundle. The specifier is still a literal so the host bundler sees it.
1103
+ const mod = await import('esbuild-wasm/esm/browser.js');
827
1104
  _esbuildMod = mod;
828
1105
  return _esbuildMod;
829
1106
  })();
@@ -831,12 +1108,101 @@ async function loadEsbuild() {
831
1108
  return await _esbuildLoadPromise;
832
1109
  }
833
1110
  catch (e) {
834
- // Reset so a future call can retry (e.g., after the caller has done
835
- // environment setup we didn't anticipate).
836
1111
  _esbuildLoadPromise = null;
837
1112
  throw e;
838
1113
  }
839
1114
  }
1115
+ async function transformWithEsbuild(esbuildApi, source, options) {
1116
+ let code = source;
1117
+ const format = options?.format || 'esm';
1118
+ const loader = options?.loader || 'ts';
1119
+ if (format === 'cjs') {
1120
+ try {
1121
+ const direct = await esbuildApi.transform(code, {
1122
+ loader,
1123
+ format,
1124
+ target: options?.target || 'esnext',
1125
+ sourcemap: options?.sourcemap ?? false,
1126
+ minify: options?.minify ?? false,
1127
+ jsx: options?.jsx,
1128
+ jsxFactory: options?.jsxFactory,
1129
+ jsxFragment: options?.jsxFragment,
1130
+ tsconfigRaw: options?.tsconfigRaw,
1131
+ define: options?.define,
1132
+ supported: { 'dynamic-import': false },
1133
+ });
1134
+ return {
1135
+ code: direct.code,
1136
+ map: direct.map || '',
1137
+ warnings: direct.warnings?.map((warning) => ({
1138
+ text: warning.text,
1139
+ location: warning.location,
1140
+ })) || [],
1141
+ };
1142
+ }
1143
+ catch (error) {
1144
+ const message = error instanceof Error ? error.message : String(error);
1145
+ if (!/top-level await.*not supported.*cjs/i.test(message))
1146
+ throw error;
1147
+ }
1148
+ if (hasEsmImports(code) || hasEsmExports(code)) {
1149
+ const pass1 = await esbuildApi.transform(code, {
1150
+ loader,
1151
+ format: 'esm',
1152
+ target: options?.target || 'esnext',
1153
+ sourcemap: false,
1154
+ minify: false,
1155
+ jsx: options?.jsx,
1156
+ jsxFactory: options?.jsxFactory,
1157
+ jsxFragment: options?.jsxFragment,
1158
+ tsconfigRaw: options?.tsconfigRaw,
1159
+ define: options?.define,
1160
+ supported: { 'dynamic-import': false },
1161
+ });
1162
+ const { requires, body } = convertEsmImportsToRequire(pass1.code);
1163
+ return {
1164
+ code: requires + '\nreturn (async () => {\n' + body + '\n})();\n',
1165
+ map: '',
1166
+ warnings: pass1.warnings?.map((warning) => ({
1167
+ text: warning.text,
1168
+ location: warning.location,
1169
+ })) || [],
1170
+ };
1171
+ }
1172
+ code = 'return (async () => {\n' + code + '\n})();\n';
1173
+ }
1174
+ const result = await esbuildApi.transform(code, {
1175
+ loader,
1176
+ format,
1177
+ target: options?.target || 'esnext',
1178
+ sourcemap: options?.sourcemap ?? false,
1179
+ minify: options?.minify ?? false,
1180
+ jsx: options?.jsx,
1181
+ jsxFactory: options?.jsxFactory,
1182
+ jsxFragment: options?.jsxFragment,
1183
+ tsconfigRaw: options?.tsconfigRaw,
1184
+ define: options?.define,
1185
+ supported: { 'dynamic-import': false },
1186
+ });
1187
+ return {
1188
+ code: result.code,
1189
+ map: result.map || '',
1190
+ warnings: result.warnings?.map((warning) => ({
1191
+ text: warning.text,
1192
+ location: warning.location,
1193
+ })) || [],
1194
+ };
1195
+ }
1196
+ /** Source needed by the slim Worker Loader transform isolate. */
1197
+ export function generateEsbuildTransformRuntimeSource() {
1198
+ return [
1199
+ stripCommentsAndStrings.toString(),
1200
+ hasEsmImports.toString(),
1201
+ hasEsmExports.toString(),
1202
+ convertEsmImportsToRequire.toString(),
1203
+ transformWithEsbuild.toString(),
1204
+ ].join('\n');
1205
+ }
840
1206
  // ── EsbuildService ──────────────────────────────────────────────────────
841
1207
  export class EsbuildService {
842
1208
  vfs;
@@ -844,8 +1210,9 @@ export class EsbuildService {
844
1210
  initPromise = null;
845
1211
  /** Resolved esbuild namespace — populated by ensureInit() after loadEsbuild(). */
846
1212
  _esbuild = null;
1213
+ /** Build reads use only the caller-supplied view; omit it for transform-only use. */
847
1214
  constructor(vfs) {
848
- this.vfs = vfs.as(CRED_KERNEL);
1215
+ this.vfs = vfs ?? null;
849
1216
  }
850
1217
  /**
851
1218
  * Initialize esbuild-wasm (lazy, on first use). Loads the namespace
@@ -987,82 +1354,7 @@ export class EsbuildService {
987
1354
  */
988
1355
  async transform(code, options) {
989
1356
  await this.ensureInit();
990
- const format = options?.format || 'esm';
991
- const loader = options?.loader || 'ts';
992
- // ── CJS + TLA + ESM module syntax: two-pass with module rewrite ──
993
- // This precedes the simple TLA-only IIFE wrap. We test ESM module
994
- // syntax first because the IIFE wrap is incompatible with import
995
- // and export statements; if both conditions hold we MUST go through
996
- // the rewrite path.
997
- if (format === 'cjs' && hasTopLevelAwait(code) && (hasEsmImports(code) || hasEsmExports(code))) {
998
- // Pass 1: emit ESM so esbuild accepts TLA + imports without complaint.
999
- // We use the same loader/target so TS/JSX is handled here too.
1000
- const pass1 = await this._esbuild.transform(code, {
1001
- loader,
1002
- format: 'esm',
1003
- target: options?.target || 'esnext',
1004
- sourcemap: false, // pass-1 source map is discarded; pass-2 doesn't run esbuild
1005
- minify: false, // minify only on the final output if requested
1006
- jsx: options?.jsx,
1007
- jsxFactory: options?.jsxFactory,
1008
- jsxFragment: options?.jsxFragment,
1009
- tsconfigRaw: options?.tsconfigRaw,
1010
- define: options?.define,
1011
- // Dynamic-import lowering: rewrites `import(x)` to
1012
- // `Promise.resolve().then(() => __toESM(require(x)))` so the
1013
- // call routes through Nimbus's scopedRequire → VFS lookup
1014
- // instead of workerd's worker-module-map resolver (which only
1015
- // knows {'runner.js': workerCode}). Without this, every user
1016
- // dynamic import() rejects with "No such module ..." — and if
1017
- // user code has no .catch() (e.g. create-astro.mjs's
1018
- // `import('./dist/index.js').then(({main}) => main())`), the
1019
- // rejection is unhandled and the facet exits silently
1020
- supported: { 'dynamic-import': false },
1021
- });
1022
- const { requires, body } = convertEsmImportsToRequire(pass1.code);
1023
- const assembled = requires + '\n' +
1024
- 'return (async () => {\n' +
1025
- body +
1026
- '\n})();\n';
1027
- return {
1028
- code: assembled,
1029
- map: '',
1030
- warnings: pass1.warnings?.map((w) => ({
1031
- text: w.text,
1032
- location: w.location,
1033
- })) || [],
1034
- };
1035
- }
1036
- // ── CJS + TLA (no ESM imports): single-pass IIFE wrap (P2 fix) ──
1037
- let sourceToTransform = code;
1038
- if (format === 'cjs' && hasTopLevelAwait(code)) {
1039
- sourceToTransform =
1040
- 'return (async () => {\n' +
1041
- code +
1042
- '\n})();\n';
1043
- }
1044
- const result = await this._esbuild.transform(sourceToTransform, {
1045
- loader,
1046
- format,
1047
- target: options?.target || 'esnext',
1048
- sourcemap: options?.sourcemap ?? false,
1049
- minify: options?.minify ?? false,
1050
- jsx: options?.jsx,
1051
- jsxFactory: options?.jsxFactory,
1052
- jsxFragment: options?.jsxFragment,
1053
- tsconfigRaw: options?.tsconfigRaw,
1054
- define: options?.define,
1055
- // Dynamic-import lowering — see two-pass branch above for rationale.
1056
- supported: { 'dynamic-import': false },
1057
- });
1058
- return {
1059
- code: result.code,
1060
- map: result.map || '',
1061
- warnings: result.warnings?.map(w => ({
1062
- text: w.text,
1063
- location: w.location,
1064
- })) || [],
1065
- };
1357
+ return transformWithEsbuild(this._esbuild, code, options);
1066
1358
  }
1067
1359
  /**
1068
1360
  * Bundle entry points from the VFS.
@@ -1105,13 +1397,18 @@ export class EsbuildService {
1105
1397
  warnings: result.warnings?.map(w => ({ text: w.text, location: w.location })) || [],
1106
1398
  };
1107
1399
  }
1400
+ requireVfs() {
1401
+ if (!this.vfs)
1402
+ throw new Error('EsbuildService build requires a VFS');
1403
+ return this.vfs;
1404
+ }
1108
1405
  /**
1109
1406
  * VFS resolver plugin for esbuild.
1110
- * Reads directly from the SqliteVFS (synchronous, co-located — no snapshot needed).
1407
+ * Reads through the caller's credentialed view (synchronous, no snapshot needed).
1111
1408
  * Handles: absolute paths, relative paths, bare specifiers (node_modules).
1112
1409
  */
1113
1410
  makeVfsPlugin() {
1114
- const vfs = this.vfs;
1411
+ const vfs = this.requireVfs();
1115
1412
  const EXTS = ['', '.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cjs', '.json', '.css'];
1116
1413
  const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs'];
1117
1414
  // Path helpers shared with git-commands via ./vfs-path.ts.