@nimbus-sh/core 0.6.0 → 0.8.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 (96) 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 +24 -5
  8. package/dist/_shared/tarball.d.ts.map +1 -1
  9. package/dist/_shared/tarball.js +42 -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 +32 -29
  13. package/dist/runtime/comment-strip.d.ts.map +1 -1
  14. package/dist/runtime/comment-strip.js +273 -51
  15. package/dist/runtime/cpython-runner.d.ts +1 -0
  16. package/dist/runtime/cpython-runner.d.ts.map +1 -1
  17. package/dist/runtime/cpython-runner.js +1 -1
  18. package/dist/runtime/esbuild-service.d.ts +38 -42
  19. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  20. package/dist/runtime/esbuild-service.js +500 -388
  21. package/dist/runtime/javascript-ast.d.ts.map +1 -1
  22. package/dist/runtime/javascript-ast.js +45 -7
  23. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
  24. package/dist/runtime/opentui-wasm-backend.js +18 -16
  25. package/dist/runtime/port-registry.d.ts +4 -2
  26. package/dist/runtime/port-registry.d.ts.map +1 -1
  27. package/dist/runtime/port-registry.js +60 -8
  28. package/dist/runtime/ruby-runner.d.ts +1 -0
  29. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  30. package/dist/runtime/ruby-runner.js +1 -0
  31. package/dist/shell/stdin-adapter.d.ts +11 -0
  32. package/dist/shell/stdin-adapter.d.ts.map +1 -0
  33. package/dist/shell/stdin-adapter.js +59 -0
  34. package/dist/shell/unix-commands.d.ts +2 -1
  35. package/dist/shell/unix-commands.d.ts.map +1 -1
  36. package/dist/shell/unix-commands.js +1078 -50
  37. package/dist/substrate/lifo/commands/io/dd.js +1 -1
  38. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
  39. package/dist/substrate/lifo/commands/net/curl.js +270 -54
  40. package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
  41. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
  42. package/dist/substrate/lifo/commands/system/npm.js +25 -43
  43. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
  44. package/dist/substrate/lifo/commands/text/head.js +2 -1
  45. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
  46. package/dist/substrate/lifo/commands/text/sed.js +499 -117
  47. package/dist/substrate/lifo/commands/types.d.ts +8 -4
  48. package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
  49. package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
  50. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
  51. package/dist/substrate/lifo/node-compat/zlib.js +122 -23
  52. package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
  53. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
  54. package/dist/substrate/lifo/shell/interpreter.js +79 -29
  55. package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
  56. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
  57. package/dist/substrate/lifo/shell/pipe.js +62 -18
  58. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
  59. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
  60. package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
  61. package/dist/vfs/sqlite-vfs.d.ts +19 -0
  62. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  63. package/dist/vfs/sqlite-vfs.js +106 -23
  64. package/dist/workspace/nimbus-workspace.d.ts +22 -1
  65. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  66. package/dist/workspace/nimbus-workspace.js +19 -3
  67. package/dist/workspace/supervisor-op.d.ts +107 -0
  68. package/dist/workspace/supervisor-op.d.ts.map +1 -0
  69. package/dist/workspace/supervisor-op.js +224 -0
  70. package/package.json +2 -2
  71. package/src/_shared/byte-stream.ts +5 -4
  72. package/src/_shared/real-node-imports.ts +8 -0
  73. package/src/_shared/tarball.ts +60 -5
  74. package/src/_shared/vfs-write-ledger.ts +22 -16
  75. package/src/runtime/comment-strip.ts +218 -52
  76. package/src/runtime/cpython-runner.ts +2 -1
  77. package/src/runtime/esbuild-service.ts +572 -372
  78. package/src/runtime/javascript-ast.ts +40 -9
  79. package/src/runtime/opentui-wasm-backend.ts +19 -17
  80. package/src/runtime/port-registry.ts +66 -8
  81. package/src/runtime/ruby-runner.ts +2 -0
  82. package/src/shell/stdin-adapter.ts +58 -0
  83. package/src/shell/unix-commands.ts +1054 -50
  84. package/src/substrate/lifo/commands/io/dd.ts +2 -2
  85. package/src/substrate/lifo/commands/net/curl.ts +310 -58
  86. package/src/substrate/lifo/commands/system/npm.ts +55 -38
  87. package/src/substrate/lifo/commands/text/head.ts +2 -1
  88. package/src/substrate/lifo/commands/text/sed.ts +519 -109
  89. package/src/substrate/lifo/commands/types.ts +8 -5
  90. package/src/substrate/lifo/node-compat/zlib.ts +155 -26
  91. package/src/substrate/lifo/shell/interpreter.ts +74 -27
  92. package/src/substrate/lifo/shell/pipe.ts +65 -24
  93. package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
  94. package/src/vfs/sqlite-vfs.ts +104 -23
  95. package/src/workspace/nimbus-workspace.ts +30 -3
  96. package/src/workspace/supervisor-op.ts +309 -0
@@ -17,11 +17,19 @@
17
17
  * facet once wasm module passing to dynamic workers is stable.
18
18
  */
19
19
 
20
- import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
21
- import { CRED_KERNEL } from './os-contracts.js';
20
+ import type { CredentialedVfs } from '../vfs/sqlite-vfs.js';
22
21
  import { resolvePackageEntry, resolveExports, type ResolvablePackageJson } from '../_shared/exports-resolver.js';
23
22
  import { normalizeVfsPath, stripLeadingSlashes } from '../vfs/path.js';
24
23
  import { errorText } from '../_shared/error-text.js';
24
+ import { tokenizer, tokTypes } from 'acorn';
25
+ import {
26
+ literalStringValue,
27
+ nodeList,
28
+ nodeName,
29
+ nodeProp,
30
+ parseJavaScriptModule,
31
+ } from './javascript-ast.js';
32
+ import { scanJsSource } from './comment-strip.js';
25
33
 
26
34
  /**
27
35
  * Bundler version tag. BUMP THIS whenever bundling semantics change —
@@ -55,7 +63,7 @@ import { errorText } from '../_shared/error-text.js';
55
63
  * rows hold post-rewrite text and must be re-bundled. user_module_
56
64
  * transforms is likewise re-keyed by mount base.
57
65
  */
58
- export const BUNDLER_VERSION = 'v8';
66
+ export const BUNDLER_VERSION = 'v9';
59
67
 
60
68
  // ── Shared-runtime externals ────────────────────────────────────────────
61
69
 
@@ -141,77 +149,6 @@ export function getSharedRuntimeExternals(specifier: string): string[] {
141
149
  });
142
150
  }
143
151
 
