@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
@@ -16,10 +16,12 @@
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';
24
+ import { scanJsSource } from './comment-strip.js';
23
25
  /**
24
26
  * Bundler version tag. BUMP THIS whenever bundling semantics change —
25
27
  * the esbuild plugin's resolver logic, the shared-externals rules, the
@@ -52,7 +54,7 @@ import { errorText } from '../_shared/error-text.js';
52
54
  * rows hold post-rewrite text and must be re-bundled. user_module_
53
55
  * transforms is likewise re-keyed by mount base.
54
56
  */
55
- export const BUNDLER_VERSION = 'v8';
57
+ export const BUNDLER_VERSION = 'v9';
56
58
  // ── Shared-runtime externals ────────────────────────────────────────────
57
59
  /**
58
60
  * Returns the list of specifiers that must be marked `external` when bundling
@@ -139,95 +141,6 @@ export function getSharedRuntimeExternals(specifier) {
139
141
  return true;
140
142
  });
141
143
  }
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
144
  /**
232
145
  * Cheap heuristic: does the source contain a top-level ESM `import`
233
146
  * statement? Used by `EsbuildService.transform` to detect sources that
@@ -261,10 +174,8 @@ export function hasTopLevelAwait(src) {
261
174
  * EXCLUDED — those are expressions, legal anywhere including IIFE
262
175
  * bodies, and need no rewrite.
263
176
  *
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.
177
+ * Operates on comment-and-string-stripped source so commented-out imports
178
+ * and "import" inside string literals do not trigger.
268
179
  */