144
- /**
145
- * Detect top-level await in source. Used by `EsbuildService.transform`
146
- * to decide whether to async-IIFE-wrap CJS-target sources (see
147
- * transform()'s header comment).
148
- *
149
- * Token scan over a comment-and-string-stripped source. Tracks paren
150
- * depth AND function-body depth so a `{` inside a parameter list (a
151
- * default-value object literal — `async function f(opts = {})`) is NOT
152
- * mistaken for the function body. The earlier heuristic lacked the paren
153
- * guard: the `= {}` braces consumed the body-tracking slot, leaving the
154
- * real body untracked so an internal `await` read as top-level. That
155
- * wrongly routed such files (e.g. @bluwy/giget-core's
156
- * download-template.js, reached by create-astro) into the two-pass
157
- * ESM→require rewrite, whose assembled output still carried export/import
158
- * statements → "Cannot use import statement outside a module" at startup.
159
- *
160
- * Arrow bodies are distinguished from arrow expression bodies (the
161
- * `arrowPending` flag): only a `{` immediately following `=>` opens a
162
- * function scope. The idiomatic object-returning arrow `x => ({ ... })`
163
- * has its `{` at parenDepth 1, so it opens no body slot — otherwise the
164
- * leaked slot would be consumed by a later top-level `{ ... }` block,
165
- * making a real top-level `await` inside it read as non-TLA (false
166
- * negative) and routing genuine ESM-with-TLA into the wrong transform.
167
- *
168
- * A regex/token scan (not acorn) is kept here deliberately: pulling
169
- * acorn into the esbuild-service bundle chunk duplicates its ~15 KiB
170
- * Unicode identifier tables.
171
- */
172
- export function hasTopLevelAwait(src: string): boolean {
173
- if (!src || src.indexOf('await') === -1) return false;
174
- const stripped = stripCommentsAndStrings(src);
175
- // Tokens: keywords, arrow, and each bracket kind. `parenDepth` rises
176
- // inside `(...)`; a `{` only opens a function body when a `function`/
177
- // `=>` is pending AND we're at parenDepth 0 (past the param list).
178
- const re = /\b(await|function|class)\b|=>|\{|\}|\(|\)/g;
179
- const fnEntryDepths: number[] = [];
180
- let depth = 0;
181
- let parenDepth = 0;
182
- let pendingFn = 0;
183
- // An arrow can have either a block body (`=> { ... }`) — which opens a
184
- // function scope — or an expression body (`=> expr`), which does not.
185
- // We can only tell which by looking at the token right after `=>`: a
186
- // block body has `{` as its very next token. `arrowPending` carries
187
- // that "the next `{` (and only an immediately-following `{`) is this
188
- // arrow's body" intent, separately from `pendingFn` so the idiomatic
189
- // `x => ({ ... })` (object-returning expression body, `{` at parenDepth
190
- // 1) does not leak a body slot onto a later top-level block.
191
- let arrowPending = false;
192
- for (let m = re.exec(stripped); m !== null; m = re.exec(stripped)) {
193
- const tok = m[0];
194
- if (arrowPending && tok !== '{') arrowPending = false;
195
- if (tok === '(') { parenDepth++; }
196
- else if (tok === ')') { if (parenDepth > 0) parenDepth--; }
197
- else if (tok === '{') {
198
- depth++;
199
- if (arrowPending) { fnEntryDepths.push(depth); arrowPending = false; }
200
- else if (pendingFn > 0 && parenDepth === 0) { fnEntryDepths.push(depth); pendingFn--; }
201
- } else if (tok === '}') {
202
- if (fnEntryDepths.length > 0 && fnEntryDepths[fnEntryDepths.length - 1] === depth) fnEntryDepths.pop();
203
- depth--;
204
- } else if (tok === 'function' || tok === 'class') {
205
- pendingFn++;
206
- } else if (tok === '=>') {
207
- arrowPending = true;
208
- } else if (tok === 'await') {
209
- if (fnEntryDepths.length === 0) return true;
210
- }
211
- }
212
- return false;
213
- }
214
-
215
152
  /**
216
153
  * Cheap heuristic: does the source contain a top-level ESM `import`
217
154
  * statement? Used by `EsbuildService.transform` to detect sources that
@@ -245,10 +182,8 @@ export function hasTopLevelAwait(src: string): boolean {
245
182
  * EXCLUDED — those are expressions, legal anywhere including IIFE
246
183
  * bodies, and need no rewrite.
247
184
  *
248
- * Operates on the comment-and-string-stripped source so commented-out
249
- * imports and "import" appearing inside string literals don't trigger.
250
- * We reuse the comment/string stripper from `hasTopLevelAwait`'s pass
251
- * — see `stripCommentsAndStrings` below.
185
+ * Operates on comment-and-string-stripped source so commented-out imports
186
+ * and "import" inside string literals do not trigger.
252
187
  */
253
188
  function hasEsmImports(src: string): boolean {
254
189
  if (!src || src.indexOf('import') === -1) return false;
@@ -269,170 +204,14 @@ function hasEsmExports(src: string): boolean {
269
204
  }
270
205
 
271
206
  /**
272
- * Strip `//` and `/* * /` comments and string / template literals from
273
- * source, replacing each with a single space. The result is byte-aligned
274
- * with the input on a per-line basis (newlines are preserved), so error
275
- * line numbers from downstream parsers still align with the original.
276
- *
277
- * Shared by `hasTopLevelAwait` (which had this inline) and
278
- * `hasEsmImports`. Pure function — no caching needed for the small
279
- * inputs we see (typical .mjs entry-point: <2KiB).
207
+ * Strip `//` and `/* *\/` comments and string / template literals from
208
+ * source for the import/export classifiers one scanner shared with
209
+ * prefetch's import detection in `comment-strip.ts`. The result is
210
+ * byte-aligned with the input per line (comment and literal newlines are
211
+ * preserved), so error line numbers still match the original.
280
212
  */
281
213
  function stripCommentsAndStrings(src: string): string {
282
- let stripped = '';
283
- let i = 0;
284
- const N = src.length;
285
- // Track the last non-whitespace non-comment output character so we
286
- // can disambiguate `/` as division (after identifier/literal/`)`/`]`)
287
- // vs regex-literal opener (after operator / punctuator / keyword /
288
- // start-of-file). Pre-fix the stripper had no regex awareness, so
289
- // patterns like `var X = /^(?:'…)/` contained an unmatched `'` that
290
- // bit it as a string opener that didn't close until many lines
291
- // later — corrupting every downstream classifier (export-scanner,
292
- // hasEsmImports, hasTopLevelAwait). Real-world bite: sv-utils@0.0.3
293
- // index.mjs has dozens of these regex literals.
294
- let lastNonWsChar = '';
295
- const recordOut = (ch: string) => {
296
- if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') {
297
- lastNonWsChar = ch;
298
- }
299
- };
300
- // Identifier-suffix detection on lastNonWsChar: any of these means
301
- // `/` is division. Anything else means `/` opens a regex.
302
- // Includes ascii word chars + `)` `]` to cover `foo()/x` `a[i]/y`.
303
- const DIVISION_AFTER = /[A-Za-z0-9_$\)\]]/;
304
- while (i < N) {
305
- const c = src[i];
306
- if (c === '/' && src[i + 1] === '/') {
307
- while (i < N && src[i] !== '\n') i++;
308
- stripped += ' ';
309
- continue;
310
- }
311
- if (c === '/' && src[i + 1] === '*') {
312
- i += 2;
313
- while (i < N && !(src[i] === '*' && src[i + 1] === '/')) {
314
- // Preserve newlines so line numbers stay aligned.
315
- if (src[i] === '\n') stripped += '\n';
316
- i++;
317
- }
318
- i += 2;
319
- stripped += ' ';
320
- continue;
321
- }
322
- // Regex literal: `/` not followed by `/` or `*` (already handled
323
- // above), AND preceded by something that makes regex valid here.
324
- // The classifier looks at lastNonWsChar — for `(`, `,`, `=`, `;`,
325
- // `{`, `[`, `:`, `!`, `&`, `|`, `?`, operators, or start-of-file,
326
- // a slash is a regex opener.
327
- if (c === '/' && !DIVISION_AFTER.test(lastNonWsChar)) {
328
- stripped += ' ';
329
- i++;
330
- while (i < N) {
331
- const rc = src[i];
332
- if (rc === '\\') { i += 2; continue; }
333
- // Regex character class [...]: `/` inside it is content.
334
- if (rc === '[') {
335
- i++;
336
- while (i < N && src[i] !== ']') {
337
- if (src[i] === '\\') { i += 2; continue; }
338
- if (src[i] === '\n') { stripped += '\n'; }
339
- i++;
340
- }
341
- if (i < N) i++; // consume ']'
342
- continue;
343
- }
344
- if (rc === '/') { i++; break; }
345
- if (rc === '\n') {
346
- // Regex literals can't span newlines. If we hit one without
347
- // finding the closing `/`, this was probably NOT a regex —
348
- // bail out gracefully (rare in practice; better than getting
349
- // stuck in a wrong state).
350
- break;
351
- }
352
- i++;
353
- }
354
- // Skip regex flags (g, i, m, s, u, y, d).
355
- while (i < N && /[gimsuyd]/.test(src[i])) i++;
356
- recordOut('/');
357
- continue;
358
- }
359
- if (c === '"' || c === "'" || c === '`') {
360
- const q = c;
361
- stripped += ' ';
362
- i++;
363
- while (i < N) {
364
- const cc = src[i];
365
- if (cc === '\\') { i += 2; continue; }
366
- if (cc === q) { i++; break; }
367
- // Template interpolation: ${...} — preserve the inner
368
- // expression as raw code so the caller's depth-tracker can
369
- // see `await` etc. inside it. Strip the wrapping `${`/`}`
370
- // (those don't affect brace-depth from the caller's POV).
371
- //
372
- // CRITICAL: inside the interpolation expression, we must
373
- // recognise NESTED strings (`'}'`, `"}"`, `` `${...}` ``) so
374
- // their internal `}` characters don't decrement the depth
375
- // counter — otherwise we exit the interpolation early and
376
- // start consuming code as string content, eventually parsing
377
- // the rest of the file as one massive unclosed string. This
378
- // bit sv-utils/dist/index.mjs: multi-line backticks with
379
- // `${url.replace('}', '_')}`-style interpolations corrupted
380
- // line classification downstream. Recursive-by-loop here.
381
- if (q === '`' && cc === '$' && src[i + 1] === '{') {
382
- stripped += '${';
383
- i += 2;
384
- let depth = 1;
385
- while (i < N && depth > 0) {
386
- const ic = src[i];
387
- // Nested string inside interpolation: skip its content.
388
- if (ic === '"' || ic === "'" || ic === '`') {
389
- const iq = ic;
390
- stripped += ' ';
391
- i++;
392
- while (i < N) {
393
- const icc = src[i];
394
- if (icc === '\\') { i += 2; continue; }
395
- if (icc === iq) { i++; break; }
396
- // Nested template inside interpolation can also have its
397
- // own ${...} — recurse once more (the practical depth
398
- // observed in real code never exceeds 2; deeper nesting
399
- // falls back to brace counting which is a best-effort).
400
- if (iq === '`' && icc === '$' && src[i + 1] === '{') {
401
- stripped += '${'; i += 2;
402
- let d2 = 1;
403
- while (i < N && d2 > 0) {
404
- const i2 = src[i];
405
- if (i2 === '{') d2++;
406
- else if (i2 === '}') d2--;
407
- if (d2 > 0) stripped += i2;
408
- i++;
409
- }
410
- stripped += '}';
411
- continue;
412
- }
413
- if (icc === '\n') stripped += '\n';
414
- i++;
415
- }
416
- continue;
417
- }
418
- if (ic === '{') depth++;
419
- else if (ic === '}') depth--;
420
- if (depth > 0) stripped += ic;
421
- i++;
422
- }
423
- stripped += '}';
424
- continue;
425
- }
426
- if (cc === '\n') stripped += '\n';
427
- i++;
428
- }
429
- continue;
430
- }
431
- stripped += c;
432
- recordOut(c);
433
- i++;
434
- }
435
- return stripped;
214
+ return scanJsSource(src, 'blank');
436
215
  }
437
216
 