269
180
  function hasEsmImports(src) {
270
181
  if (!src || src.indexOf('import') === -1)
@@ -285,206 +196,14 @@ function hasEsmExports(src) {
285
196
  return /^[ \t]*export\b/m.test(stripped);
286
197
  }
287
198
  /**
288
- * Strip `//` and `/* * /` comments and string / template literals from
289
- * source, replacing each with a single space. The result is byte-aligned
290
- * with the input on a per-line basis (newlines are preserved), so error
291
- * 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).
199
+ * Strip `//` and `/* *\/` comments and string / template literals from
200
+ * source for the import/export classifiers one scanner shared with
201
+ * prefetch's import detection in `comment-strip.ts`. The result is
202
+ * byte-aligned with the input per line (comment and literal newlines are
203
+ * preserved), so error line numbers still match the original.
296
204
  */
297
205
  function stripCommentsAndStrings(src) {
298
- let stripped = '';
299
- let i = 0;
300
- const N = src.length;
301
- // Track the last non-whitespace non-comment output character so we
302
- // can disambiguate `/` as division (after identifier/literal/`)`/`]`)
303
- // vs regex-literal opener (after operator / punctuator / keyword /
304
- // start-of-file). Pre-fix the stripper had no regex awareness, so
305
- // patterns like `var X = /^(?:'…)/` contained an unmatched `'` that
306
- // 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.
310
- let lastNonWsChar = '';
311
- const recordOut = (ch) => {
312
- if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') {
313
- lastNonWsChar = ch;
314
- }
315
- };
316
- // Identifier-suffix detection on lastNonWsChar: any of these means
317
- // `/` is division. Anything else means `/` opens a regex.
318
- // Includes ascii word chars + `)` `]` to cover `foo()/x` `a[i]/y`.
319
- const DIVISION_AFTER = /[A-Za-z0-9_$\)\]]/;
320
- while (i < N) {
321
- const c = src[i];
322
- if (c === '/' && src[i + 1] === '/') {
323
- while (i < N && src[i] !== '\n')
324
- i++;
325
- stripped += ' ';
326
- continue;
327
- }
328
- if (c === '/' && src[i + 1] === '*') {
329
- i += 2;
330
- while (i < N && !(src[i] === '*' && src[i + 1] === '/')) {
331
- // Preserve newlines so line numbers stay aligned.
332
- if (src[i] === '\n')
333
- stripped += '\n';
334
- i++;
335
- }
336
- i += 2;
337
- stripped += ' ';
338
- continue;
339
- }
340
- // Regex literal: `/` not followed by `/` or `*` (already handled
341
- // above), AND preceded by something that makes regex valid here.
342
- // The classifier looks at lastNonWsChar — for `(`, `,`, `=`, `;`,
343
- // `{`, `[`, `:`, `!`, `&`, `|`, `?`, operators, or start-of-file,
344
- // a slash is a regex opener.
345
- if (c === '/' && !DIVISION_AFTER.test(lastNonWsChar)) {
346
- stripped += ' ';
347
- i++;
348
- while (i < N) {
349
- const rc = src[i];
350
- if (rc === '\\') {
351
- i += 2;
352
- continue;
353
- }
354
- // Regex character class [...]: `/` inside it is content.
355
- if (rc === '[') {
356
- i++;
357
- while (i < N && src[i] !== ']') {
358
- if (src[i] === '\\') {
359
- i += 2;
360
- continue;
361
- }
362
- if (src[i] === '\n') {
363
- stripped += '\n';
364
- }
365
- i++;
366
- }
367
- if (i < N)
368
- i++; // consume ']'
369
- continue;
370
- }
371
- if (rc === '/') {
372
- i++;
373
- break;
374
- }
375
- if (rc === '\n') {
376
- // Regex literals can't span newlines. If we hit one without
377
- // finding the closing `/`, this was probably NOT a regex —
378
- // bail out gracefully (rare in practice; better than getting
379
- // stuck in a wrong state).
380
- break;
381
- }
382
- i++;
383
- }
384
- // Skip regex flags (g, i, m, s, u, y, d).
385
- while (i < N && /[gimsuyd]/.test(src[i]))
386
- i++;
387
- recordOut('/');
388
- continue;
389
- }
390
- if (c === '"' || c === "'" || c === '`') {
391
- const q = c;
392
- stripped += ' ';
393
- i++;
394
- while (i < N) {
395
- const cc = src[i];
396
- if (cc === '\\') {
397
- i += 2;
398
- continue;
399
- }
400
- if (cc === q) {
401
- i++;
402
- break;
403
- }
404
- // Template interpolation: ${...} — preserve the inner
405
- // expression as raw code so the caller's depth-tracker can
406
- // see `await` etc. inside it. Strip the wrapping `${`/`}`
407
- // (those don't affect brace-depth from the caller's POV).
408
- //
409
- // CRITICAL: inside the interpolation expression, we must
410
- // recognise NESTED strings (`'}'`, `"}"`, `` `${...}` ``) so
411
- // their internal `}` characters don't decrement the depth
412
- // counter — otherwise we exit the interpolation early and
413
- // start consuming code as string content, eventually parsing
414
- // the rest of the file as one massive unclosed string. This
415
- // bit sv-utils/dist/index.mjs: multi-line backticks with
416
- // `${url.replace('}', '_')}`-style interpolations corrupted
417
- // line classification downstream. Recursive-by-loop here.
418
- if (q === '`' && cc === '$' && src[i + 1] === '{') {
419
- stripped += '${';
420
- i += 2;
421
- let depth = 1;
422
- while (i < N && depth > 0) {
423
- const ic = src[i];
424
- // Nested string inside interpolation: skip its content.
425
- if (ic === '"' || ic === "'" || ic === '`') {
426
- const iq = ic;
427
- stripped += ' ';
428
- i++;
429
- while (i < N) {
430
- const icc = src[i];
431
- if (icc === '\\') {
432
- i += 2;
433
- continue;
434
- }
435
- if (icc === iq) {
436
- i++;
437
- break;
438
- }
439
- // Nested template inside interpolation can also have its
440
- // own ${...} — recurse once more (the practical depth
441
- // observed in real code never exceeds 2; deeper nesting
442
- // falls back to brace counting which is a best-effort).
443
- if (iq === '`' && icc === '$' && src[i + 1] === '{') {
444
- stripped += '${';
445
- i += 2;
446
- let d2 = 1;
447
- while (i < N && d2 > 0) {
448
- const i2 = src[i];
449
- if (i2 === '{')
450
- d2++;
451
- else if (i2 === '}')
452
- d2--;
453
- if (d2 > 0)
454
- stripped += i2;
455
- i++;
456
- }
457
- stripped += '}';
458
- continue;
459
- }
460
- if (icc === '\n')
461
- stripped += '\n';
462
- i++;
463
- }
464
- continue;
465
- }
466
- if (ic === '{')
467
- depth++;
468
- else if (ic === '}')
469
- depth--;
470
- if (depth > 0)
471
- stripped += ic;
472
- i++;
473
- }
474
- stripped += '}';
475
- continue;
476
- }
477
- if (cc === '\n')
478
- stripped += '\n';
479
- i++;
480
- }
481
- continue;
482
- }
483
- stripped += c;
484
- recordOut(c);
485
- i++;
486
- }
487
- return stripped;
206
+ return scanJsSource(src, 'blank');
488
207
  }