438
217
  /**
@@ -508,7 +287,7 @@ function convertEsmImportsToRequire(src: string): { requires: string; body: stri
508
287
  continue;
509
288
  }
510
289
  // Side-effect import: `import "m";` / `import 'm';`
511
- let m = line.match(/^[ \t]*import\s+["']([^"']+)["']\s*;?\s*$/);
290
+ let m = line.match(/^[ \t]*import\s*["']([^"']+)["']\s*;?\s*$/);
512
291
  if (m) { requires.push(`require(${JSON.stringify(m[1])});`); continue; }
513
292
  // Identifier class: JS spec allows `$` and `_` in addition to `\w`
514
293
  // (letters/digits/underscore). esbuild's ESM-pass-1 emits `process$1`
@@ -518,14 +297,14 @@ function convertEsmImportsToRequire(src: string): { requires: string; body: stri
518
297
  // statement survived into the async-IIFE wrap → SyntaxError
519
298
  // "import statement outside module" at facet pre-compile.
520
299
  // Default + namespace: `import x, * as ns from "m";`
521
- m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\*\s+as\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
300
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
522
301
  if (m) {
523
302
  const def = m[1], ns = m[2], mod = m[3];
524
303
  requires.push(`const ${ns} = require(${JSON.stringify(mod)}); const ${def} = ${ns}.__esModule ? ${ns}.default : ${ns};`);
525
304
  continue;
526
305
  }
527
306
  // Default + named: `import x, { a, b as c } from "m";`
528
- m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\{([^}]+)\}\s+from\s+["']([^"']+)["']\s*;?\s*$/);
307
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\{([^}]+)\}\s+from\s*["']([^"']+)["']\s*;?\s*$/);
529
308
  if (m) {
530
309
  const def = m[1], bindings = m[2], mod = m[3];
531
310
  const tmp = `_nimbus_m_${counter++}`;
@@ -538,10 +317,10 @@ function convertEsmImportsToRequire(src: string): { requires: string; body: stri
538
317
  continue;
539
318
  }
540
319
  // Namespace: `import * as ns from "m";`
541
- m = line.match(/^[ \t]*import\s+\*\s+as\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
320
+ m = line.match(/^[ \t]*import\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
542
321
  if (m) { requires.push(`const ${m[1]} = require(${JSON.stringify(m[2])});`); continue; }
543
322
  // Named only: `import { a, b as c } from "m";`
544
- m = line.match(/^[ \t]*import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']\s*;?\s*$/);
323
+ m = line.match(/^[ \t]*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']\s*;?\s*$/);
545
324
  if (m) {
546
325
  const bindings = m[1], mod = m[2];
547
326
  const named = bindings.split(',').map((b) => {
@@ -553,7 +332,7 @@ function convertEsmImportsToRequire(src: string): { requires: string; body: stri
553
332
  continue;
554
333
  }
555
334
  // Default only: `import x from "m";`
556
- m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
335
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
557
336
  if (m) {
558
337
  const def = m[1], mod = m[2];
559
338
  requires.push(`const ${def} = (() => { const _m = require(${JSON.stringify(mod)}); return _m && _m.__esModule ? _m.default : _m; })();`);
@@ -742,57 +521,447 @@ function convertEsmImportsToRequire(src: string): { requires: string; body: stri
742
521
  return { requires: requires.join('\n'), body: out.join('\n') };
743
522
  }
744
523
 
524
+ interface ModuleDeclarationRange {
525
+ start: number;
526
+ end: number;
527
+ kind: 'import' | 'export';
528
+ }
529
+
530
+ function topLevelModuleDeclarationRanges(source: string): ModuleDeclarationRange[] | null {
531
+ try {
532
+ const tokens = tokenizer(source, {
533
+ ecmaVersion: 'latest',
534
+ sourceType: 'module',
535
+ allowHashBang: true,
536
+ });
537
+ const ranges: ModuleDeclarationRange[] = [];
538
+ let active: Omit<ModuleDeclarationRange, 'end'> | null = null;
539
+ let braces = 0;
540
+ let parens = 0;
541
+ let brackets = 0;
542
+
543
+ const updateDepth = (type: typeof tokTypes.eof): void => {
544
+ if (type === tokTypes.braceL || type === tokTypes.dollarBraceL) braces++;
545
+ else if (type === tokTypes.braceR) braces = Math.max(0, braces - 1);
546
+ else if (type === tokTypes.parenL) parens++;
547
+ else if (type === tokTypes.parenR) parens = Math.max(0, parens - 1);
548
+ else if (type === tokTypes.bracketL) brackets++;
549
+ else if (type === tokTypes.bracketR) brackets = Math.max(0, brackets - 1);
550
+ };
551
+
552
+ while (true) {
553
+ const token = tokens.getToken();
554
+ const type = token.type;
555
+ if (type === tokTypes.eof) return active ? null : ranges;
556
+
557
+ if (active) {
558
+ updateDepth(type);
559
+ if (type === tokTypes.semi && braces === 0 && parens === 0 && brackets === 0) {
560
+ ranges.push({ ...active, end: token.end });
561
+ active = null;
562
+ }
563
+ continue;
564
+ }
565
+
566
+ const topLevel = braces === 0 && parens === 0 && brackets === 0;
567
+ if (topLevel && type === tokTypes._import) {
568
+ const next = tokens.getToken();
569
+ if (next.type !== tokTypes.parenL && next.type !== tokTypes.dot) {
570
+ active = { start: token.start, kind: 'import' };
571
+ }
572
+ updateDepth(next.type);
573
+ continue;
574
+ }
575
+ if (topLevel && type === tokTypes._export) {
576
+ active = { start: token.start, kind: 'export' };
577
+ continue;
578
+ }
579
+ updateDepth(type);
580
+ }
581
+ } catch {
582
+ return null;
583
+ }
584
+ }
585
+
586
+
587
+ function hasUnscopedAwait(source: string): boolean {
588
+ try {
589
+ const tokens = tokenizer(source, {
590
+ ecmaVersion: 'latest',
591
+ sourceType: 'module',
592
+ allowHashBang: true,
593
+ });
594
+ const functionBraces: boolean[] = [];
595
+ const functionParenDepths: number[] = [];
596
+ const methodParenCandidates: boolean[] = [];
597
+ const arrowExpressions: Array<{ parens: number; braces: number; brackets: number }> = [];
598
+ let bracketDepth = 0;
599
+ let pendingMethodBody = false;
600
+ let pendingArrowBody = false;
601
+ let pendingFunctionKeyword = false;
602
+ let previous = tokTypes.eof;
603
+ let previousEnd = 0;
604
+
605
+ while (true) {
606
+ const token = tokens.getToken();
607
+ const type = token.type;
608
+ if (type === tokTypes.eof) return false;
609
+
610
+ if (pendingMethodBody && type !== tokTypes.braceL) pendingMethodBody = false;
611
+ if (pendingArrowBody && type !== tokTypes.braceL) {
612
+ arrowExpressions.push({
613
+ parens: methodParenCandidates.length,
614
+ braces: functionBraces.length,
615
+ brackets: bracketDepth,
616
+ });
617
+ pendingArrowBody = false;
618
+ }
619
+
620
+ if (pendingFunctionKeyword) {
621
+ if (
622
+ type === tokTypes.colon || type === tokTypes.comma || type === tokTypes.braceR
623
+ || type === tokTypes.parenR || type === tokTypes.bracketR || type === tokTypes.eq
624
+ ) functionParenDepths.pop();
625
+ pendingFunctionKeyword = false;
626
+ }
627
+
628
+ if (source.slice(previousEnd, token.start).includes('\n')) {
629
+ while (arrowExpressions.length > 0) {
630
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
631
+ if (
632
+ methodParenCandidates.length !== arrow.parens
633
+ || functionBraces.length !== arrow.braces
634
+ || bracketDepth !== arrow.brackets
635
+ ) break;
636
+ arrowExpressions.pop();
637
+ }
638
+ }
639
+
640
+ while (arrowExpressions.length > 0) {
641
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
642
+ const delimited = (type === tokTypes.semi || type === tokTypes.comma)
643
+ && methodParenCandidates.length === arrow.parens
644
+ && functionBraces.length === arrow.braces
645
+ && bracketDepth === arrow.brackets;
646
+ const closed = (type === tokTypes.parenR && methodParenCandidates.length === arrow.parens)
647
+ || (type === tokTypes.bracketR && bracketDepth === arrow.brackets)
648
+ || (type === tokTypes.braceR && functionBraces.length === arrow.braces);
649
+ if (!delimited && !closed) break;
650
+ arrowExpressions.pop();
651
+ }
652
+
653
+ if (
654
+ type === tokTypes.name
655
+ && source.slice(token.start, token.end) === 'await'
656
+ && !functionBraces.includes(true)
657
+ && arrowExpressions.length === 0
658
+ ) return true;
659
+
660
+ if (type === tokTypes._function || type === tokTypes._class) {
661
+ if (previous !== tokTypes.dot && previous !== tokTypes.questionDot) {
662
+ functionParenDepths.push(methodParenCandidates.length);
663
+ pendingFunctionKeyword = true;
664
+ }
665
+ } else if (type === tokTypes.arrow) {
666
+ pendingArrowBody = true;
667
+ } else if (type === tokTypes.parenL) {
668
+ methodParenCandidates.push(
669
+ functionBraces.length > 0
670
+ && (previous === tokTypes.name || previous === tokTypes.string
671
+ || previous === tokTypes.num || previous === tokTypes.bracketR),
672
+ );
673
+ } else if (type === tokTypes.parenR) {
674
+ pendingMethodBody = methodParenCandidates.pop() === true;
675
+ } else if (type === tokTypes.bracketL) {
676
+ bracketDepth++;
677
+ } else if (type === tokTypes.bracketR) {
678
+ bracketDepth = Math.max(0, bracketDepth - 1);
679
+ } else if (type === tokTypes.dollarBraceL) {
680
+ functionBraces.push(false);
681
+ } else if (type === tokTypes.braceL) {
682
+ const functionBody = pendingArrowBody
683
+ || pendingMethodBody
684
+ || functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length;
685
+ if (functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length) {
686
+ functionParenDepths.pop();
687
+ }
688
+ functionBraces.push(functionBody);
689
+ pendingArrowBody = false;
690
+ pendingMethodBody = false;
691
+ } else if (type === tokTypes.braceR) {
692
+ functionBraces.pop();
693
+ }
694
+ previousEnd = token.end;
695
+ previous = type;
696
+ }
697
+ } catch {
698
+ return true;
699
+ }
700
+ }
701
+
702
+ interface ConvertedModuleDeclarations {
703
+ imports: string;
704
+ exports: string;
705
+ }
706
+
707
+ function convertBundledModuleDeclarations(snippets: string[]): ConvertedModuleDeclarations | null {
708
+ const imports: string[] = [];
709
+ const exports: string[] = [];
710
+ let importIndex = 0;
711
+ let markedEsm = false;
712
+
713
+ for (const snippet of snippets) {
714
+ const bindingList = snippet.match(/^[ \t]*export\s*\{([\s\S]*)\}\s*;?\s*$/);
715
+ if (bindingList && !/\}\s*from\b/.test(snippet)) {
716
+ if (!markedEsm) {
717
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
718
+ markedEsm = true;
719
+ }
720
+ for (const binding of bindingList[1].split(',')) {
721
+ const match = binding.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
722
+ if (!match) return null;
723
+ const local = match[1];
724
+ const exported = match[2] || local;
725
+ exports.push(
726
+ `Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`,
727
+ );
728
+ }
729
+ continue;
730
+ }
731
+ let ast: ReturnType<typeof parseJavaScriptModule>;
732
+ try {
733
+ ast = parseJavaScriptModule(snippet);
734
+ } catch {
735
+ return null;
736
+ }
737
+ const body = nodeList(ast, 'body');
738
+ if (body.length !== 1) return null;
739
+ const declaration = body[0];
740
+
741
+ if (declaration.type === 'ImportDeclaration') {
742
+ const source = literalStringValue(nodeProp(declaration, 'source'));
743
+ if (!source) return null;
744
+ const specifiers = nodeList(declaration, 'specifiers');
745
+ if (specifiers.length === 0) {
746
+ imports.push(`module.require(${JSON.stringify(source)});`);
747
+ continue;
748
+ }
749
+ const moduleName = `__nimbus_import_${importIndex++}`;
750
+ imports.push(`const ${moduleName} = module.require(${JSON.stringify(source)});`);
751
+ for (const specifier of specifiers) {
752
+ const local = nodeName(nodeProp(specifier, 'local'));
753
+ if (!local) return null;
754
+ if (specifier.type === 'ImportDefaultSpecifier') {
755
+ imports.push(`const ${local} = ${moduleName} && ${moduleName}.__esModule ? ${moduleName}.default : ${moduleName};`);
756
+ } else if (specifier.type === 'ImportNamespaceSpecifier') {
757
+ imports.push(`const ${local} = ${moduleName};`);
758
+ } else if (specifier.type === 'ImportSpecifier') {
759
+ const imported = nodeName(nodeProp(specifier, 'imported'));
760
+ if (!imported) return null;
761
+ imports.push(`const ${local} = ${moduleName}[${JSON.stringify(imported)}];`);
762
+ } else {
763
+ return null;
764
+ }
765
+ }
766
+ continue;
767
+ }
768
+
769
+ if (declaration.type === 'ExportNamedDeclaration') {
770
+ if (nodeProp(declaration, 'source') || nodeProp(declaration, 'declaration')) return null;
771
+ if (!markedEsm) {
772
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
773
+ markedEsm = true;
774
+ }
775
+ for (const specifier of nodeList(declaration, 'specifiers')) {
776
+ const local = nodeName(nodeProp(specifier, 'local'));
777
+ const exported = nodeName(nodeProp(specifier, 'exported'));
778
+ if (!local || !exported) return null;
779
+ exports.push(
780
+ `Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`,
781
+ );
782
+ }
783
+ continue;
784
+ }
785
+
786
+ if (declaration.type === 'ExportDefaultDeclaration') {
787
+ const value = nodeProp(declaration, 'declaration');
788
+ if (!value || typeof value.start !== 'number' || typeof value.end !== 'number') return null;
789
+ if (value.type === 'FunctionDeclaration' || value.type === 'ClassDeclaration') return null;
790
+ if (!markedEsm) {
791
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
792
+ markedEsm = true;
793
+ }
794
+ exports.push(
795
+ `Object.defineProperty(module.exports, "default", { enumerable: true, value: (${snippet.slice(value.start, value.end)}) });`,
796
+ );
797
+ continue;
798
+ }
799
+
800
+ return null;
801
+ }
802
+
803
+ return { imports: imports.join('\n'), exports: exports.join('\n') };
804
+ }
805
+ interface SourceEdit {
806
+ start: number;
807
+ end: number;
808
+ text: string;
809
+ }
810
+
811
+ function importMetaEdits(source: string, absoluteUrl: string): SourceEdit[] | null {
812
+ const edits: SourceEdit[] = [];
813
+ try {
814
+ const tokens = tokenizer(source, {
815
+ ecmaVersion: 'latest',
816
+ sourceType: 'module',
817
+ allowHashBang: true,
818
+ });
819
+ while (true) {
820
+ const start = tokens.getToken();
821
+ if (start.type === tokTypes.eof) return edits;
822
+ if (start.type !== tokTypes._import) continue;
823
+ const dot1 = tokens.getToken();
824
+ if (dot1.type !== tokTypes.dot) continue;
825
+ const meta = tokens.getToken();
826
+ if (meta.type !== tokTypes.name || source.slice(meta.start, meta.end) !== 'meta') return null;
827
+ const dot2 = tokens.getToken();
828
+ if (dot2.type !== tokTypes.dot) return null;
829
+ const property = tokens.getToken();
830
+ if (property.type !== tokTypes.name) return null;
831
+ const propertyName = source.slice(property.start, property.end);
832
+ if (propertyName === 'url') {
833
+ edits.push({ start: start.start, end: property.end, text: JSON.stringify(absoluteUrl) });
834
+ } else if (propertyName === 'resolve') {
835
+ edits.push({
836
+ start: start.start,
837
+ end: property.end,
838
+ text: `(specifier => globalThis.__nimbusImportMetaResolve(specifier, ${JSON.stringify(absoluteUrl)}))`,
839
+ });
840
+ } else {
841
+ return null;
842
+ }
843
+ }
844
+ } catch {
845
+ return null;
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Converts bundler-emitted ESM without constructing an AST or loading
851
+ * esbuild-wasm. Returns null for module declarations that are not the compact,
852
+ * semicolon-terminated shapes emitted by current JS bundlers.
853
+ */
854
+ export function rewriteBundledEsmToCjs(
855
+ source: string,
856
+ absoluteUrl: string,
857
+ ): TransformResult | null {
858
+ if (hasUnscopedAwait(source)) return null;
859
+ const declarations = topLevelModuleDeclarationRanges(source);
860
+ if (!declarations || declarations.length === 0) return null;
861
+ const declarationSnippets = declarations.map(({ start, end }) => source.slice(start, end));
862
+ for (let i = 0; i < declarations.length; i++) {
863
+ if (/^[ \t]*export\s+default\b/.test(declarationSnippets[i])
864
+ && source.slice(declarations[i].end).trim() !== '') return null;
865
+ }
866
+ const converted = convertBundledModuleDeclarations(declarationSnippets);
867
+ if (!converted) return null;
868
+ const metaEdits = importMetaEdits(source, absoluteUrl);
869
+ if (!metaEdits) return null;
870
+
871
+ const edits: SourceEdit[] = [
872
+ ...declarations.map(({ start, end }) => ({ start, end, text: '' })),
873
+ ...metaEdits.filter((edit) =>
874
+ !declarations.some(({ start, end }) => edit.start >= start && edit.end <= end)
875
+ ),
876
+ ].sort((a, b) => a.start - b.start);
877
+
878
+ const bodyParts: string[] = [];
879
+ let cursor = 0;
880
+ for (const edit of edits) {
881
+ if (edit.start < cursor) return null;
882
+ bodyParts.push(source.slice(cursor, edit.start), edit.text);
883
+ cursor = edit.end;
884
+ }
885
+ bodyParts.push(source.slice(cursor));
886
+ const body = bodyParts.join('');
887
+
888
+ return {
889
+ code: converted.imports + '\n' + body + '\n' + converted.exports,
890
+ map: '',
891
+ warnings: [],
892
+ };
893
+ }
894
+
745
895
  // ── esbuild-wasm imports ────────────────────────────────────────────────
746
896
  //
747
- // We must NOT eagerly import('esbuild-wasm') at module evaluation.
748
- // esbuild-wasm's CJS `lib/main.js` runs `createRequire(import.meta.url)('fs')`
749
- // at module-init, which workerd rejects with:
750
- // "Dynamic require of \"fs\" is not supported"
751
- // on dynamic-worker instances (nodejs_compat only satisfies static
752
- // `import 'node:fs'`, NOT runtime __require2-style CJS requires).
897
+ // `esbuild-wasm` ships no `exports` map. Its `main` is the Node CJS build
898
+ // `lib/main.js`, and only the LEGACY `browser` field points at a build that
899
+ // can run here. Every resolver that ignores that legacy field resolves the
900
+ // bare specifier to `lib/main.js`: Node, Bun, and — the case that matters in
901
+ // production — the worker environment of a host bundler, because Vite leaves
902
+ // `browser` out of `resolve.mainFields` for every non-client environment.
903
+ //
904
+ // `lib/main.js` cannot serve this file, for two independent reasons:
905
+ // 1. It runs `createRequire(import.meta.url)('fs')` at module init, which
906
+ // workerd rejects with `Dynamic require of "fs" is not supported`
907
+ // (`nodejs_compat` satisfies static `import 'node:fs'`, not runtime
908
+ // CJS requires).
909
+ // 2. Its `initialize()` refuses the `wasmModule` option outright:
910
+ // `The "wasmModule" option only works in the browser`.
911
+ //
912
+ // `wasmModule` is the only initialization form available to us. There is no
913
+ // filesystem for the Node build to read, and `wasmURL` would fetch a third
914
+ // party mid-request, which the 100% edge contract forbids. So the entrypoint
915
+ // is named rather than inferred: `esm/browser.js` is the one build whose
916
+ // `initialize()` accepts a precompiled module. It is also the exact file
917
+ // `packages/worker/scripts/bundle-esbuild-wasm.mjs` stages for facets, and
918
+ // that script gates it against `createRequire` / `require('fs')` on every
919
+ // install — so both consumers of esbuild-wasm load the same vetted build.
753
920
  //