489
208
  /**
490
209
  * Convert ESM `import` statements at the top of `src` to CJS
@@ -559,7 +278,7 @@ function convertEsmImportsToRequire(src) {
559
278
  continue;
560
279
  }
561
280
  // Side-effect import: `import "m";` / `import 'm';`
562
- let m = line.match(/^[ \t]*import\s+["']([^"']+)["']\s*;?\s*$/);
281
+ let m = line.match(/^[ \t]*import\s*["']([^"']+)["']\s*;?\s*$/);
563
282
  if (m) {
564
283
  requires.push(`require(${JSON.stringify(m[1])});`);
565
284
  continue;
@@ -572,14 +291,14 @@ function convertEsmImportsToRequire(src) {
572
291
  // statement survived into the async-IIFE wrap → SyntaxError
573
292
  // "import statement outside module" at facet pre-compile.
574
293
  // 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*$/);
294
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
576
295
  if (m) {
577
296
  const def = m[1], ns = m[2], mod = m[3];
578
297
  requires.push(`const ${ns} = require(${JSON.stringify(mod)}); const ${def} = ${ns}.__esModule ? ${ns}.default : ${ns};`);
579
298
  continue;
580
299
  }
581
300
  // 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*$/);
301
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s*,\s*\{([^}]+)\}\s+from\s*["']([^"']+)["']\s*;?\s*$/);
583
302
  if (m) {
584
303
  const def = m[1], bindings = m[2], mod = m[3];
585
304
  const tmp = `_nimbus_m_${counter++}`;
@@ -593,13 +312,13 @@ function convertEsmImportsToRequire(src) {
593
312
  continue;
594
313
  }
595
314
  // Namespace: `import * as ns from "m";`
596
- m = line.match(/^[ \t]*import\s+\*\s+as\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
315
+ m = line.match(/^[ \t]*import\s*\*\s*as\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
597
316
  if (m) {
598
317
  requires.push(`const ${m[1]} = require(${JSON.stringify(m[2])});`);
599
318
  continue;
600
319
  }
601
320
  // Named only: `import { a, b as c } from "m";`
602
- m = line.match(/^[ \t]*import\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']\s*;?\s*$/);
321
+ m = line.match(/^[ \t]*import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']\s*;?\s*$/);
603
322
  if (m) {
604
323
  const bindings = m[1], mod = m[2];
605
324
  const named = bindings.split(',').map((b) => {
@@ -612,7 +331,7 @@ function convertEsmImportsToRequire(src) {
612
331
  continue;
613
332
  }
614
333
  // Default only: `import x from "m";`
615
- m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s+["']([^"']+)["']\s*;?\s*$/);
334
+ m = line.match(/^[ \t]*import\s+([\w$]+)\s+from\s*["']([^"']+)["']\s*;?\s*$/);
616
335
  if (m) {
617
336
  const def = m[1], mod = m[2];
618
337
  requires.push(`const ${def} = (() => { const _m = require(${JSON.stringify(mod)}); return _m && _m.__esModule ? _m.default : _m; })();`);
@@ -801,29 +520,399 @@ function convertEsmImportsToRequire(src) {
801
520
  }
802
521
  return { requires: requires.join('\n'), body: out.join('\n') };
803
522
  }
523
+ function topLevelModuleDeclarationRanges(source) {
524
+ try {
525
+ const tokens = tokenizer(source, {
526
+ ecmaVersion: 'latest',
527
+ sourceType: 'module',
528
+ allowHashBang: true,
529
+ });
530
+ const ranges = [];
531
+ let active = null;
532
+ let braces = 0;
533
+ let parens = 0;
534
+ let brackets = 0;
535
+ const updateDepth = (type) => {
536
+ if (type === tokTypes.braceL || type === tokTypes.dollarBraceL)
537
+ braces++;
538
+ else if (type === tokTypes.braceR)
539
+ braces = Math.max(0, braces - 1);
540
+ else if (type === tokTypes.parenL)
541
+ parens++;
542
+ else if (type === tokTypes.parenR)
543
+ parens = Math.max(0, parens - 1);
544
+ else if (type === tokTypes.bracketL)
545
+ brackets++;
546
+ else if (type === tokTypes.bracketR)
547
+ brackets = Math.max(0, brackets - 1);
548
+ };
549
+ while (true) {
550
+ const token = tokens.getToken();
551
+ const type = token.type;
552
+ if (type === tokTypes.eof)
553
+ return active ? null : ranges;
554
+ if (active) {
555
+ updateDepth(type);
556
+ if (type === tokTypes.semi && braces === 0 && parens === 0 && brackets === 0) {
557
+ ranges.push({ ...active, end: token.end });
558
+ active = null;
559
+ }
560
+ continue;
561
+ }
562
+ const topLevel = braces === 0 && parens === 0 && brackets === 0;
563
+ if (topLevel && type === tokTypes._import) {
564
+ const next = tokens.getToken();
565
+ if (next.type !== tokTypes.parenL && next.type !== tokTypes.dot) {
566
+ active = { start: token.start, kind: 'import' };
567
+ }
568
+ updateDepth(next.type);
569
+ continue;
570
+ }
571
+ if (topLevel && type === tokTypes._export) {
572
+ active = { start: token.start, kind: 'export' };
573
+ continue;
574
+ }
575
+ updateDepth(type);
576
+ }
577
+ }
578
+ catch {
579
+ return null;
580
+ }
581
+ }
582
+ function hasUnscopedAwait(source) {
583
+ try {
584
+ const tokens = tokenizer(source, {
585
+ ecmaVersion: 'latest',
586
+ sourceType: 'module',
587
+ allowHashBang: true,
588
+ });
589
+ const functionBraces = [];
590
+ const functionParenDepths = [];
591
+ const methodParenCandidates = [];
592
+ const arrowExpressions = [];
593
+ let bracketDepth = 0;
594
+ let pendingMethodBody = false;
595
+ let pendingArrowBody = false;
596
+ let pendingFunctionKeyword = false;
597
+ let previous = tokTypes.eof;
598
+ let previousEnd = 0;
599
+ while (true) {
600
+ const token = tokens.getToken();
601
+ const type = token.type;
602
+ if (type === tokTypes.eof)
603
+ return false;
604
+ if (pendingMethodBody && type !== tokTypes.braceL)
605
+ pendingMethodBody = false;
606
+ if (pendingArrowBody && type !== tokTypes.braceL) {
607
+ arrowExpressions.push({
608
+ parens: methodParenCandidates.length,
609
+ braces: functionBraces.length,
610
+ brackets: bracketDepth,
611
+ });
612
+ pendingArrowBody = false;
613
+ }
614
+ if (pendingFunctionKeyword) {
615
+ if (type === tokTypes.colon || type === tokTypes.comma || type === tokTypes.braceR
616
+ || type === tokTypes.parenR || type === tokTypes.bracketR || type === tokTypes.eq)
617
+ functionParenDepths.pop();
618
+ pendingFunctionKeyword = false;
619
+ }
620
+ if (source.slice(previousEnd, token.start).includes('\n')) {
621
+ while (arrowExpressions.length > 0) {
622
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
623
+ if (methodParenCandidates.length !== arrow.parens
624
+ || functionBraces.length !== arrow.braces
625
+ || bracketDepth !== arrow.brackets)
626
+ break;
627
+ arrowExpressions.pop();
628
+ }
629
+ }
630
+ while (arrowExpressions.length > 0) {
631
+ const arrow = arrowExpressions[arrowExpressions.length - 1];
632
+ const delimited = (type === tokTypes.semi || type === tokTypes.comma)
633
+ && methodParenCandidates.length === arrow.parens
634
+ && functionBraces.length === arrow.braces
635
+ && bracketDepth === arrow.brackets;
636
+ const closed = (type === tokTypes.parenR && methodParenCandidates.length === arrow.parens)
637
+ || (type === tokTypes.bracketR && bracketDepth === arrow.brackets)
638
+ || (type === tokTypes.braceR && functionBraces.length === arrow.braces);
639
+ if (!delimited && !closed)
640
+ break;
641
+ arrowExpressions.pop();
642
+ }
643
+ if (type === tokTypes.name
644
+ && source.slice(token.start, token.end) === 'await'
645
+ && !functionBraces.includes(true)
646
+ && arrowExpressions.length === 0)
647
+ return true;
648
+ if (type === tokTypes._function || type === tokTypes._class) {
649
+ if (previous !== tokTypes.dot && previous !== tokTypes.questionDot) {
650
+ functionParenDepths.push(methodParenCandidates.length);
651
+ pendingFunctionKeyword = true;
652
+ }
653
+ }
654
+ else if (type === tokTypes.arrow) {
655
+ pendingArrowBody = true;
656
+ }
657
+ else if (type === tokTypes.parenL) {
658
+ methodParenCandidates.push(functionBraces.length > 0
659
+ && (previous === tokTypes.name || previous === tokTypes.string
660
+ || previous === tokTypes.num || previous === tokTypes.bracketR));
661
+ }
662
+ else if (type === tokTypes.parenR) {
663
+ pendingMethodBody = methodParenCandidates.pop() === true;
664
+ }
665
+ else if (type === tokTypes.bracketL) {
666
+ bracketDepth++;
667
+ }
668
+ else if (type === tokTypes.bracketR) {
669
+ bracketDepth = Math.max(0, bracketDepth - 1);
670
+ }
671
+ else if (type === tokTypes.dollarBraceL) {
672
+ functionBraces.push(false);
673
+ }
674
+ else if (type === tokTypes.braceL) {
675
+ const functionBody = pendingArrowBody
676
+ || pendingMethodBody
677
+ || functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length;
678
+ if (functionParenDepths[functionParenDepths.length - 1] === methodParenCandidates.length) {
679
+ functionParenDepths.pop();
680
+ }
681
+ functionBraces.push(functionBody);
682
+ pendingArrowBody = false;
683
+ pendingMethodBody = false;
684
+ }
685
+ else if (type === tokTypes.braceR) {
686
+ functionBraces.pop();
687
+ }
688
+ previousEnd = token.end;
689
+ previous = type;
690
+ }
691
+ }
692
+ catch {
693
+ return true;
694
+ }
695
+ }
696
+ function convertBundledModuleDeclarations(snippets) {
697
+ const imports = [];
698
+ const exports = [];
699
+ let importIndex = 0;
700
+ let markedEsm = false;
701
+ for (const snippet of snippets) {
702
+ const bindingList = snippet.match(/^[ \t]*export\s*\{([\s\S]*)\}\s*;?\s*$/);
703
+ if (bindingList && !/\}\s*from\b/.test(snippet)) {
704
+ if (!markedEsm) {
705
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
706
+ markedEsm = true;
707
+ }
708
+ for (const binding of bindingList[1].split(',')) {
709
+ const match = binding.trim().match(/^([\w$]+)(?:\s+as\s+([\w$]+))?$/);
710
+ if (!match)
711
+ return null;
712
+ const local = match[1];
713
+ const exported = match[2] || local;
714
+ exports.push(`Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`);
715
+ }
716
+ continue;
717
+ }
718
+ let ast;
719
+ try {
720
+ ast = parseJavaScriptModule(snippet);
721
+ }
722
+ catch {
723
+ return null;
724
+ }
725
+ const body = nodeList(ast, 'body');
726
+ if (body.length !== 1)
727
+ return null;
728
+ const declaration = body[0];
729
+ if (declaration.type === 'ImportDeclaration') {
730
+ const source = literalStringValue(nodeProp(declaration, 'source'));
731
+ if (!source)
732
+ return null;
733
+ const specifiers = nodeList(declaration, 'specifiers');
734
+ if (specifiers.length === 0) {
735
+ imports.push(`module.require(${JSON.stringify(source)});`);
736
+ continue;
737
+ }
738
+ const moduleName = `__nimbus_import_${importIndex++}`;
739
+ imports.push(`const ${moduleName} = module.require(${JSON.stringify(source)});`);
740
+ for (const specifier of specifiers) {
741
+ const local = nodeName(nodeProp(specifier, 'local'));
742
+ if (!local)
743
+ return null;
744
+ if (specifier.type === 'ImportDefaultSpecifier') {
745
+ imports.push(`const ${local} = ${moduleName} && ${moduleName}.__esModule ? ${moduleName}.default : ${moduleName};`);
746
+ }
747
+ else if (specifier.type === 'ImportNamespaceSpecifier') {
748
+ imports.push(`const ${local} = ${moduleName};`);
749
+ }
750
+ else if (specifier.type === 'ImportSpecifier') {
751
+ const imported = nodeName(nodeProp(specifier, 'imported'));
752
+ if (!imported)
753
+ return null;
754
+ imports.push(`const ${local} = ${moduleName}[${JSON.stringify(imported)}];`);
755
+ }
756
+ else {
757
+ return null;
758
+ }
759
+ }
760
+ continue;
761
+ }
762
+ if (declaration.type === 'ExportNamedDeclaration') {
763
+ if (nodeProp(declaration, 'source') || nodeProp(declaration, 'declaration'))
764
+ return null;
765
+ if (!markedEsm) {
766
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
767
+ markedEsm = true;
768
+ }
769
+ for (const specifier of nodeList(declaration, 'specifiers')) {
770
+ const local = nodeName(nodeProp(specifier, 'local'));
771
+ const exported = nodeName(nodeProp(specifier, 'exported'));
772
+ if (!local || !exported)
773
+ return null;
774
+ exports.push(`Object.defineProperty(module.exports, ${JSON.stringify(exported)}, { enumerable: true, get: () => ${local} });`);
775
+ }
776
+ continue;
777
+ }
778
+ if (declaration.type === 'ExportDefaultDeclaration') {
779
+ const value = nodeProp(declaration, 'declaration');
780
+ if (!value || typeof value.start !== 'number' || typeof value.end !== 'number')
781
+ return null;
782
+ if (value.type === 'FunctionDeclaration' || value.type === 'ClassDeclaration')
783
+ return null;
784
+ if (!markedEsm) {
785
+ exports.push('Object.defineProperty(module.exports, "__esModule", { value: true });');
786
+ markedEsm = true;
787
+ }
788
+ exports.push(`Object.defineProperty(module.exports, "default", { enumerable: true, value: (${snippet.slice(value.start, value.end)}) });`);
789
+ continue;
790
+ }
791
+ return null;
792
+ }
793
+ return { imports: imports.join('\n'), exports: exports.join('\n') };
794
+ }
795
+ function importMetaEdits(source, absoluteUrl) {
796
+ const edits = [];
797
+ try {
798
+ const tokens = tokenizer(source, {
799
+ ecmaVersion: 'latest',
800
+ sourceType: 'module',
801
+ allowHashBang: true,
802
+ });
803
+ while (true) {
804
+ const start = tokens.getToken();
805
+ if (start.type === tokTypes.eof)
806
+ return edits;
807
+ if (start.type !== tokTypes._import)
808
+ continue;
809
+ const dot1 = tokens.getToken();
810
+ if (dot1.type !== tokTypes.dot)
811
+ continue;
812
+ const meta = tokens.getToken();
813
+ if (meta.type !== tokTypes.name || source.slice(meta.start, meta.end) !== 'meta')
814
+ return null;
815
+ const dot2 = tokens.getToken();
816
+ if (dot2.type !== tokTypes.dot)
817
+ return null;
818
+ const property = tokens.getToken();
819
+ if (property.type !== tokTypes.name)
820
+ return null;
821
+ const propertyName = source.slice(property.start, property.end);
822
+ if (propertyName === 'url') {
823
+ edits.push({ start: start.start, end: property.end, text: JSON.stringify(absoluteUrl) });
824
+ }
825
+ else if (propertyName === 'resolve') {
826
+ edits.push({
827
+ start: start.start,
828
+ end: property.end,
829
+ text: `(specifier => globalThis.__nimbusImportMetaResolve(specifier, ${JSON.stringify(absoluteUrl)}))`,
830
+ });
831
+ }
832
+ else {
833
+ return null;
834
+ }
835
+ }
836
+ }
837
+ catch {
838
+ return null;
839
+ }
840
+ }
841
+ /**
842
+ * Converts bundler-emitted ESM without constructing an AST or loading
843
+ * esbuild-wasm. Returns null for module declarations that are not the compact,
844
+ * semicolon-terminated shapes emitted by current JS bundlers.
845
+ */
846
+ export function rewriteBundledEsmToCjs(source, absoluteUrl) {
847
+ if (hasUnscopedAwait(source))
848
+ return null;
849
+ const declarations = topLevelModuleDeclarationRanges(source);
850
+ if (!declarations || declarations.length === 0)
851
+ return null;
852
+ const declarationSnippets = declarations.map(({ start, end }) => source.slice(start, end));
853
+ for (let i = 0; i < declarations.length; i++) {
854
+ if (/^[ \t]*export\s+default\b/.test(declarationSnippets[i])
855
+ && source.slice(declarations[i].end).trim() !== '')
856
+ return null;
857
+ }
858
+ const converted = convertBundledModuleDeclarations(declarationSnippets);
859
+ if (!converted)
860
+ return null;
861
+ const metaEdits = importMetaEdits(source, absoluteUrl);
862
+ if (!metaEdits)
863
+ return null;
864
+ const edits = [
865
+ ...declarations.map(({ start, end }) => ({ start, end, text: '' })),
866
+ ...metaEdits.filter((edit) => !declarations.some(({ start, end }) => edit.start >= start && edit.end <= end)),
867
+ ].sort((a, b) => a.start - b.start);
868
+ const bodyParts = [];
869
+ let cursor = 0;
870
+ for (const edit of edits) {
871
+ if (edit.start < cursor)
872
+ return null;
873
+ bodyParts.push(source.slice(cursor, edit.start), edit.text);
874
+ cursor = edit.end;
875
+ }
876
+ bodyParts.push(source.slice(cursor));
877
+ const body = bodyParts.join('');
878
+ return {
879
+ code: converted.imports + '\n' + body + '\n' + converted.exports,
880
+ map: '',
881
+ warnings: [],
882
+ };
883
+ }
804
884
  import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm';