754
- // Types are imported type-only so TypeScript sees `esbuild.Plugin` /
755
- // `esbuild.Loader` without emitting a runtime require. The actual
756
- // namespace is loaded lazily by `loadEsbuild()` below, triggered on
757
- // the first transform/build/initialize call. If no caller ever runs
758
- // esbuild (e.g. an inner Nimbus that only serves its shell), the
759
- // module never loads and `__require2('fs')` is never hit.
921
+ // The load stays lazy: a session that never bundles never evaluates it.
760
922
  //
761
- // The .wasm import is a compile-time asset binding (wrangler resolves
762
- // it to a WebAssembly.Module) it does NOT execute esbuild-wasm's
763
- // main.js, so it's safe to keep at the top level.
764
- import type * as esbuild from 'esbuild-wasm';
923
+ // The .wasm import is a compile-time asset binding (the host bundler resolves
924
+ // it to a WebAssembly.Module) and executes no esbuild-wasm JS, so it is safe
925
+ // at the top level.
926
+ import type * as esbuild from 'esbuild-wasm/esm/browser.js';
765
927
  import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm';
766
928
 
767
929
  /**
768
930
  * Cached reference to the esbuild namespace. Populated on first
769
931
  * `loadEsbuild()` call; nullable until then so module-load code paths
770
932
  * that never touch bundling can complete without ever evaluating
771
- * `esbuild-wasm/lib/main.js`.
933
+ * esbuild-wasm's JS at all.
772
934
  */
773
935
  let _esbuildMod: typeof esbuild | null = null;
774
936
  let _esbuildLoadPromise: Promise<typeof esbuild> | null = null;
775
937
 
776
938
  /**
777
- * Lazily load the esbuild-wasm namespace. Safe to call many times;
778
- * concurrent callers share a single in-flight Promise. Throws if the
779
- * CJS main module itself can't run (i.e. the runtime doesn't support
780
- * the require('fs') pattern esbuild-wasm uses) — callers should catch
781
- * and surface a helpful error rather than crash the Worker.
939
+ * Load the esbuild-wasm namespace. Safe to call many times; concurrent
940
+ * callers share a single in-flight Promise, and a rejection clears the
941
+ * cache so a later call can retry.
942
+ *
943
+ * Exported so `tests/unit/esbuild-wasm-entrypoint.mjs` can drive the real
944
+ * specifier under a Node-style resolver. A test that restated the specifier
945
+ * would grade its own copy of it, and this defect reached production
946
+ * precisely because nothing graded the resolution.
947
+ *
948
+ * The specifier stays a literal: a computed one would defeat the host
949
+ * bundler's static analysis and leave the module out of the deployed worker.
782
950
  */
783
- async function loadEsbuild(): Promise<typeof esbuild> {
951
+ export async function loadEsbuild(): Promise<typeof esbuild> {
784
952
  if (_esbuildMod) return _esbuildMod;
785
953
  if (_esbuildLoadPromise) return _esbuildLoadPromise;
786
954
  _esbuildLoadPromise = (async () => {
787
- const mod = await import('esbuild-wasm');
955
+ // Deliberately dynamic: a static import would evaluate esbuild-wasm in
956
+ // every session, including the ones that only serve a shell and never
957
+ // bundle. The specifier is still a literal so the host bundler sees it.
958
+ const mod = await import('esbuild-wasm/esm/browser.js');
788
959
  _esbuildMod = mod as unknown as typeof esbuild;
789
960
  return _esbuildMod;
790
961
  })();
791
962
  try {
792
963
  return await _esbuildLoadPromise;
793
964
  } catch (e) {
794
- // Reset so a future call can retry (e.g., after the caller has done
795
- // environment setup we didn't anticipate).
796
965
  _esbuildLoadPromise = null;
797
966
  throw e;
798
967
  }
@@ -800,6 +969,19 @@ async function loadEsbuild(): Promise<typeof esbuild> {
800
969
 
801
970
  // ── Types ───────────────────────────────────────────────────────────────
802
971
 
972
+ export interface EsbuildTransformOptions {
973
+ loader?: 'ts' | 'tsx' | 'jsx' | 'js' | 'css' | 'json';
974
+ format?: 'esm' | 'cjs' | 'iife';
975
+ target?: string;
976
+ sourcemap?: boolean | 'inline' | 'external';
977
+ minify?: boolean;
978
+ jsx?: 'transform' | 'preserve' | 'automatic';
979
+ jsxFactory?: string;
980
+ jsxFragment?: string;
981
+ tsconfigRaw?: string;
982
+ define?: Record<string, string>;
983
+ }
984
+
803
985
  export interface TransformResult {
804
986
  code: string;
805
987
  map: string;
@@ -817,17 +999,122 @@ export interface BuildResult {
817
999
  warnings: { text: string; location?: esbuild.Location | null }[];
818
1000
  }
819
1001
 
820
- // ── EsbuildService ──────────────────────────────────────────────────────
1002
+ type EsbuildTransformApi = Pick<typeof esbuild, 'transform'>;
1003
+
1004
+ async function transformWithEsbuild(
1005
+ esbuildApi: EsbuildTransformApi,
1006
+ source: string,
1007
+ options?: EsbuildTransformOptions,
1008
+ ): Promise<TransformResult> {
1009
+ let code = source;
1010
+ const format = options?.format || 'esm';
1011
+ const loader = options?.loader || 'ts';
821
1012
 
1013
+ if (format === 'cjs') {
1014
+ try {
1015
+ const direct = await esbuildApi.transform(code, {
1016
+ loader,
1017
+ format,
1018
+ target: options?.target || 'esnext',
1019
+ sourcemap: options?.sourcemap ?? false,
1020
+ minify: options?.minify ?? false,
1021
+ jsx: options?.jsx,
1022
+ jsxFactory: options?.jsxFactory,
1023
+ jsxFragment: options?.jsxFragment,
1024
+ tsconfigRaw: options?.tsconfigRaw,
1025
+ define: options?.define,
1026
+ supported: { 'dynamic-import': false },
1027
+ });
1028
+ return {
1029
+ code: direct.code,
1030
+ map: direct.map || '',
1031
+ warnings: direct.warnings?.map((warning) => ({
1032
+ text: warning.text,
1033
+ location: warning.location,
1034
+ })) || [],
1035
+ };
1036
+ } catch (error) {
1037
+ const message = error instanceof Error ? error.message : String(error);
1038
+ if (!/top-level await.*not supported.*cjs/i.test(message)) throw error;
1039
+ }
1040
+
1041
+ if (hasEsmImports(code) || hasEsmExports(code)) {
1042
+ const pass1 = await esbuildApi.transform(code, {
1043
+ loader,
1044
+ format: 'esm',
1045
+ target: options?.target || 'esnext',
1046
+ sourcemap: false,
1047
+ minify: false,
1048
+ jsx: options?.jsx,
1049
+ jsxFactory: options?.jsxFactory,
1050
+ jsxFragment: options?.jsxFragment,
1051
+ tsconfigRaw: options?.tsconfigRaw,
1052
+ define: options?.define,
1053
+ supported: { 'dynamic-import': false },
1054
+ });
1055
+ const { requires, body } = convertEsmImportsToRequire(pass1.code);
1056
+ return {
1057
+ code: requires + '\nreturn (async () => {\n' + body + '\n})();\n',
1058
+ map: '',
1059
+ warnings: pass1.warnings?.map((warning) => ({
1060
+ text: warning.text,
1061
+ location: warning.location,
1062
+ })) || [],
1063
+ };
1064
+ }
1065
+
1066
+ code = 'return (async () => {\n' + code + '\n})();\n';
1067
+ }
1068
+
1069
+ const result = await esbuildApi.transform(code, {
1070
+ loader,
1071
+ format,
1072
+ target: options?.target || 'esnext',
1073
+ sourcemap: options?.sourcemap ?? false,
1074
+ minify: options?.minify ?? false,
1075
+ jsx: options?.jsx,
1076
+ jsxFactory: options?.jsxFactory,
1077
+ jsxFragment: options?.jsxFragment,
1078
+ tsconfigRaw: options?.tsconfigRaw,
1079
+ define: options?.define,
1080
+ supported: { 'dynamic-import': false },
1081
+ });
1082
+
1083
+ return {
1084
+ code: result.code,
1085
+ map: result.map || '',
1086
+ warnings: result.warnings?.map((warning) => ({
1087
+ text: warning.text,
1088
+ location: warning.location,
1089
+ })) || [],
1090
+ };
1091
+ }
1092
+
1093
+ /** Source needed by the slim Worker Loader transform isolate. */
1094
+ export function generateEsbuildTransformRuntimeSource(): string {
1095
+ return [
1096
+ // scanJsSource is self-contained — its constants live in the body —
1097
+ // so this serialized copy carries the whole scanner.
1098
+ scanJsSource.toString(),
1099
+ stripCommentsAndStrings.toString(),
1100
+ hasEsmImports.toString(),
1101
+ hasEsmExports.toString(),
1102
+ convertEsmImportsToRequire.toString(),
1103
+ transformWithEsbuild.toString(),
1104
+ ].join('\n');
1105
+ }
1106
+
1107
+ // ── EsbuildService ──────────────────────────────────────────────────────
822
1108
  export class EsbuildService {
823
- private vfs: CredentialedVfs;
1109
+ private vfs: CredentialedVfs | null;
824
1110
  private initialized = false;
825
1111
  private initPromise: Promise<void> | null = null;
826
1112
  /** Resolved esbuild namespace — populated by ensureInit() after loadEsbuild(). */
827
1113
  private _esbuild: typeof esbuild | null = null;
828
1114
 
829
- constructor(vfs: SqliteVFS) {
830
- this.vfs = vfs.as(CRED_KERNEL);
1115
+ /** Build reads use only the caller-supplied view; omit it for transform-only use. */
1116
+ constructor(vfs?: CredentialedVfs) {
1117
+ this.vfs = vfs ?? null;
831
1118
  }
832
1119
 
833
1120
  /**
@@ -973,102 +1260,10 @@ export class EsbuildService {
973
1260
  */
974
1261
  async transform(
975
1262
  code: string,
976
- options?: {
977
- loader?: 'ts' | 'tsx' | 'jsx' | 'js' | 'css' | 'json';
978
- format?: 'esm' | 'cjs' | 'iife';
979
- target?: string;
980
- sourcemap?: boolean | 'inline' | 'external';
981
- minify?: boolean;
982
- jsx?: 'transform' | 'preserve' | 'automatic';
983
- jsxFactory?: string;
984
- jsxFragment?: string;
985
- tsconfigRaw?: string;
986
- define?: Record<string, string>;
987
- },
1263
+ options?: EsbuildTransformOptions,
988
1264
  ): Promise<TransformResult> {
989
1265
  await this.ensureInit();
990
-
991
- const format = options?.format || 'esm';
992
- const loader = options?.loader || 'ts';
993
-
994
- // ── CJS + TLA + ESM module syntax: two-pass with module rewrite ──
995
- // This precedes the simple TLA-only IIFE wrap. We test ESM module
996
- // syntax first because the IIFE wrap is incompatible with import
997
- // and export statements; if both conditions hold we MUST go through
998
- // the rewrite path.
999
- if (format === 'cjs' && hasTopLevelAwait(code) && (hasEsmImports(code) || hasEsmExports(code))) {
1000
- // Pass 1: emit ESM so esbuild accepts TLA + imports without complaint.
1001
- // We use the same loader/target so TS/JSX is handled here too.
1002
- const pass1 = await this._esbuild!.transform(code, {
1003
- loader,
1004
- format: 'esm',
1005
- target: options?.target || 'esnext',
1006
- sourcemap: false, // pass-1 source map is discarded; pass-2 doesn't run esbuild
1007
- minify: false, // minify only on the final output if requested
1008
- jsx: options?.jsx,
1009
- jsxFactory: options?.jsxFactory,
1010
- jsxFragment: options?.jsxFragment,
1011
- tsconfigRaw: options?.tsconfigRaw,
1012
- define: options?.define,
1013
- // Dynamic-import lowering: rewrites `import(x)` to
1014
- // `Promise.resolve().then(() => __toESM(require(x)))` so the
1015
- // call routes through Nimbus's scopedRequire → VFS lookup
1016
- // instead of workerd's worker-module-map resolver (which only
1017
- // knows {'runner.js': workerCode}). Without this, every user
1018
- // dynamic import() rejects with "No such module ..." — and if
1019
- // user code has no .catch() (e.g. create-astro.mjs's
1020
- // `import('./dist/index.js').then(({main}) => main())`), the
1021
- // rejection is unhandled and the facet exits silently
1022
- supported: { 'dynamic-import': false },
1023
- });
1024
- const { requires, body } = convertEsmImportsToRequire(pass1.code);
1025
- const assembled =
1026
- requires + '\n' +
1027
- 'return (async () => {\n' +
1028
- body +
1029
- '\n})();\n';
1030
- return {
1031
- code: assembled,
1032
- map: '',
1033
- warnings: pass1.warnings?.map((w) => ({
1034
- text: w.text,
1035
- location: w.location,
1036
- })) || [],
1037
- };
1038
- }
1039
-
1040
- // ── CJS + TLA (no ESM imports): single-pass IIFE wrap (P2 fix) ──
1041
- let sourceToTransform = code;
1042
- if (format === 'cjs' && hasTopLevelAwait(code)) {
1043
- sourceToTransform =
1044
- 'return (async () => {\n' +
1045
- code +
1046
- '\n})();\n';
1047
- }
1048
-
1049
- const result = await this._esbuild!.transform(sourceToTransform, {
1050
- loader,
1051
- format,
1052
- target: options?.target || 'esnext',
1053
- sourcemap: options?.sourcemap ?? false,
1054
- minify: options?.minify ?? false,
1055
- jsx: options?.jsx,
1056
- jsxFactory: options?.jsxFactory,
1057
- jsxFragment: options?.jsxFragment,
1058
- tsconfigRaw: options?.tsconfigRaw,
1059
- define: options?.define,
1060
- // Dynamic-import lowering — see two-pass branch above for rationale.
1061
- supported: { 'dynamic-import': false },
1062
- });
1063
-
1064
- return {
1065
- code: result.code,
1066
- map: result.map || '',
1067
- warnings: result.warnings?.map(w => ({
1068
- text: w.text,
1069
- location: w.location,
1070
- })) || [],
1071
- };
1266
+ return transformWithEsbuild(this._esbuild!, code, options);
1072
1267
  }
1073
1268
 
1074
1269
  /**
@@ -1134,13 +1329,18 @@ export class EsbuildService {
1134
1329
  };
1135
1330
  }
1136
1331
 
1332
+ private requireVfs(): CredentialedVfs {
1333
+ if (!this.vfs) throw new Error('EsbuildService build requires a VFS');
1334
+ return this.vfs;
1335
+ }
1336
+
1137
1337
  /**
1138
1338
  * VFS resolver plugin for esbuild.
1139
- * Reads directly from the SqliteVFS (synchronous, co-located — no snapshot needed).
1339
+ * Reads through the caller's credentialed view (synchronous, no snapshot needed).
1140
1340
  * Handles: absolute paths, relative paths, bare specifiers (node_modules).
1141
1341
  */
1142
1342
  private makeVfsPlugin(): esbuild.Plugin {
1143
- const vfs = this.vfs;
1343
+ const vfs = this.requireVfs();
1144
1344
  const EXTS = ['', '.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cjs', '.json', '.css'];
1145
1345
  const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs'];
1146
1346