805
885
  /**
806
886
  * Cached reference to the esbuild namespace. Populated on first
807
887
  * `loadEsbuild()` call; nullable until then so module-load code paths
808
888
  * that never touch bundling can complete without ever evaluating
809
- * `esbuild-wasm/lib/main.js`.
889
+ * esbuild-wasm's JS at all.
810
890
  */
811
891
  let _esbuildMod = null;
812
892
  let _esbuildLoadPromise = null;
813
893
  /**
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.
894
+ * Load the esbuild-wasm namespace. Safe to call many times; concurrent
895
+ * callers share a single in-flight Promise, and a rejection clears the
896
+ * cache so a later call can retry.
897
+ *
898
+ * Exported so `tests/unit/esbuild-wasm-entrypoint.mjs` can drive the real
899
+ * specifier under a Node-style resolver. A test that restated the specifier
900
+ * would grade its own copy of it, and this defect reached production
901
+ * precisely because nothing graded the resolution.
902
+ *
903
+ * The specifier stays a literal: a computed one would defeat the host
904
+ * bundler's static analysis and leave the module out of the deployed worker.
819
905
  */
820
- async function loadEsbuild() {
906
+ export async function loadEsbuild() {
821
907
  if (_esbuildMod)
822
908
  return _esbuildMod;
823
909
  if (_esbuildLoadPromise)
824
910
  return _esbuildLoadPromise;
825
911
  _esbuildLoadPromise = (async () => {
826
- const mod = await import('esbuild-wasm');
912
+ // Deliberately dynamic: a static import would evaluate esbuild-wasm in
913
+ // every session, including the ones that only serve a shell and never
914
+ // bundle. The specifier is still a literal so the host bundler sees it.
915
+ const mod = await import('esbuild-wasm/esm/browser.js');
827
916
  _esbuildMod = mod;
828
917
  return _esbuildMod;
829
918
  })();
@@ -831,12 +920,104 @@ async function loadEsbuild() {
831
920
  return await _esbuildLoadPromise;
832
921
  }
833
922
  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
923
  _esbuildLoadPromise = null;
837
924
  throw e;
838
925
  }
839
926
  }
927
+ async function transformWithEsbuild(esbuildApi, source, options) {
928
+ let code = source;
929
+ const format = options?.format || 'esm';
930
+ const loader = options?.loader || 'ts';
931
+ if (format === 'cjs') {
932
+ try {
933
+ const direct = await esbuildApi.transform(code, {
934
+ loader,
935
+ format,
936
+ target: options?.target || 'esnext',
937
+ sourcemap: options?.sourcemap ?? false,
938
+ minify: options?.minify ?? false,
939
+ jsx: options?.jsx,
940
+ jsxFactory: options?.jsxFactory,
941
+ jsxFragment: options?.jsxFragment,
942
+ tsconfigRaw: options?.tsconfigRaw,
943
+ define: options?.define,
944
+ supported: { 'dynamic-import': false },
945
+ });
946
+ return {
947
+ code: direct.code,
948
+ map: direct.map || '',
949
+ warnings: direct.warnings?.map((warning) => ({
950
+ text: warning.text,
951
+ location: warning.location,
952
+ })) || [],
953
+ };
954
+ }
955
+ catch (error) {
956
+ const message = error instanceof Error ? error.message : String(error);
957
+ if (!/top-level await.*not supported.*cjs/i.test(message))
958
+ throw error;
959
+ }
960
+ if (hasEsmImports(code) || hasEsmExports(code)) {
961
+ const pass1 = await esbuildApi.transform(code, {
962
+ loader,
963
+ format: 'esm',
964
+ target: options?.target || 'esnext',
965
+ sourcemap: false,
966
+ minify: false,
967
+ jsx: options?.jsx,
968
+ jsxFactory: options?.jsxFactory,
969
+ jsxFragment: options?.jsxFragment,
970
+ tsconfigRaw: options?.tsconfigRaw,
971
+ define: options?.define,
972
+ supported: { 'dynamic-import': false },
973
+ });
974
+ const { requires, body } = convertEsmImportsToRequire(pass1.code);
975
+ return {
976
+ code: requires + '\nreturn (async () => {\n' + body + '\n})();\n',
977
+ map: '',
978
+ warnings: pass1.warnings?.map((warning) => ({
979
+ text: warning.text,
980
+ location: warning.location,
981
+ })) || [],
982
+ };
983
+ }
984
+ code = 'return (async () => {\n' + code + '\n})();\n';
985
+ }
986
+ const result = await esbuildApi.transform(code, {
987
+ loader,
988
+ format,
989
+ target: options?.target || 'esnext',
990
+ sourcemap: options?.sourcemap ?? false,
991
+ minify: options?.minify ?? false,
992
+ jsx: options?.jsx,
993
+ jsxFactory: options?.jsxFactory,
994
+ jsxFragment: options?.jsxFragment,
995
+ tsconfigRaw: options?.tsconfigRaw,
996
+ define: options?.define,
997
+ supported: { 'dynamic-import': false },
998
+ });
999
+ return {
1000
+ code: result.code,
1001
+ map: result.map || '',
1002
+ warnings: result.warnings?.map((warning) => ({
1003
+ text: warning.text,
1004
+ location: warning.location,
1005
+ })) || [],
1006
+ };
1007
+ }
1008
+ /** Source needed by the slim Worker Loader transform isolate. */
1009
+ export function generateEsbuildTransformRuntimeSource() {
1010
+ return [
1011
+ // scanJsSource is self-contained — its constants live in the body —
1012
+ // so this serialized copy carries the whole scanner.
1013
+ scanJsSource.toString(),
1014
+ stripCommentsAndStrings.toString(),
1015
+ hasEsmImports.toString(),
1016
+ hasEsmExports.toString(),
1017
+ convertEsmImportsToRequire.toString(),
1018
+ transformWithEsbuild.toString(),
1019
+ ].join('\n');
1020
+ }
840
1021
  // ── EsbuildService ──────────────────────────────────────────────────────
841
1022
  export class EsbuildService {
842
1023
  vfs;
@@ -844,8 +1025,9 @@ export class EsbuildService {
844
1025
  initPromise = null;
845
1026
  /** Resolved esbuild namespace — populated by ensureInit() after loadEsbuild(). */
846
1027
  _esbuild = null;
1028
+ /** Build reads use only the caller-supplied view; omit it for transform-only use. */
847
1029
  constructor(vfs) {
848
- this.vfs = vfs.as(CRED_KERNEL);
1030
+ this.vfs = vfs ?? null;
849
1031
  }
850
1032
  /**
851
1033
  * Initialize esbuild-wasm (lazy, on first use). Loads the namespace
@@ -987,82 +1169,7 @@ export class EsbuildService {
987
1169
  */
988
1170
  async transform(code, options) {
989
1171
  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
- };
1172
+ return transformWithEsbuild(this._esbuild, code, options);
1066
1173
  }
1067
1174
  /**
1068
1175
  * Bundle entry points from the VFS.
@@ -1105,13 +1212,18 @@ export class EsbuildService {
1105
1212
  warnings: result.warnings?.map(w => ({ text: w.text, location: w.location })) || [],
1106
1213
  };
1107
1214
  }
1215
+ requireVfs() {
1216
+ if (!this.vfs)
1217
+ throw new Error('EsbuildService build requires a VFS');
1218
+ return this.vfs;
1219
+ }
1108
1220
  /**
1109
1221
  * VFS resolver plugin for esbuild.
1110
- * Reads directly from the SqliteVFS (synchronous, co-located — no snapshot needed).
1222
+ * Reads through the caller's credentialed view (synchronous, no snapshot needed).
1111
1223
  * Handles: absolute paths, relative paths, bare specifiers (node_modules).
1112
1224
  */
1113
1225
  makeVfsPlugin() {
1114
- const vfs = this.vfs;
1226
+ const vfs = this.requireVfs();
1115
1227
  const EXTS = ['', '.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cjs', '.json', '.css'];
1116
1228
  const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs'];
1117
1229
  // Path helpers shared with git-commands via ./vfs-path.ts.