@nimbus-sh/worker 0.9.0 → 0.11.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 (286) hide show
  1. package/NOTICE.md +5 -0
  2. package/README.md +10 -0
  3. package/dist/_shared/session-router.d.ts +7 -0
  4. package/dist/_shared/session-router.d.ts.map +1 -1
  5. package/dist/_shared/session-router.js +7 -0
  6. package/dist/bindings/body.d.ts +6 -0
  7. package/dist/bindings/body.d.ts.map +1 -0
  8. package/dist/bindings/body.js +37 -0
  9. package/dist/bindings/kv.d.ts +0 -2
  10. package/dist/bindings/kv.d.ts.map +1 -1
  11. package/dist/bindings/kv.js +3 -42
  12. package/dist/bindings/r2.d.ts +0 -2
  13. package/dist/bindings/r2.d.ts.map +1 -1
  14. package/dist/bindings/r2.js +4 -44
  15. package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
  16. package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
  17. package/dist/esbuild-cli-artifact.generated.js +17 -0
  18. package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
  19. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
  20. package/dist/esbuild-wasm-bundle.generated.js +24 -24
  21. package/dist/facets/cirrus-real.d.ts.map +1 -1
  22. package/dist/facets/cirrus-real.js +11 -6
  23. package/dist/facets/compose.d.ts +11 -7
  24. package/dist/facets/compose.d.ts.map +1 -1
  25. package/dist/facets/compose.js +8 -31
  26. package/dist/facets/data-plan.d.ts +79 -0
  27. package/dist/facets/data-plan.d.ts.map +1 -0
  28. package/dist/facets/data-plan.js +477 -0
  29. package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
  30. package/dist/facets/esbuild-bundle-pool.js +3 -3
  31. package/dist/facets/esbuild-transform.d.ts +38 -8
  32. package/dist/facets/esbuild-transform.d.ts.map +1 -1
  33. package/dist/facets/esbuild-transform.js +219 -19
  34. package/dist/facets/exec-telemetry.d.ts +7 -3
  35. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  36. package/dist/facets/manager.d.ts +198 -69
  37. package/dist/facets/manager.d.ts.map +1 -1
  38. package/dist/facets/manager.js +1276 -652
  39. package/dist/facets/opencode-staging.d.ts +0 -2
  40. package/dist/facets/opencode-staging.d.ts.map +1 -1
  41. package/dist/facets/opencode-staging.js +5 -11
  42. package/dist/facets/process.d.ts +4 -0
  43. package/dist/facets/process.d.ts.map +1 -1
  44. package/dist/facets/process.js +3 -0
  45. package/dist/facets/read-profile.d.ts +149 -0
  46. package/dist/facets/read-profile.d.ts.map +1 -0
  47. package/dist/facets/read-profile.js +483 -0
  48. package/dist/facets/real-vite-hmr.d.ts +2 -0
  49. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  50. package/dist/facets/real-vite-hmr.js +3 -2
  51. package/dist/facets/resident-identity.d.ts +0 -1
  52. package/dist/facets/resident-identity.d.ts.map +1 -1
  53. package/dist/facets/resident-identity.js +0 -3
  54. package/dist/facets/wasm-swap-registry.d.ts +34 -25
  55. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  56. package/dist/facets/wasm-swap-registry.js +62 -56
  57. package/dist/facets/ws-terminal.d.ts +4 -4
  58. package/dist/facets/ws-terminal.d.ts.map +1 -1
  59. package/dist/facets/ws-terminal.js +4 -5
  60. package/dist/git/commands.d.ts +33 -10
  61. package/dist/git/commands.d.ts.map +1 -1
  62. package/dist/git/commands.js +1891 -197
  63. package/dist/git/network-facet.d.ts +12 -2
  64. package/dist/git/network-facet.d.ts.map +1 -1
  65. package/dist/git/network-facet.js +69 -18
  66. package/dist/git/unified-diff.d.ts +74 -0
  67. package/dist/git/unified-diff.d.ts.map +1 -0
  68. package/dist/git/unified-diff.js +676 -0
  69. package/dist/git-bundle.generated.d.ts +7 -11
  70. package/dist/git-bundle.generated.d.ts.map +1 -1
  71. package/dist/git-bundle.generated.js +7 -11
  72. package/dist/hosted/commands.d.ts.map +1 -1
  73. package/dist/hosted/commands.js +64 -245
  74. package/dist/hosted/runtime.d.ts +22 -15
  75. package/dist/hosted/runtime.d.ts.map +1 -1
  76. package/dist/hosted/runtime.js +72 -14
  77. package/dist/hosted/services.d.ts +8 -1
  78. package/dist/hosted/services.d.ts.map +1 -1
  79. package/dist/hosted/services.js +12 -13
  80. package/dist/hosted/session.d.ts +165 -0
  81. package/dist/hosted/session.d.ts.map +1 -0
  82. package/dist/hosted/session.js +132 -0
  83. package/dist/loaders/generated-workers.d.ts +6 -3
  84. package/dist/loaders/generated-workers.d.ts.map +1 -1
  85. package/dist/loaders/generated-workers.js +8 -5
  86. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  87. package/dist/loaders/npm-resolve-preamble.js +0 -27
  88. package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
  89. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
  90. package/dist/loaders/pre-bundle-preamble.js +13 -8
  91. package/dist/node-shims-artifact.generated.d.ts +13 -4
  92. package/dist/node-shims-artifact.generated.d.ts.map +1 -1
  93. package/dist/node-shims-artifact.generated.js +16 -7
  94. package/dist/npm/bin-links.d.ts +3 -1
  95. package/dist/npm/bin-links.d.ts.map +1 -1
  96. package/dist/npm/bin-links.js +32 -8
  97. package/dist/npm/cache.d.ts +11 -1
  98. package/dist/npm/cache.d.ts.map +1 -1
  99. package/dist/npm/cache.js +21 -7
  100. package/dist/npm/install-batch-facet.d.ts +4 -2
  101. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  102. package/dist/npm/install-batch-facet.js +32 -18
  103. package/dist/npm/installer.d.ts +23 -3
  104. package/dist/npm/installer.d.ts.map +1 -1
  105. package/dist/npm/installer.js +330 -131
  106. package/dist/npm/npx-install.d.ts +3 -1
  107. package/dist/npm/npx-install.d.ts.map +1 -1
  108. package/dist/npm/npx-install.js +4 -1
  109. package/dist/npm/package-lock.d.ts +32 -0
  110. package/dist/npm/package-lock.d.ts.map +1 -0
  111. package/dist/npm/package-lock.js +65 -0
  112. package/dist/npm/placement.d.ts +16 -0
  113. package/dist/npm/placement.d.ts.map +1 -0
  114. package/dist/npm/placement.js +33 -0
  115. package/dist/npm/pre-bundle-facet.d.ts +1 -30
  116. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  117. package/dist/npm/pre-bundle-facet.js +3 -5
  118. package/dist/npm/r2-cache.d.ts +12 -6
  119. package/dist/npm/r2-cache.d.ts.map +1 -1
  120. package/dist/npm/r2-cache.js +34 -16
  121. package/dist/npm/resolve-one-facet.d.ts +3 -0
  122. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  123. package/dist/npm/resolve-one-facet.js +15 -3
  124. package/dist/npm/resolver.d.ts +11 -13
  125. package/dist/npm/resolver.d.ts.map +1 -1
  126. package/dist/npm/resolver.js +12 -25
  127. package/dist/opencode-artifact.generated.js +2 -2
  128. package/dist/replica/routing.d.ts +2 -38
  129. package/dist/replica/routing.d.ts.map +1 -1
  130. package/dist/replica/routing.js +3 -55
  131. package/dist/router/index.d.ts.map +1 -1
  132. package/dist/router/index.js +15 -2
  133. package/dist/router/public-directory.d.ts +18 -0
  134. package/dist/router/public-directory.d.ts.map +1 -1
  135. package/dist/router/public-directory.js +1 -1
  136. package/dist/router/remote-api.d.ts +3 -1
  137. package/dist/router/remote-api.d.ts.map +1 -1
  138. package/dist/router/remote-api.js +42 -79
  139. package/dist/runtime/bash-repl.js +2 -2
  140. package/dist/runtime/bun-repl.d.ts.map +1 -1
  141. package/dist/runtime/bun-repl.js +2 -13
  142. package/dist/runtime/cpython-resident.d.ts.map +1 -1
  143. package/dist/runtime/cpython-resident.js +2 -10
  144. package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
  145. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
  146. package/dist/runtime/esbuild-wasm-bytes.js +66 -92
  147. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  148. package/dist/runtime/facet-loader-host.js +0 -7
  149. package/dist/runtime/git-bundle-artifact.d.ts +17 -0
  150. package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
  151. package/dist/runtime/git-bundle-artifact.js +25 -0
  152. package/dist/runtime/node-repl.d.ts.map +1 -1
  153. package/dist/runtime/node-repl.js +2 -13
  154. package/dist/runtime/node-shims-artifact.d.ts +22 -23
  155. package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
  156. package/dist/runtime/node-shims-artifact.js +48 -78
  157. package/dist/runtime/node-shims.d.ts.map +1 -1
  158. package/dist/runtime/node-shims.js +2358 -996
  159. package/dist/runtime/opencode-artifact.d.ts.map +1 -1
  160. package/dist/runtime/opencode-artifact.js +17 -53
  161. package/dist/runtime/opencode-facet-runner.d.ts +3 -6
  162. package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
  163. package/dist/runtime/opencode-facet-runner.js +45 -10
  164. package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
  165. package/dist/runtime/opentui-wasm-bytes.js +16 -49
  166. package/dist/runtime/package-manager.d.ts +0 -2
  167. package/dist/runtime/package-manager.d.ts.map +1 -1
  168. package/dist/runtime/package-manager.js +0 -3
  169. package/dist/runtime/python-repl.d.ts.map +1 -1
  170. package/dist/runtime/python-repl.js +4 -35
  171. package/dist/runtime/repl-session.d.ts +10 -0
  172. package/dist/runtime/repl-session.d.ts.map +1 -1
  173. package/dist/runtime/repl-session.js +7 -0
  174. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  175. package/dist/runtime/ruby-repl.js +2 -2
  176. package/dist/runtime/ruby-resident.d.ts.map +1 -1
  177. package/dist/runtime/ruby-resident.js +41 -51
  178. package/dist/runtime/runtime-catalog.d.ts +10 -3
  179. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  180. package/dist/runtime/runtime-catalog.js +125 -15
  181. package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
  182. package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
  183. package/dist/runtime/sqlite-wasm-bytes.js +21 -57
  184. package/dist/runtime/staged-source.d.ts +67 -0
  185. package/dist/runtime/staged-source.d.ts.map +1 -0
  186. package/dist/runtime/staged-source.js +113 -0
  187. package/dist/runtime/wasi/preamble.d.ts +0 -22
  188. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  189. package/dist/runtime/wasi/preamble.js +98 -1425
  190. package/dist/runtime-catalog.generated.js +1 -1
  191. package/dist/session/agent.d.ts +14 -2
  192. package/dist/session/agent.d.ts.map +1 -1
  193. package/dist/session/agent.js +57 -19
  194. package/dist/session/ai.d.ts +0 -11
  195. package/dist/session/ai.d.ts.map +1 -1
  196. package/dist/session/ai.js +0 -4
  197. package/dist/session/fs-watch.d.ts +0 -11
  198. package/dist/session/fs-watch.d.ts.map +1 -1
  199. package/dist/session/fs-watch.js +0 -21
  200. package/dist/session/hibernation.d.ts +68 -2
  201. package/dist/session/hibernation.d.ts.map +1 -1
  202. package/dist/session/hibernation.js +95 -1
  203. package/dist/session/init-phases.d.ts +1 -2
  204. package/dist/session/init-phases.d.ts.map +1 -1
  205. package/dist/session/init-phases.js +1 -2
  206. package/dist/session/init.d.ts.map +1 -1
  207. package/dist/session/init.js +7 -20
  208. package/dist/session/keys.d.ts +0 -3
  209. package/dist/session/keys.d.ts.map +1 -1
  210. package/dist/session/keys.js +0 -3
  211. package/dist/session/legacy-reset.d.ts +11 -0
  212. package/dist/session/legacy-reset.d.ts.map +1 -0
  213. package/dist/session/legacy-reset.js +16 -0
  214. package/dist/session/nimbus-session.d.ts +50 -16
  215. package/dist/session/nimbus-session.d.ts.map +1 -1
  216. package/dist/session/nimbus-session.js +77 -71
  217. package/dist/session/npm-install-port.d.ts.map +1 -1
  218. package/dist/session/npm-install-port.js +2 -0
  219. package/dist/session/programmatic.d.ts +13 -18
  220. package/dist/session/programmatic.d.ts.map +1 -1
  221. package/dist/session/programmatic.js +53 -39
  222. package/dist/session/replica-routes.d.ts +2 -3
  223. package/dist/session/replica-routes.d.ts.map +1 -1
  224. package/dist/session/replica-routes.js +3 -10
  225. package/dist/session/routes.d.ts.map +1 -1
  226. package/dist/session/routes.js +11 -12
  227. package/dist/session/rpc.d.ts +108 -21
  228. package/dist/session/rpc.d.ts.map +1 -1
  229. package/dist/session/rpc.js +194 -39
  230. package/dist/session/start-real-vite.js +2 -2
  231. package/dist/session/state-store.d.ts +1 -17
  232. package/dist/session/state-store.d.ts.map +1 -1
  233. package/dist/session/state-store.js +3 -53
  234. package/dist/session/supervisor-op.d.ts +7 -0
  235. package/dist/session/supervisor-op.d.ts.map +1 -1
  236. package/dist/session/supervisor-op.js +6 -1
  237. package/dist/session/supervisor-rpc.d.ts +116 -19
  238. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  239. package/dist/session/supervisor-rpc.js +194 -84
  240. package/dist/session/vite-command.d.ts.map +1 -1
  241. package/dist/session/vite-command.js +17 -14
  242. package/dist/session/ws.d.ts.map +1 -1
  243. package/dist/session/ws.js +20 -8
  244. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  245. package/dist/shell/npm-bin-entrypoints.js +30 -5
  246. package/dist/vfs/facet-resident-limits.d.ts +33 -0
  247. package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
  248. package/dist/vfs/facet-resident-limits.js +33 -0
  249. package/dist/vfs/facet-resident-store.d.ts +2 -10
  250. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  251. package/dist/vfs/facet-resident-store.js +1594 -219
  252. package/dist/workspace-host.d.ts +3 -1
  253. package/dist/workspace-host.d.ts.map +1 -1
  254. package/dist/workspace-host.js +2 -0
  255. package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
  256. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
  257. package/dist/wrangler/nimbus-wrangler.js +8 -22
  258. package/package.json +34 -7
  259. package/public/_assets/esbuild-0.24.2.js +2380 -0
  260. package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
  261. package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
  262. package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
  263. package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
  264. package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
  265. package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
  266. package/scripts/build-opencode-attach-entry.mjs +51 -1
  267. package/scripts/bundle-esbuild-wasm.mjs +55 -39
  268. package/scripts/bundle-facet-workers.mjs +159 -10
  269. package/scripts/bundle-git.mjs +88 -27
  270. package/scripts/bundle-node-shims.mjs +72 -46
  271. package/scripts/bundle-runtime.mjs +18 -421
  272. package/scripts/cf-git-patch.mjs +137 -0
  273. package/scripts/patch-install-deps.mjs +49 -96
  274. package/scripts/runtime-specs.mjs +435 -0
  275. package/vendor/git-http.generated.d.ts +82 -0
  276. package/vendor/git-upstream.generated.d.ts +4378 -0
  277. package/vendor/git.LICENSE +7 -0
  278. package/vendor/git.generated.d.mts +21 -0
  279. package/vendor/git.generated.mjs +103 -0
  280. package/dist/replica/suspension.d.ts +0 -36
  281. package/dist/replica/suspension.d.ts.map +0 -1
  282. package/dist/replica/suspension.js +0 -49
  283. package/dist/runtime/static-server.d.ts +0 -3
  284. package/dist/runtime/static-server.d.ts.map +0 -1
  285. package/dist/runtime/static-server.js +0 -133
  286. package/scripts/clean-dist.mjs +0 -7
@@ -15,29 +15,38 @@
15
15
  * Long-running processes use a dynamic Worker entrypoint that stays
16
16
  * registered in ProcessTable and PortRegistry until exit or kill.
17
17
  */
18
- import { fetchNodeShimsCode } from '../runtime/node-shims-artifact.js';
18
+ import { MK_COMPILED_FN_SOURCE } from '@nimbus-sh/core/_shared/compiled-fn.js';
19
+ import { exitCodeForSignal } from '@nimbus-sh/core/substrate/lifo/shell/signals.js';
20
+ import { fetchNodeFacetSources } from '../runtime/node-shims-artifact.js';
19
21
  import { generateSqliteFacetPreamble } from '../runtime/sqlite-shim.js';
20
22
  import { getRealNodeImportsCode } from '@nimbus-sh/core/_shared/real-node-imports.js';
21
- import { FACET_RESIDENT_STORE_SOURCE } from '../vfs/facet-resident-store.js';
22
23
  import { VFS_CURSOR_SEED_SOURCE, serializeFacetVfsCursor, } from '@nimbus-sh/core/_shared/facet-vfs-cursor.js';
23
- import { VFS_WRITE_LEDGER_SOURCE } from '@nimbus-sh/core/_shared/vfs-write-ledger.js';
24
- import { ExecutionFs } from '@nimbus-sh/core/shell/execution-fs.js';
24
+ import { typescriptLoader } from '@nimbus-sh/core/_shared/typescript-specifiers.js';
25
25
  import { stripLeadingSlashes, vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
26
26
  import { clearPortCapability, listPortReservations, readPortReservation, readPortReservationByOwner, releasePortReservation, restoreReservedPortCapability, } from '../session/port-capability.js';
27
27
  import { deriveResidentOwner } from './resident-identity.js';
28
28
  import { z } from 'zod/v4';
29
29
  import { RESIDENT_OWNER_KEY_PREFIX, DURABLE_IMAGES_KEY_PREFIX } from '../session/keys.js';
30
30
  import { PORT_CAPABILITY_KEY_PREFIX } from '../session/keys.js';
31
- import { unbindPublicPortCapability } from '../router/public-directory.js';
32
- import { prefetchForRequire, ClosureBoundExceededError } from '@nimbus-sh/core/runtime/require-resolver.js';
33
- import { hasTopLevelModuleSyntax } from '@nimbus-sh/core/runtime/javascript-ast.js';
31
+ import { sessionIdentity, unbindPublicPortCapability } from '../router/public-directory.js';
32
+ import { prefetchForRequire, requireFsOverBridge, ClosureBoundExceededError } from '@nimbus-sh/core/runtime/require-resolver.js';
33
+ import { hasTopLevelModuleSyntax, parseJavaScriptModule } from '@nimbus-sh/core/runtime/javascript-ast.js';
34
+ import { findStaticFsReferences } from '@nimbus-sh/core/runtime/static-fs-refs.js';
35
+ import { planFacetData } from './data-plan.js';
36
+ import { principalTag, profilePrincipal, ReadProfile, verifiedEvidence, } from './read-profile.js';
37
+ /** What the shared read profile may add to one launch: an eighth of its module map's bytes. */
38
+ const READ_PROFILE_LAUNCH_BYTES = Math.floor(VFS_BUNDLE_MAX_BYTES / 8);
39
+ import { NpmCache } from '../npm/cache.js';
34
40
  import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
41
+ import { mayHaveDynamicImport } from '@nimbus-sh/core/runtime/dynamic-import-rewrite.js';
35
42
  import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/platform/oom-discriminator.js';
36
43
  import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
37
44
  import { TurnBudget, PacedWork, turnChunkMaxBytes, withResolvers } from '@nimbus-sh/fabric/turn-budget.js';
38
45
  import { onColdStart } from '@nimbus-sh/fabric/generation.js';
39
46
  import { FencedWork, FENCED_WORK_KEY_PREFIX, } from '@nimbus-sh/fabric/fenced-work.js';
40
- import { EsbuildService, rewriteBundledEsmToCjs, rewriteProvidedCommonJsModules, } from '@nimbus-sh/core/runtime/esbuild-service.js';
47
+ import { rewriteBundledEsmToCjs, rewriteProvidedCommonJsModules, } from '@nimbus-sh/core/runtime/esbuild-service.js';
48
+ import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
49
+ import { DEFAULT_CJS_CONDITIONS, DEFAULT_ESM_CONDITIONS, resolvePackageEntry, parseResolvablePackageJson, } from '@nimbus-sh/core/_shared/exports-resolver.js';
41
50
  import { isExecDiagEnabled, recordExecTelemetry } from './exec-telemetry.js';
42
51
  import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/platform/rpc-dispose.js';
43
52
  import { sqliteWasmModuleEntry } from './opencode-staging.js';
@@ -49,13 +58,26 @@ import { persistDurableWorkerImage, purgeDurableWorkerImages, } from './durable-
49
58
  import { SQLITE_WASM_MODULE_NAME, } from '../runtime/opencode-facet-runner.js';
50
59
  import { parsePortFromArgv, resolveLongRunningPort } from '@nimbus-sh/core/runtime/long-running-handle.js';
51
60
  import { DEFAULT_FACET_BUNDLE_PROFILE, } from '@nimbus-sh/core/runtime/bundle-profile.js';
52
- import { CF_COMPAT_DATE, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES, PREFETCH_CACHE_MAX_BYTES, } from '@nimbus-sh/core/constants.js';
61
+ import { CF_COMPAT_DATE, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES, PREFETCH_CACHE_MAX_BYTES, ESM_TRANSFORM_CACHE_MAX_BYTES, FS_LIST_PAGE_LIMIT, } from '@nimbus-sh/core/constants.js';
62
+ import { LEDGER_ROW_BYTES } from '@nimbus-sh/core/runtime/storage-ledger.js';
63
+ import { ONE_SHOT_STORE_MEMORY_BYTES, RESIDENT_CHUNK_BYTES } from '../vfs/facet-resident-limits.js';
53
64
  import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
54
- import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
65
+ import { CRED_KERNEL, isNativeBinPath } from '@nimbus-sh/core/runtime/os-contracts.js';
55
66
  import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
67
+ import { supervisorBindingProps } from '@nimbus-sh/fabric/supervisor-props.js';
56
68
  import { wasmImageDigest } from './wasm-image-digest.js';
57
- import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
58
- const ISOLATED_ESM_TRANSFORM_MIN_BYTES = 512 * 1024;
69
+ import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, setTransformCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
70
+ const launchAdapters = new WeakMap();
71
+ function filesOf(fs) {
72
+ let files = launchAdapters.get(fs);
73
+ if (files === undefined) {
74
+ files = requireFsOverBridge(fs);
75
+ launchAdapters.set(fs, files);
76
+ }
77
+ return files;
78
+ }
79
+ /** A bundled ESM file this large is rewritten to CJS without esbuild when its shape allows. */
80
+ const BUNDLED_ESM_REWRITE_MIN_BYTES = 512 * 1024;
59
81
  /**
60
82
  * Detect & restore a Uint8Array that's been JSON-mangled to a
61
83
  * {"0":n,"1":n,...} object during the result-envelope round-trip.
@@ -281,6 +303,14 @@ const ENTRYPOINT_TIMER_TRACKER = `
281
303
  g.setInterval = function(fn, ms, ...a){ const id = si(fn, ms, ...a); iv.add(id); g.__nimbusPendingTimers++; return id; };
282
304
  g.clearInterval = function(id){ if (iv.delete(id)) g.__nimbusPendingTimers--; return ci(id); };
283
305
  }
306
+ // process.exit: the program's pending timers and intervals never fire again.
307
+ g.__nimbusStopProgramTimers = function(){
308
+ for (const id of one) ct(id);
309
+ one.clear();
310
+ if (typeof ci === "function") for (const id of iv) ci(id);
311
+ iv.clear();
312
+ g.__nimbusPendingTimers = 0;
313
+ };
284
314
  })(globalThis);
285
315
  `;
286
316
  /**
@@ -304,6 +334,11 @@ const ENTRYPOINT_TIMER_TRACKER = `
304
334
  */
305
335
  const RESIDENCY_MISS_REPORT = `
306
336
  const __NIMBUS_RESIDENCY_NAMED_MAX = 20;
337
+ // Everything the next launch of this entry must stage: unanswered reads and
338
+ // modules whose text arrived after boot, too late to compile.
339
+ function __nimbusStagingMisses() {
340
+ return [...(globalThis.__nimbusVfsResidencyMisses || []), ...(globalThis.__nimbusModuleMisses || [])];
341
+ }
307
342
  function __nimbusResidencyMissReport() {
308
343
  const __missed = globalThis.__nimbusVfsResidencyMisses;
309
344
  if (!__missed || __missed.size === 0) return "";
@@ -370,11 +405,9 @@ const SQLITE_FACET_IMPORT = `import __nimbusSqliteWasmModule from "${SQLITE_WASM
370
405
  /**
371
406
  * Generate one-shot runtime code with a plain fetch handler.
372
407
  */
373
- export async function generateEntrypointCode(userCode, vfsState, usesSqlite, shims, wasmImports = []) {
408
+ export async function generateEntrypointCode(userCode, vfsState, usesSqlite, sources, wasmImports = []) {
374
409
  const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
375
410
  const bundleSource = await facetVfsBundleSourceFor(vfsState);
376
- const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
377
- const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
378
411
  return {
379
412
  code: `
380
413
  ${bundleSource.imports}
@@ -384,25 +417,7 @@ ${facetWasmImportsSource(wasmImports)}
384
417
  const USER_CODE = ${safeCode};
385
418
  const __NimbusHostResponse = globalThis.Response;
386
419
 
387
- function __mkCompiledFn(code) {
388
- // Node strips a leading shebang from every module before evaluation;
389
- // bin scripts are commonly bundled verbatim with their
390
- // "#!/usr/bin/env node" line, which is a SyntaxError under new Function.
391
- if (typeof code === "string" && code.charCodeAt(0) === 35 && code.charCodeAt(1) === 33) {
392
- const __nl = code.indexOf("\\n");
393
- code = __nl >= 0 ? code.slice(__nl + 1) : "";
394
- }
395
- function renameIfDeclared(name) {
396
- const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
397
- return re.test(code) ? name + "__nimbus_unused" : name;
398
- }
399
- const baseParams = [
400
- "exports", renameIfDeclared("require"), "module",
401
- renameIfDeclared("__filename"),
402
- renameIfDeclared("__dirname"),
403
- ];
404
- return new Function(...baseParams, code);
405
- }
420
+ ${MK_COMPILED_FN_SOURCE}
406
421
 
407
422
  let __compiledFn = null;
408
423
  let __entryCompileFailure = null;
@@ -412,15 +427,13 @@ try {
412
427
  __entryCompileFailure = (__e && __e.stack) || (__e && __e.message) || String(__e);
413
428
  }
414
429
 
415
- // VFS bundle + manifest + pre-compiled modules — all at module level (startup time).
430
+ // The module bundle and its pre-compiled modules, at module level (startup time).
416
431
  const __MODULE_VFS_BUNDLE = ${bundleSource.expression};
417
- const __MODULE_VFS_MANIFEST = ${safeManifest};
418
- const __MODULE_VFS_METADATA = ${safeMetadata};
419
432
  const __compiledModules = new Map();
420
433
  const __compileFailures = new Map();
421
434
  // Precompile JS modules AND extensionless CJS entries (bin scripts and
422
435
  // shims). workerd forbids new Function at request time, so anything not
423
- // precompiled here surfaces the misleading "file was not pre-bundled".
436
+ // precompiled here surfaces the misleading "not in this launch's module map".
424
437
  ${BUNDLE_PRECOMPILE_LOOP}
425
438
 
426
439
  class __ProcessExit extends Error {
@@ -430,7 +443,7 @@ class __ProcessExit extends Error {
430
443
  export default {
431
444
  async fetch(request, workerEnv) {
432
445
  const args = await request.json();
433
- const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag, vfsCursor } = args;
446
+ const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag, vfsCursor, dataPlan } = args;
434
447
  // Per invocation, not per module: this body is cached on
435
448
  // hash(code + bundle + manifest) and reused by any session whose snapshot
436
449
  // hashes the same, and epochs are per supervisor incarnation.
@@ -441,10 +454,30 @@ ${VFS_CURSOR_SEED_SOURCE}
441
454
  // ends it early.
442
455
  const __entryBudgetMs = Infinity;
443
456
  let __drainPasses = 0;
444
- const __vfsBundle = __MODULE_VFS_BUNDLE;
445
- const __vfsManifest = __MODULE_VFS_MANIFEST;
446
- const __vfsMetadata = __MODULE_VFS_METADATA;
447
457
  const __supervisor = workerEnv?.SUPERVISOR || null;
458
+ // The same store, namespace and data plan a resident boots on, backed by
459
+ // this run's heap (runOnce hosts no SQLite; vfs/facet-resident-store.ts).
460
+ // Declared inside the request, beside the shims, so a loader that reuses
461
+ // this isolate for another run starts it from an empty store.
462
+ ${sources.residentStore}
463
+ __residentBindInMemory(${ONE_SHOT_STORE_MEMORY_BYTES});
464
+ __residentSetStorage(undefined, __supervisor);
465
+ __nsSetCred(cred);
466
+ __residentSetPlan(dataPlan);
467
+ __residentSetPushRoots([_cwd || "/home/user", "/tmp"]);
468
+ // The module bundle stays at module scope: a reused isolate adopts it
469
+ // again on its next run, and its text cells are shared, not copied.
470
+ const __residentBooted = await __residentBoot(() => __MODULE_VFS_BUNDLE, __MODULE_VFS_CURSOR, __supervisor);
471
+ if (__residentBooted.cursor) {
472
+ globalThis.__nimbusVfsCursor = { epoch: __residentBooted.cursor.epoch, rev: __residentBooted.cursor.rev };
473
+ }
474
+ // No user code runs before the namespace answers: it is the view every
475
+ // synchronous stat, exists and readdir reads (CUTOVER #13).
476
+ const __namespaceFailure = await __residentRequireNamespace(__supervisor, __residentBooted.failure);
477
+ if (__namespaceFailure) {
478
+ return __NimbusHostResponse.json({ exitCode: 1, stdout: "", stderr: __namespaceFailure + "\\n", vfsWrites: {}, residencyMisses: [] });
479
+ }
480
+ const __vfsBundle = __nimbusResidentBundle;
448
481
  const __pendingIO = [];
449
482
  // Fix 6 orphan counters (same as NodeProcess.run) — count RPC writes
450
483
  // that get dropped during isolate teardown so reportExit can report them.
@@ -471,24 +504,24 @@ ${VFS_CURSOR_SEED_SOURCE}
471
504
  let stdout = "", stderr = "";
472
505
  let exitCode = 0;
473
506
  const __nimbusDeferProcessExitReport = true;
474
- ${VFS_WRITE_LEDGER_SOURCE}
507
+ ${sources.ledger}
475
508
  const __vfsDirs = {};
476
509
 
477
510
  ${ENTRYPOINT_TIMER_TRACKER}
478
- ${shims}
511
+ ${sources.shims}
479
512
 
480
513
  ${ENTRYPOINT_EVENT_LOOP}
481
514
  ${RESIDENCY_MISS_REPORT}
482
515
 
483
516
  // Override console AND process.stdout/stderr for live SUPERVISOR streaming
484
517
  if (__supervisor && !captureOutput) {
485
- __consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
486
- __consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
518
+ __consoleMod.log = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
519
+ __consoleMod.error = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
487
520
  __consoleMod.warn = __consoleMod.error;
488
521
  __consoleMod.info = __consoleMod.log;
489
522
  __consoleMod.debug = __consoleMod.log;
490
- __processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
491
- __processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
523
+ __processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
524
+ __processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
492
525
  }
493
526
 
494
527
  try { globalThis.console = __consoleMod; } catch {}
@@ -532,6 +565,12 @@ ${RESIDENCY_MISS_REPORT}
532
565
  }
533
566
  }
534
567
  }
568
+ // A program that ended without process.exit still gets its 'exit' event.
569
+ if (__nimbusProcessExitCode === null) {
570
+ __nimbusEmitExit(exitCode);
571
+ if (__nimbusProcessExitCode !== null) exitCode = __nimbusProcessExitCode;
572
+ __nimbusProgramStopped = true;
573
+ }
535
574
 
536
575
  async function __drainPendingIO(maxPasses = 12) {
537
576
  let __settledIO = 0;
@@ -602,8 +641,11 @@ ${RESIDENCY_MISS_REPORT}
602
641
  // Unconditional, unlike diag: the supervisor stages these paths into
603
642
  // the next bundle for the same entry, so withholding them behind a
604
643
  // debug flag would leave the miss to repeat forever.
605
- residencyMisses: [...(globalThis.__nimbusVfsResidencyMisses || [])],
606
- ...(__diag ? { diag: { drainPasses: __drainPasses, rpcWrites: __rpcWriteCount, fsRpcReads: globalThis.__nimbusFsRpcReads || 0 } } : {}),
644
+ residencyMisses: __nimbusStagingMisses(),
645
+ ...(__diag ? { diag: {
646
+ drainPasses: __drainPasses, rpcWrites: __rpcWriteCount, fsRpcReads: globalThis.__nimbusFsRpcReads || 0,
647
+ namespaceRefusals: (globalThis.__nimbusVfsCoherence && globalThis.__nimbusVfsCoherence.namespaceRefusals) || 0,
648
+ } } : {}),
607
649
  });
608
650
  }
609
651
  };
@@ -673,7 +715,7 @@ function facetWasmImportsSource(wasmImports) {
673
715
  return `${lines.join('\n')}\nglobalThis.__nimbusPrecompiledWasm = new Map([${entries.join(', ')}]);`
674
716
  + `\nglobalThis.__nimbusPrecompiledWasmByDigest = new Map([${byDigest.join(', ')}]);`;
675
717
  }
676
- export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite, shims, pacer) {
718
+ export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite, sources, pacer) {
677
719
  const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
678
720
  const safeArgs = JSON.stringify({
679
721
  argv: opts.argv || [],
@@ -686,8 +728,6 @@ export async function generateLongRunningNodeCode(userCode, vfsState, opts, uses
686
728
  cred: opts.cred,
687
729
  });
688
730
  const bundleSource = await facetVfsBundleSourceFor(vfsState, pacer);
689
- const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
690
- const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
691
731
  return {
692
732
  code: `
693
733
  ${bundleSource.imports}
@@ -699,18 +739,7 @@ const USER_CODE = ${safeCode};
699
739
  const __NIMBUS_ARGS = ${safeArgs};
700
740
  const __NimbusHostResponse = globalThis.Response;
701
741
 
702
- function __mkCompiledFn(code) {
703
- function renameIfDeclared(name) {
704
- const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
705
- return re.test(code) ? name + "__nimbus_unused" : name;
706
- }
707
- const baseParams = [
708
- "exports", renameIfDeclared("require"), "module",
709
- renameIfDeclared("__filename"),
710
- renameIfDeclared("__dirname"),
711
- ];
712
- return new Function(...baseParams, code);
713
- }
742
+ ${MK_COMPILED_FN_SOURCE}
714
743
 
715
744
  let __compiledFn = null;
716
745
  let __entryCompileFailure = null;
@@ -725,8 +754,6 @@ try {
725
754
  // and this object are the same bytes twice, and for pi that is the largest
726
755
  // single allocation in the facet before the program starts.
727
756
  let __MODULE_VFS_BUNDLE = ${bundleSource.expression};
728
- const __MODULE_VFS_MANIFEST = ${safeManifest};
729
- const __MODULE_VFS_METADATA = ${safeMetadata};
730
757
  const __compiledModules = new Map();
731
758
  const __compileFailures = new Map();
732
759
  // Module evaluation is the ONLY place workerd allows a string to become code —
@@ -735,7 +762,7 @@ const __compileFailures = new Map();
735
762
  // here, off the module map. That is why code cannot come from the store.
736
763
  ${BUNDLE_PRECOMPILE_LOOP}
737
764
 
738
- ${FACET_RESIDENT_STORE_SOURCE}
765
+ ${sources.residentStore}
739
766
 
740
767
  class __ProcessExit extends Error {
741
768
  constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
@@ -746,6 +773,15 @@ let __nimbusStarting = null;
746
773
  let __nimbusRuntime = null;
747
774
  let __nimbusAttachedLifecycle = null;
748
775
 
776
+ // The platform's timer, captured at module evaluation, before the shims wrap
777
+ // setTimeout in the resumption barrier. The flush below yields turns for its
778
+ // own bookkeeping; that is not a resumption of the program, and through the
779
+ // wrapped timer it paid a hidden ACQUIRE per yield — the one an inbound
780
+ // request relied on without saying so, and a second one after the response
781
+ // on every request. The request's barrier is taken explicitly, in the shared
782
+ // dispatch (__nimbusServeHttp), exactly once.
783
+ const __nimbusPlatformSetTimeout = setTimeout;
784
+
749
785
  async function __nimbusFlushRuntime() {
750
786
  const rt = __nimbusRuntime;
751
787
  if (!rt) return;
@@ -765,7 +801,9 @@ async function __nimbusFlushRuntime() {
765
801
  rt.drainVfsMutations(),
766
802
  ]);
767
803
  for (let pass = 0; pass < 12; pass++) {
768
- await new Promise((resolve) => setTimeout(resolve, 0));
804
+ const turn = Promise.withResolvers();
805
+ __nimbusPlatformSetTimeout(turn.resolve, 0);
806
+ await turn.promise;
769
807
  if (rt.pendingIO.length <= rt.settledIO) break;
770
808
  const slice = rt.pendingIO.slice(rt.settledIO);
771
809
  rt.settledIO = rt.pendingIO.length;
@@ -794,8 +832,6 @@ async function __nimbusEnsureStarted(workerEnv, workerCtx, __startArgs) {
794
832
  // time. Same reason argv/env/pid want to move here.
795
833
  const __MODULE_VFS_CURSOR = (__startArgs && __startArgs.vfsCursor) || null;
796
834
  ${VFS_CURSOR_SEED_SOURCE}
797
- const __vfsManifest = __MODULE_VFS_MANIFEST;
798
- const __vfsMetadata = __MODULE_VFS_METADATA;
799
835
  const __supervisor = workerEnv?.SUPERVISOR || null;
800
836
  // The resident set lives in this facet's own SQLite rather than its heap.
801
837
  // A synchronous read cannot block and no JS stack here can be suspended, so
@@ -803,41 +839,56 @@ ${VFS_CURSOR_SEED_SOURCE}
803
839
  // \`ctx.storage.sql.exec\` returns a Cursor, not a Promise. See
804
840
  // vfs/facet-resident-store.ts.
805
841
  __residentBind(workerCtx);
806
- const __residentAdopted = __residentAdoptModuleBundle(__MODULE_VFS_BUNDLE, __MODULE_VFS_CURSOR);
807
- // The store now holds these bytes; the parsed object is the duplicate.
808
- __MODULE_VFS_BUNDLE = null;
809
- // One cursor, not two. The seed above publishes the cursor this SPAWN
810
- // staged at, which is right for a cold slot and stale for a warm one — the
811
- // store's persisted cursor describes what the rows actually are, and it is
812
- // the only one that survived the last incarnation.
813
- if (__residentAdopted) {
814
- globalThis.__nimbusVfsCursor = { epoch: __residentAdopted.epoch, rev: __residentAdopted.rev };
815
- }
816
- // Bring the store to the authority's current state. This is what makes a
817
- // first synchronous read of an untouched file succeed, and it is the ONLY
818
- // blocking step: the waiting is done once, here, before the program's
819
- // first instruction, so that no synchronous read after it ever has to
820
- // wait.
842
+ __residentSetStorage(__startArgs && __startArgs.storage, __supervisor);
843
+ __nsSetCred(cred);
844
+ __residentSetPlan(__startArgs && __startArgs.dataPlan);
845
+ // The shared read profile's files this launch staged; each is struck off
846
+ // when read, and what is left at exit is reported unread (read-profile.ts).
847
+ globalThis.__nimbusProfileStaged = new Set((__startArgs && __startArgs.profileStaged) || []);
848
+ __residentSetPushRoots([_cwd || "/home/user", "/tmp"]);
849
+ // Bring the store to the authority's current state before the program's
850
+ // first instruction. This is what makes a first synchronous read of an
851
+ // untouched file succeed, and it is the ONLY blocking step: the waiting is
852
+ // done once, here, so that no synchronous read after it ever has to wait.
821
853
  //
822
- // Cheap on a warm slot, and no longer merely by assumption: rows the
823
- // absolute listing proves current are kept, so the pass fetches what
824
- // changed rather than trusting that nothing did. Run whether or not the
825
- // adopt produced a cursor — a facet whose snapshot carried none holds an
826
- // empty store, and filling it here is what stops its first ACQUIRE from
827
- // asking about a null epoch and being answered with a poison.
828
- if (__supervisor) {
854
+ // A cold store adopts this spawn's snapshot and fills the rest. A store
855
+ // that outlived its process — a durable application's, relaunched or
856
+ // re-driven — is cheap to bring current: rows the absolute listing proves
857
+ // current are kept, and only what changed is fetched. It is SERVED only
858
+ // once that listing has vouched for it: rows it cannot vouch for are a
859
+ // previous process's, of unknown age, so the kept store is emptied and
860
+ // this launch boots as a cold one does. See __residentBoot.
861
+ //
862
+ // The thunk hands the parsed bundle over and drops this scope's reference,
863
+ // so a cold boot frees it the moment it is adopted rather than holding it
864
+ // through the fill. A kept store that reconciles never takes it.
865
+ const __residentBootStarted = Date.now();
866
+ const __residentBooted = await __residentBoot(
867
+ () => { const __bundle = __MODULE_VFS_BUNDLE; __MODULE_VFS_BUNDLE = null; return __bundle; },
868
+ __MODULE_VFS_CURSOR,
869
+ __supervisor,
870
+ );
871
+ if (__startArgs && __startArgs.diag && __supervisor) {
872
+ // NIMBUS_DEBUG only: what this process holds, for per-process storage measurement.
829
873
  try {
830
- const __synced = await __residentSynchronizeFromSupervisor(__supervisor);
831
- if (__synced.cursor) {
832
- globalThis.__nimbusVfsCursor = { epoch: __synced.cursor.epoch, rev: __synced.cursor.rev };
833
- }
834
- } catch (__e) {
835
- // A failed pass is a smaller resident set, not a dead process: every
836
- // path it did not reach reads exactly as it would have without this
837
- // store. Surfacing beats a silent capability loss.
838
- try { globalThis.__nimbusResidentFillError = (__e && __e.message) || String(__e); } catch {}
839
- }
874
+ const __stats = __residentStats();
875
+ await __supervisor.stderr(new TextEncoder().encode("[nimbus-debug] resident boot " + JSON.stringify({
876
+ ms: Date.now() - __residentBootStarted, ...__stats, failure: __residentBooted.failure || null,
877
+ }) + "\\n"));
878
+ } catch {}
840
879
  }
880
+ __MODULE_VFS_BUNDLE = null;
881
+ // One cursor, not two. The seed above publishes the cursor this SPAWN
882
+ // staged at, which is right for a cold store and stale for a kept one —
883
+ // the cursor the boot returns describes what the rows actually are.
884
+ if (__residentBooted.cursor) {
885
+ globalThis.__nimbusVfsCursor = { epoch: __residentBooted.cursor.epoch, rev: __residentBooted.cursor.rev };
886
+ }
887
+ // No user code runs before the namespace answers: it is the view every
888
+ // synchronous stat, exists and readdir reads (CUTOVER #13). A launch whose
889
+ // namespace cannot be listed fails here, naming why.
890
+ const __namespaceFailure = await __residentRequireNamespace(__supervisor, __residentBooted.failure);
891
+ if (__namespaceFailure) throw new Error(__namespaceFailure);
841
892
  const __vfsBundle = __nimbusResidentBundle;
842
893
  const __pendingIO = [];
843
894
  let __rpcDrops = 0;
@@ -863,23 +914,23 @@ ${VFS_CURSOR_SEED_SOURCE}
863
914
  let stdout = "", stderr = "";
864
915
  let exitCode = 0;
865
916
  const __nimbusDeferProcessExitReport = true;
866
- ${VFS_WRITE_LEDGER_SOURCE}
917
+ ${sources.ledger}
867
918
  const __vfsDirs = {};
868
919
 
869
920
  ${ENTRYPOINT_TIMER_TRACKER}
870
- ${shims}
921
+ ${sources.shims}
871
922
 
872
923
  ${ENTRYPOINT_EVENT_LOOP}
873
924
  ${RESIDENCY_MISS_REPORT}
874
925
 
875
926
  if (__supervisor && !captureOutput) {
876
- __consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
877
- __consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
927
+ __consoleMod.log = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
928
+ __consoleMod.error = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
878
929
  __consoleMod.warn = __consoleMod.error;
879
930
  __consoleMod.info = __consoleMod.log;
880
931
  __consoleMod.debug = __consoleMod.log;
881
- __processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
882
- __processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
932
+ __processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
933
+ __processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
883
934
  }
884
935
 
885
936
  try { globalThis.console = __consoleMod; } catch {}
@@ -964,7 +1015,7 @@ ${RESIDENCY_MISS_REPORT}
964
1015
  if (Number(code ?? 0) === 0) code = 1;
965
1016
  try { await __supervisor.stderr(__nimbusOutEnc.encode(__residencyReport)); } catch {}
966
1017
  }
967
- await __supervisor.reportExit(code, reason || "");
1018
+ await __supervisor.reportExit(code, reason || "", __nimbusStagingMisses(), [...(globalThis.__nimbusProfileStaged || [])]);
968
1019
  __nimbusProcessExitReported = true;
969
1020
  };
970
1021
  const __nimbusReportLifecycleFailure = async (e) => {
@@ -996,6 +1047,11 @@ ${RESIDENCY_MISS_REPORT}
996
1047
  } else {
997
1048
  finalCode = Number(await __nimbusProcessExitPromise);
998
1049
  }
1050
+ if (__nimbusProcessExitCode === null) {
1051
+ __nimbusEmitExit(finalCode);
1052
+ if (__nimbusProcessExitCode !== null) finalCode = __nimbusProcessExitCode;
1053
+ __nimbusProgramStopped = true;
1054
+ }
999
1055
  await __nimbusFlushRuntime();
1000
1056
  await __nimbusReportFinalExit(finalCode, "");
1001
1057
  })().catch(async (e) => {
@@ -1023,9 +1079,7 @@ ${RESIDENCY_MISS_REPORT}
1023
1079
  try { await __supervisor.stderr(__nimbusOutEnc.encode(tail)); } catch {}
1024
1080
  }
1025
1081
  if (exitCode !== 0) {
1026
- if (__supervisor && !__nimbusProcessExitReported) {
1027
- await __supervisor.reportExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
1028
- }
1082
+ await __nimbusReportFinalExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
1029
1083
  throw new Error(stderr || ("long-running node startup exited " + exitCode));
1030
1084
  }
1031
1085
  __nimbusStarted = true;
@@ -1061,7 +1115,13 @@ export class NimbusProcess extends DurableObject {
1061
1115
  if (startArgs) __nimbusStartArgs = startArgs;
1062
1116
  await __nimbusEnsureStarted(this.env, this.ctx, __nimbusStartArgs);
1063
1117
  if (__nimbusAttachedLifecycle) await __nimbusAttachedLifecycle;
1064
- return { ok: true };
1118
+ // What this facet's database holds now, and the cap its store keeps
1119
+ // under, for the session's storage ledger (N18).
1120
+ let databaseSize;
1121
+ try { databaseSize = this.ctx.storage.sql.databaseSize; } catch {}
1122
+ let storageCap = null;
1123
+ try { storageCap = __residentStorageCap(); } catch {}
1124
+ return { ok: true, databaseSize, storageCap };
1065
1125
  }
1066
1126
  async fetch(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
1067
1127
  async handleHttpRequest(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
@@ -1070,13 +1130,33 @@ export class NimbusProcess extends DurableObject {
1070
1130
  modules: bundleSource.modules,
1071
1131
  };
1072
1132
  }
1133
+ /**
1134
+ * What the facet store charges itself to hold one cell (facet-resident-store's
1135
+ * __residentCellCost, which this must match): its bytes (a text cell's UTF-8
1136
+ * length) plus 1%, a row per chunk and head, and a page of slack.
1137
+ */
1138
+ /** What a module map costs the facet's store that adopts it (N18). A denial is a head row only. */
1139
+ function moduleMapStorageBytes(bundle) {
1140
+ let bytes = 0;
1141
+ for (const cell of Object.values(bundle)) {
1142
+ bytes += typeof cell === 'string' || cell instanceof Uint8Array ? residentCellCost(cell) : 2 * LEDGER_ROW_BYTES;
1143
+ }
1144
+ return bytes;
1145
+ }
1146
+ function residentCellCost(cell) {
1147
+ const isText = typeof cell === 'string';
1148
+ const size = isText ? cell.length : cell.byteLength;
1149
+ const limit = isText ? Math.floor(RESIDENT_CHUNK_BYTES / 3) : RESIDENT_CHUNK_BYTES;
1150
+ const chunks = Math.max(1, Math.ceil(size / limit));
1151
+ const bytes = isText ? new TextEncoder().encode(cell).byteLength : size;
1152
+ return Math.ceil(bytes * 1.01) + (chunks + 1) * LEDGER_ROW_BYTES + 4096;
1153
+ }
1073
1154
  /**
1074
1155
  * Drop the raw forms of everything that has been serialized, in place.
1075
1156
  *
1076
- * `bundleSource`, `serializedManifest` and `serializedMetadata` are total
1077
- * encodings of `bundle`, `manifest` and `metadata` — no caller can distinguish
1078
- * a state carrying both from one carrying only the serialized halves, because
1079
- * both facet generators read the serialized halves and nothing else does.
1157
+ * `bundleSource` is a total encoding of `bundle`: no caller can distinguish
1158
+ * a state carrying both from one carrying only the serialized half, because
1159
+ * both facet generators read the serialized half and nothing else does.
1080
1160
  * Holding both doubles the cost of a cached entry for its whole lifetime, and
1081
1161
  * that lifetime spans execs.
1082
1162
  *
@@ -1089,16 +1169,12 @@ export class NimbusProcess extends DurableObject {
1089
1169
  export function releaseSerializedSources(vfsState) {
1090
1170
  if (vfsState.bundleSource)
1091
1171
  vfsState.bundle = {};
1092
- if (vfsState.serializedManifest !== undefined)
1093
- vfsState.manifest = {};
1094
- if (vfsState.serializedMetadata !== undefined)
1095
- vfsState.metadata = {};
1096
1172
  }
1097
1173
  /**
1098
1174
  * Drop the serialized forms once a module map has been generated from them.
1099
1175
  *
1100
- * The generated source is a total encoding of all three: every byte of the
1101
- * bundle expression, the manifest and the metadata is inside it. Holding them
1176
+ * The generated source is a total encoding of it: every byte of the bundle
1177
+ * expression is inside it. Holding them
1102
1178
  * afterwards keeps a second copy of the largest thing this DO builds alive for
1103
1179
  * as long as the facet runs — for pi, 22.7 MB across the ~20 s window in which
1104
1180
  * the isolate was being reset. For the one-shot path that window is the run;
@@ -1115,8 +1191,6 @@ export function releaseSerializedSources(vfsState) {
1115
1191
  */
1116
1192
  export function releaseGeneratedSources(vfsState) {
1117
1193
  vfsState.bundleSource = undefined;
1118
- vfsState.serializedManifest = undefined;
1119
- vfsState.serializedMetadata = undefined;
1120
1194
  vfsState.generatedSourcesReleased = true;
1121
1195
  }
1122
1196
  /**
@@ -1150,7 +1224,9 @@ async function facetVfsBundleSourceFor(vfsState, pacer) {
1150
1224
  * pre-fix readFileString contract).
1151
1225
  */
1152
1226
  async function _readBundleCell(vfs, path) {
1153
- const bytes = await vfs.readFile(path);
1227
+ const bytes = await filesOf(vfs).readBytes(path);
1228
+ if (bytes === null)
1229
+ throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT', path });
1154
1230
  try {
1155
1231
  return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
1156
1232
  }
@@ -1163,6 +1239,12 @@ async function _readBundleCell(vfs, path) {
1163
1239
  * Strings counted as char-length (a slight under-count for non-ASCII
1164
1240
  * but matches the pre-fix behaviour); Uint8Array counted as byteLength.
1165
1241
  */
1242
+ /** A cell's raw bytes: UTF-8 for text, as the file is on disk. */
1243
+ function _bundleCellRawBytes(cell) {
1244
+ if (cell instanceof Uint8Array)
1245
+ return cell.byteLength;
1246
+ return typeof cell === 'string' ? _encodedSourceBytes(cell) : 0;
1247
+ }
1166
1248
  function _bundleCellLength(cell) {
1167
1249
  return typeof cell === 'string' ? cell.length : cell.byteLength;
1168
1250
  }
@@ -1178,13 +1260,10 @@ function _bundleCellLength(cell) {
1178
1260
  * BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES); omit it for budget-only checks.
1179
1261
  */
1180
1262
  async function _bundleAdmits(vfs, path, budgetState, perFileCeiling) {
1181
- let size;
1182
- try {
1183
- size = (await vfs.lstat(path)).size;
1184
- }
1185
- catch {
1263
+ const stat = await filesOf(vfs).lstat(path).catch(() => null);
1264
+ if (stat === null)
1186
1265
  return false;
1187
- }
1266
+ const size = stat.size;
1188
1267
  if (perFileCeiling !== undefined && size > perFileCeiling)
1189
1268
  return false;
1190
1269
  return budgetState.totalBytes + size <= VFS_BUNDLE_MAX_BYTES;
@@ -1218,8 +1297,6 @@ function retainedVfsStateBytes(state) {
1218
1297
  for (const moduleSource of Object.values(source.modules))
1219
1298
  bytes += moduleSource.length;
1220
1299
  }
1221
- bytes += state.serializedManifest?.length ?? 0;
1222
- bytes += state.serializedMetadata?.length ?? 0;
1223
1300
  return bytes;
1224
1301
  }
1225
1302
  /**
@@ -1299,8 +1376,8 @@ function _jsonEncodedBytes(value) {
1299
1376
  }
1300
1377
  return bytes;
1301
1378
  }
1302
- /** `{"bundle":` + `,"manifest":` + `}` — the frame around the two members. */
1303
- const ENCODED_PAYLOAD_FRAME_BYTES = 23;
1379
+ /** `{"bundle":` + `}`: the frame around the one member. */
1380
+ const ENCODED_PAYLOAD_FRAME_BYTES = 11;
1304
1381
  /**
1305
1382
  * Running UTF-8 byte length of `JSON.stringify({ bundle, manifest })`,
1306
1383
  * accumulated one cell at a time.
@@ -1316,10 +1393,9 @@ const ENCODED_PAYLOAD_FRAME_BYTES = 23;
1316
1393
  * the total is a sum of independent per-cell terms: exact, incremental, and
1317
1394
  * never holding more than one cell's worth of scratch.
1318
1395
  */
1319
- export function encodedBundleSize(bundle, manifest) {
1396
+ export function encodedBundleSize(bundle) {
1320
1397
  const cells = new Map();
1321
1398
  let cellSum = 0;
1322
- const manifestBytes = _jsonEncodedBytes(manifest);
1323
1399
  const self = {
1324
1400
  add(path, cell) {
1325
1401
  if (cells.has(path))
@@ -1337,7 +1413,7 @@ export function encodedBundleSize(bundle, manifest) {
1337
1413
  },
1338
1414
  get bytes() {
1339
1415
  const separators = Math.max(0, cells.size - 1);
1340
- return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators + manifestBytes;
1416
+ return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators;
1341
1417
  },
1342
1418
  };
1343
1419
  for (const [path, cell] of Object.entries(bundle))
@@ -1500,7 +1576,13 @@ function _inlineBundleSourceBytes(bundle) {
1500
1576
  * the merge expression concatenates those fragments back to the original
1501
1577
  * string or Uint8Array before module precompilation begins.
1502
1578
  */
1503
- export async function buildFacetVfsBundleSource(bundle, forceSideModules = false, pacer) {
1579
+ export async function buildFacetVfsBundleSource(bundle, forceSideModules = false, pacer, { consume = false } = {}) {
1580
+ // \`consume\`: the caller releases the raw cells right after (a launch's
1581
+ // build), so each is taken as its side module is serialized and the map is
1582
+ // never held twice. Side modules always: an inline expression is copied
1583
+ // again into the runner's own source.
1584
+ if (consume)
1585
+ forceSideModules = true;
1504
1586
  // Size the inline form before building it. A bundle that will be split has
1505
1587
  // no use for the whole-bundle expression, and building one to read its
1506
1588
  // length off cost a second full copy of the largest string this DO makes.
@@ -1551,46 +1633,55 @@ export async function buildFacetVfsBundleSource(bundle, forceSideModules = false
1551
1633
  }
1552
1634
  return pieces;
1553
1635
  }
1554
- const chunks = [];
1555
- let chunk = {};
1556
- let estimatedBytes = 0;
1557
- function flushChunk() {
1558
- if (Object.keys(chunk).length === 0)
1559
- return;
1560
- chunks.push(chunk);
1561
- chunk = {};
1562
- estimatedBytes = 0;
1563
- }
1564
- for (const [path, cell] of Object.entries(bundle)) {
1565
- for (const [piecePath, pieceCell] of splitCell(path, cell)) {
1566
- const pieceBytes = sourceBytes(piecePath, pieceCell);
1567
- if (piecePath in chunk
1568
- || (estimatedBytes > 0 && estimatedBytes + pieceBytes > maxModuleBytes)) {
1569
- flushChunk();
1570
- }
1571
- chunk[piecePath] = pieceCell;
1572
- estimatedBytes += pieceBytes;
1573
- }
1574
- }
1575
- flushChunk();
1576
1636
  const modules = {};
1577
1637
  const imports = [];
1578
1638
  const aliases = [];
1579
- for (let index = 0; index < chunks.length; index++) {
1639
+ let chunk = {};
1640
+ let chunkPaths = [];
1641
+ let estimatedBytes = 0;
1642
+ // A chunk becomes its side module the moment it is full, so the cells it
1643
+ // carries and their serialization are never both held for the whole map:
1644
+ // consumed, a cell leaves the bundle as its module is written.
1645
+ async function flushChunk() {
1646
+ // Pieces of a split cell fill chunks before the cell's path is complete,
1647
+ // so emptiness is the chunk's own, not its finished paths'.
1648
+ if (estimatedBytes === 0)
1649
+ return;
1650
+ const index = aliases.length;
1580
1651
  const moduleName = `${FACET_VFS_MODULE_PREFIX}${index}.js`;
1581
1652
  const alias = `__nimbusVfsBundle${index}`;
1582
- const source = _facetBundleModuleSource(chunks[index]);
1653
+ const source = _facetBundleModuleSource(chunk);
1583
1654
  if (_encodedSourceBytes(source) > BUNDLE_MAX_ENCODED_BYTES) {
1584
1655
  throw new Error(`Nimbus: generated VFS side module exceeds encoded limit: ${moduleName}`);
1585
1656
  }
1586
1657
  modules[moduleName] = source;
1587
1658
  imports.push(`import ${alias} from "${moduleName}";`);
1588
1659
  aliases.push(alias);
1660
+ if (consume)
1661
+ for (const path of chunkPaths)
1662
+ delete bundle[path];
1663
+ chunk = {};
1664
+ chunkPaths = [];
1665
+ estimatedBytes = 0;
1589
1666
  // Each side module is an independent serialization of its own cells, so
1590
1667
  // the turn may end between any two of them.
1591
1668
  if (pacer)
1592
1669
  await pacer.spend(source.length);
1593
1670
  }
1671
+ for (const path of Object.keys(bundle)) {
1672
+ const cell = bundle[path];
1673
+ for (const [piecePath, pieceCell] of splitCell(path, cell)) {
1674
+ const pieceBytes = sourceBytes(piecePath, pieceCell);
1675
+ if (piecePath in chunk
1676
+ || (estimatedBytes > 0 && estimatedBytes + pieceBytes > maxModuleBytes)) {
1677
+ await flushChunk();
1678
+ }
1679
+ chunk[piecePath] = pieceCell;
1680
+ estimatedBytes += pieceBytes;
1681
+ }
1682
+ chunkPaths.push(path);
1683
+ }
1684
+ await flushChunk();
1594
1685
  const expression = `(function(__parts){const __out={};for(const __part of __parts){` +
1595
1686
  `for(const [__k,__v] of Object.entries(__part)){const __prev=__out[__k];` +
1596
1687
  `if(__prev===undefined){__out[__k]=__v;}` +
@@ -1645,174 +1736,47 @@ function _fnv1a(s) {
1645
1736
  return h.toString(16);
1646
1737
  }
1647
1738
  const MANIFEST_MAX_DEPTH = 12;
1648
- /**
1649
- * Build the manifest pass — uncapped path→child-names map. UNCHANGED
1650
- * from W2.5b; this is the W2.5b root-cause fix and continues to keep
1651
- * fs.readdirSync / fs.statSync honest regardless of which subset of
1652
- * file CONTENT we ship.
1653
- */
1654
- async function buildManifest(vfs, cwd, scriptPath) {
1655
- const manifest = {};
1656
- async function walk(dirPath, depth = 0) {
1657
- if (depth > MANIFEST_MAX_DEPTH)
1658
- return;
1659
- const stripped = dirPath.replace(/^\/+/, '');
1660
- if (stripped in manifest)
1661
- return;
1662
- let entries;
1663
- try {
1664
- entries = (await vfs.readdir(stripped));
1665
- }
1666
- catch {
1667
- return;
1668
- }
1669
- manifest[stripped] = entries.map((e) => e.name);
1670
- for (const entry of entries) {
1671
- if (entry.type === 'directory') {
1672
- const childPath = stripped ? stripped + '/' + entry.name : entry.name;
1673
- (await walk(childPath, depth + 1));
1674
- }
1675
- }
1676
- }
1677
- const cwdStripped = cwd.replace(/^\/+/, '');
1678
- // ── The path from the root down to the working directory ──────────────
1679
- //
1680
- // One level each, before the deep walks, so that every directory on that
1681
- // chain is ENUMERATED rather than merely mentioned. It is what makes a
1682
- // synchronous "not there" honest for the shape programs probe most: node's
1683
- // resolver walks upward asking for `<dir>/node_modules` at every level, and
1684
- // a config lookup asks for a dotfile directory in $HOME. Those paths mostly
1685
- // do not exist, and answering that requires having listed the directory
1686
- // they would be in — otherwise the only honest answer is a refusal, and the
1687
- // resolver pays a round trip per rung to hear it.
1688
- //
1689
- // Cheap by construction: one readdir per component, names only, and the
1690
- // recursion below each stops immediately.
1691
- {
1692
- const segments = cwdStripped ? cwdStripped.split('/') : [];
1693
- for (let i = 0; i < segments.length; i++) {
1694
- (await walk(segments.slice(0, i).join('/'), MANIFEST_MAX_DEPTH));
1695
- }
1696
- }
1697
- (await walk(cwdStripped, 0));
1698
- const nmDir = cwdStripped + '/node_modules';
1699
- if ((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))) {
1700
- (await walk(nmDir, 0));
1701
- }
1702
- // ── Bin-target package + hoisted dependency roots ─────────────────────
1703
- //
1704
- // When the entry script lives in a node_modules outside cwd (npx-cache
1705
- // packages, globally-installed bins, etc.), buildManifest's cwd walk
1706
- // misses the package's sibling files. The bin's index.js gets
1707
- // require-walked + greedy-added to the BUNDLE, but the MANIFEST
1708
- // (which is the source of truth for `fs.readdirSync`) was empty for
1709
- // those paths — so `readdirSync('/tmp/.npx-cache/.../template-X')`
1710
- // returned [] and `create-vite` scaffolded zero files.
1711
- //
1712
- // Walk the innermost `node_modules/<pkg>/` of `scriptPath` so its
1713
- // entire package tree is enumerable via readdir. Also walk that
1714
- // `node_modules` directory itself: global npm bins resolve hoisted
1715
- // transitive dependencies as siblings of the bin's own package.
1716
- // Bounded by MANIFEST_MAX_DEPTH; same depth budget as the cwd walk.
1717
- if (scriptPath) {
1718
- const sp = scriptPath.replace(/^\/+/, '');
1719
- const segs = sp.split('/');
1720
- let nmIdx = -1;
1721
- for (let i = segs.length - 1; i >= 0; i--) {
1722
- if (segs[i] === 'node_modules') {
1723
- nmIdx = i;
1724
- break;
1725
- }
1726
- }
1727
- if (nmIdx >= 0) {
1728
- const isScoped = segs[nmIdx + 1]?.startsWith('@');
1729
- const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
1730
- if (pkgEnd <= segs.length) {
1731
- const pkgRoot = segs.slice(0, pkgEnd).join('/');
1732
- if ((await vfs.exists(pkgRoot)) && (await vfs.isDirectory(pkgRoot))) {
1733
- (await walk(pkgRoot, 0));
1734
- }
1735
- }
1736
- const nodeModulesRoot = segs.slice(0, nmIdx + 1).join('/');
1737
- if ((await vfs.exists(nodeModulesRoot)) && (await vfs.isDirectory(nodeModulesRoot))) {
1738
- (await walk(nodeModulesRoot, 0));
1739
- }
1740
- }
1741
- }
1742
- return manifest;
1743
- }
1744
- async function buildVfsMetadata(vfs, manifest, bundle) {
1745
- const paths = new Set(Object.keys(bundle).filter((path) => compiledCellPath(path) === null));
1746
- for (const [directory, children] of Object.entries(manifest)) {
1747
- paths.add(directory);
1748
- for (const child of children) {
1749
- paths.add(directory ? `${directory}/${child}` : child);
1750
- }
1751
- }
1752
- const metadata = {};
1753
- for (const path of paths) {
1754
- try {
1755
- const stat = await vfs.authority.stat(path, { followSymlinks: false });
1756
- if (!stat)
1757
- continue;
1758
- metadata[path] = {
1759
- type: stat.type,
1760
- size: stat.size,
1761
- mode: stat.mode,
1762
- uid: stat.uid,
1763
- gid: stat.gid,
1764
- };
1765
- }
1766
- catch {
1767
- // The credentialed lookup is authoritative; inaccessible ancestors do
1768
- // not reveal whether a leaf exists.
1769
- }
1770
- }
1771
- return metadata;
1772
- }
1773
- async function addUnreadableDenialCells(vfs, bundle, metadata) {
1774
- for (const [path, stat] of Object.entries(metadata)) {
1775
- if (stat.type === 'directory' || path in bundle)
1776
- continue;
1777
- try {
1778
- (await vfs.access(path, 0o4));
1779
- }
1780
- catch (error) {
1781
- if (typeof error === 'object'
1782
- && error !== null
1783
- && 'code' in error
1784
- && error.code === 'EACCES') {
1785
- bundle[path] = { error: 'EACCES' };
1786
- }
1787
- }
1788
- }
1789
- }
1790
- /**
1791
- * Greedy-oversample every installed package's main entry. The static
1792
- * prefetch via require-resolver covers the require() chain literally
1793
- * present in source; greedy oversampling adds a safety net for dynamic
1794
- * patterns the regex misses (jest/`bindings`/`import-local` style
1795
- * computed-path requires). Bounded to package.json + 1 main-entry file
1796
- * per package — sub-agent §Q3 quantified the worst-case cumulative
1797
- * budget impact (~322 KiB for fastify, ~1.7 MiB for ts-jest).
1798
- *
1799
- * `requiredPaths` is the static require closure. A package the closure
1800
- * already reached — but reached only through a SUBPATH — has its main entry
1801
- * skipped; see `mainIsSpeculative`.
1802
- */
1803
- // can verify the hash-chunk + shared/ oversample directly. Pre-X.5-C this
1804
- // was a file-local helper. Adding the named export is a pure surface
1805
- // addition — no callers other than buildPrefetchBundle (same file) and
1806
- // the new probe.
1807
- export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set()) {
1739
+ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set(), options = {}) {
1808
1740
  let added = 0;
1741
+ const groups = [];
1742
+ const bound = Math.min(options.maxBundleBytes ?? VFS_BUNDLE_MAX_BYTES, VFS_BUNDLE_MAX_BYTES);
1743
+ const independent = new Set(Object.keys(bundle));
1744
+ let rawBytes = 0;
1745
+ for (const cell of Object.values(bundle))
1746
+ rawBytes += _bundleCellRawBytes(cell);
1747
+ if (rawBytes >= bound)
1748
+ return { added, groups };
1809
1749
  const cwdStripped = cwd.replace(/^\/+/, '');
1810
1750
  const nmDir = cwdStripped + '/node_modules';
1811
- if (!((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))))
1812
- return { added };
1751
+ if (!((await filesOf(vfs).exists(nmDir)) && (await filesOf(vfs).isDirectory(nmDir))))
1752
+ return { added, groups };
1813
1753
  const exts = ['', '.js', '.cjs', '.mjs', '/index.js', '/index.cjs'];
1814
1754
  async function addOne(path) {
1815
1755
  const stripped = path.replace(/^\/+/, '');
1756
+ if ((isBundleModuleCandidate(stripped) || stripped.endsWith('.cjs')) && !isNativeBinPath(stripped)) {
1757
+ if (independent.has(stripped))
1758
+ return false;
1759
+ if (bundle[stripped] === undefined && !(await _bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)))
1760
+ return false;
1761
+ const closure = await prefetchForRequire(requireFsOverBridge(vfs), '', stripped.slice(0, stripped.lastIndexOf('/')), '/' + stripped, bound, options.pacer?.spend.bind(options.pacer), {
1762
+ purpose: 'dependency-closure', held: bundle,
1763
+ maxAdditionalBytes: Math.max(0, bound - rawBytes),
1764
+ maxAdditionalFiles: Math.max(0, VFS_BUNDLE_MAX_FILES - budgetState.fileCount),
1765
+ });
1766
+ if ('kind' in closure || closure.bundle[stripped] === undefined)
1767
+ return false;
1768
+ for (const [member, cell] of Object.entries(closure.bundle)) {
1769
+ if (bundle[member] !== undefined)
1770
+ continue;
1771
+ bundle[member] = cell;
1772
+ rawBytes += _bundleCellRawBytes(cell);
1773
+ budgetState.totalBytes += _bundleCellLength(cell);
1774
+ budgetState.fileCount++;
1775
+ added++;
1776
+ }
1777
+ groups.push({ root: stripped, members: new Set(Object.keys(closure.bundle)) });
1778
+ return true;
1779
+ }
1816
1780
  if (stripped in bundle)
1817
1781
  return false;
1818
1782
  if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
@@ -1820,7 +1784,7 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1820
1784
  if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
1821
1785
  return false;
1822
1786
  try {
1823
- if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
1787
+ if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
1824
1788
  return false;
1825
1789
  // This is a guess at what a program might require, and the same
1826
1790
  // per-file ceiling the entry-package walk applies bounds it: a
@@ -1828,8 +1792,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1828
1792
  // `lib/typescript.js` beside the `lib/tsc.js` that actually runs), and
1829
1793
  // one guess must not spend a third of the budget — and a third of the
1830
1794
  // supervisor's headroom — on every invocation that never reads it.
1831
- // Anything the program really requires arrives through the closure,
1832
- // which is uncapped.
1795
+ // Literal dependencies use the closure's byte allowance, not this
1796
+ // speculative-root per-file ceiling.
1833
1797
  if (!(await _bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)))
1834
1798
  return false;
1835
1799
  // A guess must not be a native binary. Nothing in a Workers isolate can
@@ -1839,14 +1803,19 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1839
1803
  // Measured: freeing 1.3 MiB of rollup let `@napi-rs/lzma-linux-x64-gnu`'s
1840
1804
  // 1,445,448-byte `.node` in, which the size guard had been evicting.
1841
1805
  // Only the guess is filtered; a path the closure requires is untouched.
1842
- if (stripped.endsWith('.node') || stripped.endsWith('.exe'))
1806
+ if (isNativeBinPath(stripped))
1843
1807
  return false;
1844
1808
  // hardening-r5: preserve binary content as Uint8Array.
1809
+ const stat = await filesOf(vfs).stat(stripped);
1810
+ if (stat === null || rawBytes + stat.size > bound)
1811
+ return false;
1845
1812
  const content = (await _readBundleCell(vfs, stripped));
1846
1813
  const cellLen = _bundleCellLength(content);
1847
- if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
1814
+ const raw = _bundleCellRawBytes(content);
1815
+ if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES || rawBytes + raw > bound)
1848
1816
  return false;
1849
1817
  bundle[stripped] = content;
1818
+ rawBytes += raw;
1850
1819
  budgetState.totalBytes += cellLen;
1851
1820
  budgetState.fileCount++;
1852
1821
  added++;
@@ -1856,26 +1825,6 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1856
1825
  return false;
1857
1826
  }
1858
1827
  }
1859
- // X.5-C Fix #2 helper: walk a (possibly nested) exports value and
1860
- // collect every string-leaf path. unbuild-shaped packages like pathe
1861
- // nest two deep — `exports."."`.{require,import}.{types,default} —
1862
- // and the previous one-level loop only caught the inner string leaves
1863
- // when default was at the top, missing the unbuild shape entirely.
1864
- function collectExportLeaves(node, out) {
1865
- if (typeof node === 'string') {
1866
- out.add(node);
1867
- return;
1868
- }
1869
- if (!node || typeof node !== 'object')
1870
- return;
1871
- // Order matters for the "most likely usable" leaf: prefer require
1872
- // (most CJS-friendly), then default, then node, then import. We add
1873
- // ALL of them to the candidate set — addPkgEntry will probe each.
1874
- for (const k of ['require', 'node', 'default', 'import']) {
1875
- if (k in node)
1876
- collectExportLeaves(node[k], out);
1877
- }
1878
- }
1879
1828
  /**
1880
1829
  * Whether guessing at `pkgDir`'s main entry is still a guess.
1881
1830
  *
@@ -1907,47 +1856,52 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1907
1856
  return !reached;
1908
1857
  }
1909
1858
  async function addPkgEntry(pkgDir) {
1910
- (await addOne(pkgDir + '/package.json'));
1859
+ const manifest = (pkgDir + '/package.json').replace(/^\/+/, '');
1860
+ if (bundle[manifest] === undefined && !(await addOne(manifest)))
1861
+ return;
1911
1862
  // A package the closure reached keeps exactly what it reached; only an
1912
1863
  // unreached one gets the guess below.
1913
1864
  if (!mainIsSpeculative(pkgDir))
1914
1865
  return;
1915
- let meta;
1866
+ // Malformed JSON, or a package whose entry fields the resolver cannot
1867
+ // read (`main: 7`, `exports: true`) drops that field and keeps the rest;
1868
+ // unparseable JSON is a package with no declared entry. Either way the
1869
+ // next package is unaffected.
1870
+ let pkg;
1916
1871
  try {
1917
- meta = JSON.parse((await vfs.readFileString(pkgDir + '/package.json')));
1872
+ const text = bundle[manifest];
1873
+ pkg = typeof text === 'string' ? parseResolvablePackageJson(text) : null;
1918
1874
  }
1919
1875
  catch {
1920
- meta = null;
1921
- }
1922
- const candidates = new Set();
1923
- if (meta) {
1924
- if (typeof meta.main === 'string')
1925
- candidates.add(meta.main);
1926
- if (typeof meta.module === 'string')
1927
- candidates.add(meta.module);
1928
- const exp = meta.exports;
1929
- if (typeof exp === 'string')
1930
- candidates.add(exp);
1931
- else if (exp && typeof exp === 'object') {
1932
- const dot = exp['.'];
1933
- // X.5-C Fix #2: walk nested condition trees recursively. Without
1934
- // this, packages with two-level exports (pathe, magic-string,
1935
- // most unbuild-emitted libs) miss their actual entry leaf and
1936
- // greedyAddMainEntries falls back to /index.js probing — which
1937
- // doesn't exist for those packages.
1938
- collectExportLeaves(dot, candidates);
1939
- }
1940
- }
1941
- if (candidates.size === 0)
1942
- candidates.add('index.js');
1876
+ pkg = null;
1877
+ }
1878
+ // The root entry a bare require of this package resolves to, in the
1879
+ // order the runtime resolves it (require-resolver.ts resolvePkgSubpathEx):
1880
+ // exports under the CJS conditions, then the ESM ones, then legacy main,
1881
+ // then an index probe, each tried until one lands. A denied root
1882
+ // (`"." : null`) resolves to nothing and falls through the same way. A
1883
+ // hand-rolled walk of `exports["."]` read nothing from a root conditional
1884
+ // map (on-change@6: `{ types, default }`, no ".", no main) and guessed
1885
+ // `index.js`, which does not exist.
1886
+ const candidates = [];
1887
+ if (pkg) {
1888
+ let entry = resolvePackageEntry(pkg, '.', DEFAULT_CJS_CONDITIONS);
1889
+ if (entry === null && pkg.exports != null)
1890
+ entry = resolvePackageEntry(pkg, '.', DEFAULT_ESM_CONDITIONS);
1891
+ if (entry !== null)
1892
+ candidates.push(entry);
1893
+ if (pkg.main !== undefined)
1894
+ candidates.push(pkg.main);
1895
+ }
1896
+ candidates.push('index.js');
1943
1897
  for (const rel of candidates) {
1944
1898
  const norm = rel.replace(/^\.\//, '');
1945
1899
  const base = pkgDir + '/' + norm;
1946
1900
  let landed = false;
1947
1901
  const tries = /\.[a-z]+$/.test(norm) ? [base] : exts.map((e) => base + e);
1948
1902
  for (const candidate of tries) {
1949
- if ((await vfs.exists(candidate.replace(/^\/+/, ''))) &&
1950
- !(await vfs.isDirectory(candidate.replace(/^\/+/, '')))) {
1903
+ if ((await filesOf(vfs).exists(candidate.replace(/^\/+/, ''))) &&
1904
+ !(await filesOf(vfs).isDirectory(candidate.replace(/^\/+/, '')))) {
1951
1905
  if ((await addOne(candidate))) {
1952
1906
  landed = true;
1953
1907
  break;
@@ -1965,8 +1919,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1965
1919
  // The static walker cannot discover computed hash-chunk imports, so
1966
1920
  // the greedy oversample is the safety net for their reachability.
1967
1921
  const entryDir = base.replace(/\/[^/]+$/, '');
1968
- try {
1969
- const sibs = (await vfs.readdir(entryDir));
1922
+ {
1923
+ const sibs = await (async () => vfs.readdir(entryDir))().catch(() => []);
1970
1924
  for (const sib of sibs) {
1971
1925
  if (sib.type !== 'file')
1972
1926
  continue;
@@ -1994,8 +1948,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
1994
1948
  // typical shared/ dir returns 1-5 files.
1995
1949
  const sharedDir = entryDir + '/shared';
1996
1950
  const sharedStripped = sharedDir.replace(/^\/+/, '');
1997
- if ((await vfs.exists(sharedStripped)) && (await vfs.isDirectory(sharedStripped))) {
1998
- for (const sh of (await vfs.readdir(sharedDir))) {
1951
+ if (await (async () => filesOf(vfs).isDirectory(sharedStripped))().catch(() => false)) {
1952
+ for (const sh of await (async () => vfs.readdir(sharedDir))().catch(() => [])) {
1999
1953
  if (sh.type !== 'file')
2000
1954
  continue;
2001
1955
  if (!/\.(cjs|mjs|js)$/.test(sh.name))
@@ -2004,22 +1958,20 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
2004
1958
  }
2005
1959
  }
2006
1960
  }
2007
- catch { /* unreadable dir — drop sibling oversample, entry
2008
- file is enough */
2009
- }
2010
1961
  break;
2011
1962
  }
2012
1963
  }
2013
1964
  }
2014
1965
  for (const pkgDir of (await speculativePackageDirs(vfs, cwdStripped, bundle)))
2015
1966
  (await addPkgEntry(pkgDir));
2016
- return { added };
1967
+ return { added, groups };
2017
1968
  }
2018
1969
  /**
2019
- * The packages a computed `require(name)` inside the program can plausibly
2020
- * name: the project root's own runtime `dependencies`, plus every package
2021
- * ONE `dependencies` hop from a package that already owns a file in the
2022
- * static closure. Never devDependencies, never a second hop.
1970
+ * The packages a name resolved at runtime — a computed `require(name)`, or a
1971
+ * resolver call like exsolve's `resolveModulePath(name, { from: rootDir })` —
1972
+ * can plausibly reach: every package ONE `dependencies` hop from a package
1973
+ * the project itself declares or that owns a file in the static closure, plus
1974
+ * those roots themselves. Never devDependencies, never a second hop.
2023
1975
  *
2024
1976
  * Unbounded, the greedy oversample read every installed package's main:
2025
1977
  * for `node -e "import('got')"` in got's repo — a one-file static closure —
@@ -2031,11 +1983,26 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
2031
1983
  * tree. Computed requires almost always target a declared runtime
2032
1984
  * dependency of the package doing the requiring, so the bound is one hop
2033
1985
  * over `dependencies` only. Directories, sorted for a stable bundle.
1986
+ *
1987
+ * The project's own dependencies are hop ROOTS and not merely members,
1988
+ * because a bin runs inside the project and resolves from the project root,
1989
+ * where what it names is its host framework's runtime peer rather than
1990
+ * anything its own package declares. Measured on staging, `nuxt dev` on a
1991
+ * `nuxi init` project: npm points `node_modules/.bin/nuxt` at
1992
+ * `@nuxt/cli/bin/nuxi.mjs`, so `@nuxt/cli` owns the entry; `@nuxt/kit` is a
1993
+ * devDependency of `@nuxt/cli` and a dependency of `nuxt`, which the project
1994
+ * declares. Admitting `nuxt` without hopping from it left
1995
+ * `@nuxt/kit/package.json` unstaged, `readFileSync` raised EAGAIN inside
1996
+ * exsolve — which swallows every error — and the CLI reported
1997
+ * `Cannot resolve module "@nuxt/kit" (from: /home/user/nuxt-probe/mvp/)`.
1998
+ * One hop from each project dependency is what the project's own node_modules
1999
+ * was hoisted for; it is the same edge kind and the same single level the
2000
+ * owner hop already spends.
2034
2001
  */
2035
2002
  export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
2036
2003
  const runtimeDeps = async (pkgJsonPath) => {
2037
2004
  try {
2038
- const meta = JSON.parse((await vfs.readFileString(pkgJsonPath)));
2005
+ const meta = JSON.parse((await filesOf(vfs).readFileString(pkgJsonPath)));
2039
2006
  const names = new Set();
2040
2007
  for (const field of ['dependencies', 'optionalDependencies']) {
2041
2008
  const deps = meta?.[field];
@@ -2064,7 +2031,7 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
2064
2031
  let dir = fromDir;
2065
2032
  for (;;) {
2066
2033
  const candidate = dir + '/node_modules/' + name;
2067
- if ((await vfs.exists(candidate + '/package.json')))
2034
+ if ((await filesOf(vfs).exists(candidate + '/package.json')))
2068
2035
  return candidate;
2069
2036
  const idx = dir.lastIndexOf('/');
2070
2037
  if (idx <= 0)
@@ -2073,14 +2040,24 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
2073
2040
  }
2074
2041
  };
2075
2042
  const reached = new Set();
2043
+ const hopped = new Set();
2044
+ // Returns what this hop landed on, so a root can be hopped from without
2045
+ // reading its manifest twice and without the caller tracking the edges.
2076
2046
  const hop = async (fromDir) => {
2047
+ if (hopped.has(fromDir))
2048
+ return [];
2049
+ hopped.add(fromDir);
2050
+ const landed = [];
2077
2051
  for (const name of (await runtimeDeps(fromDir + '/package.json'))) {
2078
2052
  const dir = (await resolveDir(name, fromDir));
2079
- if (dir !== null)
2080
- reached.add(dir);
2053
+ if (dir === null)
2054
+ continue;
2055
+ reached.add(dir);
2056
+ landed.push(dir);
2081
2057
  }
2058
+ return landed;
2082
2059
  };
2083
- (await hop(cwdStripped));
2060
+ const projectDeps = (await hop(cwdStripped));
2084
2061
  const owners = new Set();
2085
2062
  for (const path of Object.keys(bundle)) {
2086
2063
  const owner = ownerOf(path);
@@ -2091,6 +2068,12 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
2091
2068
  reached.add(owner);
2092
2069
  (await hop(owner));
2093
2070
  }
2071
+ // The second kind of root: what the project declares. A bin resolving from
2072
+ // the project root reaches these packages' dependencies, and nothing else
2073
+ // in this function would — the project's dependency owns no staged file
2074
+ // when the bin that runs belongs to a sibling package.
2075
+ for (const dir of projectDeps)
2076
+ (await hop(dir));
2094
2077
  return [...reached].sort();
2095
2078
  }
2096
2079
  /**
@@ -2159,7 +2142,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
2159
2142
  if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
2160
2143
  return false;
2161
2144
  try {
2162
- if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
2145
+ if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
2163
2146
  return false;
2164
2147
  if (!(await _bundleAdmits(vfs, stripped, budgetState)))
2165
2148
  return false;
@@ -2255,7 +2238,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
2255
2238
  * (0, path_1.resolve)(__dirname, '../../../.ts-jest-digest'), 'utf8');
2256
2239
  *
2257
2240
  * The install pipeline writes `.ts-jest-digest` to VFS correctly
2258
- * (manifest pass at buildManifest enumerates it). But the runtime
2241
+ * (the namespace lists it). But the runtime
2259
2242
  * fs shim's readFileSync (`src/node-shims.ts:202-215`) consults
2260
2243
  * `__vfsBundle` only, and none of the existing bundle-population
2261
2244
  * passes — `prefetchForRequire` (require-graph), `greedyAddMainEntries`
@@ -2334,7 +2317,7 @@ export async function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, bud
2334
2317
  if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
2335
2318
  return false;
2336
2319
  try {
2337
- if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
2320
+ if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
2338
2321
  return false;
2339
2322
  if (!(await _bundleAdmits(vfs, stripped, budgetState)))
2340
2323
  return false;
@@ -2523,13 +2506,10 @@ export async function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState,
2523
2506
  wasmPaths.push(child);
2524
2507
  if (bundle[child] !== undefined)
2525
2508
  continue;
2526
- let size;
2527
- try {
2528
- size = (await vfs.lstat(child)).size;
2529
- }
2530
- catch {
2509
+ const stat = await filesOf(vfs).lstat(child).catch(() => null);
2510
+ if (stat === null)
2531
2511
  continue;
2532
- }
2512
+ const size = stat.size;
2533
2513
  if (size > BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)
2534
2514
  continue;
2535
2515
  candidates.push({ path: child, size });
@@ -2595,13 +2575,9 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
2595
2575
  for (const path of unstagedPaths) {
2596
2576
  if (byPath.has(path))
2597
2577
  continue;
2598
- let bytes;
2599
- try {
2600
- bytes = await vfs.readFile(path);
2601
- }
2602
- catch {
2578
+ const bytes = await filesOf(vfs).readBytes(path).catch(() => null);
2579
+ if (bytes === null)
2603
2580
  continue;
2604
- }
2605
2581
  byPath.set(path, { vfsPath: '/' + stripLeadingSlashes(path), digest: wasmImageDigest(bytes) });
2606
2582
  }
2607
2583
  return [...byPath.values()];
@@ -2621,21 +2597,29 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
2621
2597
  * Admitted smallest-first for the same reason as the entry-package walk: the
2622
2598
  * budget is shared, so ordering by size maximizes the number of misses a
2623
2599
  * fixed number of bytes repairs.
2600
+ *
2601
+ * `room` is what the snapshot bound leaves after what is already committed
2602
+ * (the static closure, and for learned entries this session's own misses), in
2603
+ * raw bytes: evidence fills that room and never pushes the snapshot past the
2604
+ * bound, so it can never be the reason a launch fails. `bytes` is what this
2605
+ * call staged, in the same unit.
2624
2606
  */
2625
- export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState) {
2607
+ export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState, room = Infinity, pacer) {
2626
2608
  if (!observed || observed.size === 0)
2627
- return { added: 0 };
2609
+ return { added: 0, bytes: 0 };
2610
+ let bytes = 0;
2628
2611
  const candidates = [];
2629
2612
  for (const path of observed) {
2630
- if (path === '' || bundle[path] !== undefined)
2613
+ if (path === '')
2631
2614
  continue;
2632
- let stat;
2633
- try {
2634
- stat = (await vfs.lstat(path));
2635
- }
2636
- catch {
2615
+ // Already staged, but evictable: the evidence is what makes it required.
2616
+ if (bundle[path] !== undefined) {
2617
+ requiredPaths.add(path);
2637
2618
  continue;
2638
2619
  }
2620
+ const stat = await filesOf(vfs).lstat(path).catch(() => null);
2621
+ if (stat === null)
2622
+ continue;
2639
2623
  if (stat.type === 'directory')
2640
2624
  continue;
2641
2625
  candidates.push({ path, size: stat.size });
@@ -2657,13 +2641,48 @@ export async function addObservedReads(vfs, observed, bundle, requiredPaths, bud
2657
2641
  const cellLen = _bundleCellLength(content);
2658
2642
  if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
2659
2643
  continue;
2644
+ const raw = _bundleCellRawBytes(content);
2645
+ if (bytes + raw > room)
2646
+ continue;
2660
2647
  bundle[candidate.path] = content;
2661
2648
  requiredPaths.add(candidate.path);
2649
+ bytes += raw;
2662
2650
  budgetState.totalBytes += cellLen;
2663
2651
  budgetState.fileCount++;
2664
2652
  added++;
2665
2653
  }
2666
- return { added };
2654
+ // A module brings its static imports: learned one miss per launch, nuxt's
2655
+ // on-change alone would have cost a relaunch for each of its files.
2656
+ for (const path of observed) {
2657
+ if (!/\.[cm]?js$/.test(path) || bundle[path] === undefined)
2658
+ continue;
2659
+ const closure = await prefetchForRequire(requireFsOverBridge(vfs), '', path.slice(0, path.lastIndexOf('/')), '/' + path, undefined, pacer?.spend.bind(pacer));
2660
+ if ('kind' in closure)
2661
+ continue;
2662
+ for (const [dep, content] of Object.entries(closure.bundle)) {
2663
+ if (closure.speculative.has(dep))
2664
+ continue;
2665
+ if (bundle[dep] !== undefined) {
2666
+ requiredPaths.add(dep);
2667
+ continue;
2668
+ }
2669
+ const cellLen = _bundleCellLength(content);
2670
+ if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
2671
+ break;
2672
+ if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
2673
+ continue;
2674
+ const raw = _bundleCellRawBytes(content);
2675
+ if (bytes + raw > room)
2676
+ continue;
2677
+ bundle[dep] = content;
2678
+ requiredPaths.add(dep);
2679
+ bytes += raw;
2680
+ budgetState.totalBytes += cellLen;
2681
+ budgetState.fileCount++;
2682
+ added++;
2683
+ }
2684
+ }
2685
+ return { added, bytes };
2667
2686
  }
2668
2687
  const RUNTIME_PACKAGE_EXCLUDED_ROOT_DIRS = new Set([
2669
2688
  'docs',
@@ -2706,6 +2725,31 @@ const RUNTIME_PACKAGE_EXCLUDED_FILE_SUFFIXES = [
2706
2725
  '.mov',
2707
2726
  '.webm',
2708
2727
  ];
2728
+ /**
2729
+ * Whether the facet's require path could ever load this cell AS A MODULE.
2730
+ *
2731
+ * Not a judgement about whether the file is useful — an evicted cell is a
2732
+ * real loss either way, since the synchronous read it exists for raises
2733
+ * EAGAIN. It is a judgement about what the loss can BREAK. A cell the loader
2734
+ * can resolve is one some other module may be importing, and losing it takes
2735
+ * every importer down with it; a cell it can never resolve is only ever read
2736
+ * as data, by whoever asked for that path specifically.
2737
+ *
2738
+ * Declaration files are the clean case, and the reason this is a suffix test
2739
+ * rather than a content one: `foo.d.ts` is consumed by a type checker and is
2740
+ * never the target of a `require`, so shedding one cannot orphan a module.
2741
+ * That does not contradict the list above keeping them admissible — a `.d.ts`
2742
+ * IS a legitimate runtime read for the one package that reads its own (tsc
2743
+ * and `lib.*.d.ts`). This decides only what goes FIRST once a bound has
2744
+ * already been breached and something has to.
2745
+ */
2746
+ function _isLoadableModuleCell(path) {
2747
+ if (isTypescriptDeclarationFile(path))
2748
+ return false;
2749
+ const ext = vfsPathExtension(path);
2750
+ return ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.json'
2751
+ || ext === '.wasm' || ext === '' || bundleTypescriptLoader(path) !== null;
2752
+ }
2709
2753
  /**
2710
2754
  * Per-file ceiling for the speculative passes over installed packages: the
2711
2755
  * entry-package walk (`addBinTargetSiblings`) and the main-entry oversample
@@ -2857,7 +2901,7 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
2857
2901
  if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
2858
2902
  return;
2859
2903
  try {
2860
- if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
2904
+ if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
2861
2905
  return;
2862
2906
  if (!(await _bundleAdmits(vfs, stripped, budgetState)))
2863
2907
  return;
@@ -2910,8 +2954,19 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
2910
2954
  }
2911
2955
  return { added };
2912
2956
  }
2913
- function looksLikeEsm(src) {
2914
- return hasTopLevelModuleSyntax(src);
2957
+ function looksLikeEsm(path, src) {
2958
+ if (!hasTopLevelModuleSyntax(src))
2959
+ return false;
2960
+ if (vfsPathExtension(path) !== '')
2961
+ return true;
2962
+ // No extension: a bin script, or data such as a LICENSE whose prose says "import". Only a parse tells them apart.
2963
+ try {
2964
+ parseJavaScriptModule(src);
2965
+ return true;
2966
+ }
2967
+ catch {
2968
+ return false;
2969
+ }
2915
2970
  }
2916
2971
  /**
2917
2972
  * W3.5 Fix B — module-level cache for ESM→CJS transform results, keyed
@@ -2922,8 +2977,44 @@ function looksLikeEsm(src) {
2922
2977
  *
2923
2978
  * Lives at module scope so warm exec invocations hit the cache without
2924
2979
  * paying the wasm cold-start cost again.
2980
+ *
2981
+ * Bounded by bytes, LRU, and reported to the heap model like the prefetch
2982
+ * cache (see ESM_TRANSFORM_CACHE_MAX_BYTES for the reset it caused
2983
+ * unbounded). An output larger than the whole bound is used for the build
2984
+ * that produced it and not retained: admitting it would evict everything
2985
+ * else to hold something that still does not fit.
2925
2986
  */
2926
2987
  const __esmTransformCache = new Map();
2988
+ let __esmTransformCacheBytes = 0;
2989
+ function __esmTransformCacheGet(key) {
2990
+ const code = __esmTransformCache.get(key);
2991
+ if (code === undefined)
2992
+ return undefined;
2993
+ // Refresh recency: a Map iterates in insertion order, so the oldest
2994
+ // entry is the first one.
2995
+ __esmTransformCache.delete(key);
2996
+ __esmTransformCache.set(key, code);
2997
+ return code;
2998
+ }
2999
+ function __esmTransformCacheSet(key, code) {
3000
+ const bytes = key.length + code.length;
3001
+ if (bytes > ESM_TRANSFORM_CACHE_MAX_BYTES)
3002
+ return;
3003
+ const previous = __esmTransformCache.get(key);
3004
+ if (previous !== undefined) {
3005
+ __esmTransformCacheBytes -= key.length + previous.length;
3006
+ __esmTransformCache.delete(key);
3007
+ }
3008
+ __esmTransformCache.set(key, code);
3009
+ __esmTransformCacheBytes += bytes;
3010
+ for (const [oldest, entry] of __esmTransformCache) {
3011
+ if (__esmTransformCacheBytes <= ESM_TRANSFORM_CACHE_MAX_BYTES)
3012
+ break;
3013
+ __esmTransformCache.delete(oldest);
3014
+ __esmTransformCacheBytes -= oldest.length + entry.length;
3015
+ }
3016
+ setTransformCacheBytes(__esmTransformCacheBytes);
3017
+ }
2927
3018
  function __cacheKey(src) {
2928
3019
  // FNV-1a 32-bit. Only used for cache keys, NEVER for content
2929
3020
  // integrity. The ~30-byte string we return is a hex hash + length —
@@ -2959,14 +3050,21 @@ function _markBundleEsmAsFailed(bundle, reason) {
2959
3050
  // A TypeScript source is never runnable as staged, so it always needs
2960
3051
  // the emit it cannot get; a JavaScript file only if it is ESM.
2961
3052
  const typescript = bundleTypescriptLoader(path) !== null;
2962
- if (!typescript && !looksLikeEsm(src))
3053
+ if (!typescript && !looksLikeEsm(path, src))
2963
3054
  continue;
2964
- const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
2965
- bundle[typescript ? compiledCellKey(path) : path] =
2966
- '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
2967
- '(function () { throw new Error(' + escapedReason + '); })();\n';
3055
+ bundle[typescript ? compiledCellKey(path) : path] = esbuildDiagnosticShim(path, reason);
2968
3056
  }
2969
3057
  }
3058
+ /**
3059
+ * Parseable CommonJS standing in for a module esbuild could not transform: it
3060
+ * throws the esbuild reason when required, so the failure surfaces at the
3061
+ * `require` with its cause rather than as a bare "Cannot use import statement".
3062
+ */
3063
+ function esbuildDiagnosticShim(path, reason) {
3064
+ const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason.replace(/\n/g, ' ')}`);
3065
+ return '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
3066
+ '(function () { throw new Error(' + escapedReason + '); })();\n';
3067
+ }
2970
3068
  /**
2971
3069
  * The set of bundle entries the facet's startup pre-compile loop turns into
2972
3070
  * functions, minus the ones already in the right format. Everything the loop
@@ -2977,7 +3075,8 @@ function _markBundleEsmAsFailed(bundle, reason) {
2977
3075
  * Extensionless entries are in the set for the same reason the pre-compile
2978
3076
  * loop takes them — that is the shape of nearly every npm `bin` script.
2979
3077
  * `.json` is data and `.cjs` is CommonJS by definition; neither needs the
2980
- * transform. Content, not the path, decides from here: `looksLikeEsm` parses.
3078
+ * transform. Content decides from here: `looksLikeEsm` sniffs module syntax,
3079
+ * and parses an extensionless file, which may be data rather than a script.
2981
3080
  */
2982
3081
  export function isBundleModuleCandidate(path) {
2983
3082
  const ext = vfsPathExtension(path);
@@ -2985,7 +3084,8 @@ export function isBundleModuleCandidate(path) {
2985
3084
  }
2986
3085
  /**
2987
3086
  * The esbuild loader for a TypeScript source in the bundle, or null when the
2988
- * path does not name one.
3087
+ * path does not name one. Which extensions are TypeScript is
3088
+ * `typescriptLoader`'s table, the one a runtime's entry script is decided by.
2989
3089
  *
2990
3090
  * A resolved `.ts` file reaches the facet as TypeScript, and TypeScript is not
2991
3091
  * JavaScript: `new Function` on a type annotation is a SyntaxError whether or
@@ -3004,12 +3104,7 @@ export function isBundleModuleCandidate(path) {
3004
3104
  * gone. So a declaration file is left exactly as it was staged.
3005
3105
  */
3006
3106
  export function bundleTypescriptLoader(path) {
3007
- if (isTypescriptDeclarationFile(path))
3008
- return null;
3009
- const ext = vfsPathExtension(path);
3010
- if (ext === '.tsx')
3011
- return 'tsx';
3012
- return ext === '.ts' || ext === '.mts' || ext === '.cts' ? 'ts' : null;
3107
+ return isTypescriptDeclarationFile(path) ? null : typescriptLoader(path);
3013
3108
  }
3014
3109
  /** `name.d.ts` / `name.d.mts` / `name.d.cts`, by TypeScript's own rule. */
3015
3110
  export function isTypescriptDeclarationFile(path) {
@@ -3078,19 +3173,22 @@ for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
3078
3173
  `;
3079
3174
  /**
3080
3175
  * Transform every ESM-shaped file in the bundle to CJS via esbuild.
3081
- * Mutates `bundle` in place. Errors are swallowed (the file is left as
3082
- * ESM source); the facet's pre-compile loop will record the SyntaxError
3083
- * into __compileFailures and __loadModule will surface it (Fix C).
3176
+ * Mutates `bundle` in place. A module esbuild rejects becomes a diagnostic
3177
+ * shim that throws the reason when required (`esbuildDiagnosticShim`).
3084
3178
  *
3085
- * Candidate set is `isBundleModuleCandidate`; within it, files with no
3086
- * top-level import/export are already CJS-shaped and left alone.
3179
+ * ESM/TypeScript candidates are `isBundleModuleCandidate`. CommonJS cells,
3180
+ * including .cjs, also visit the rewrite-only path for dynamic import().
3087
3181
  *
3088
3182
  * A JavaScript cell is rewritten in place. A TypeScript source keeps its
3089
3183
  * bytes and gets a compiled cell beside it — see `compiledCellKey`.
3090
3184
  *
3185
+ * When the service transforms in another isolate every cell goes in one
3186
+ * `transformMany`, so a launch costs one round trip and this isolate never
3187
+ * grows esbuild's heap. Transforms that run here are paced like any pass.
3188
+ *
3091
3189
  * Returns the count of files transformed (for diagnostics).
3092
3190
  */
3093
- async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
3191
+ async function transformEsmInBundle(bundle, esbuild, pacer) {
3094
3192
  let transformed = 0;
3095
3193
  let failed = 0;
3096
3194
  // Snapshot the keys first — esbuild calls await; never iterate-and-mutate.
@@ -3098,104 +3196,140 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
3098
3196
  for (const path of Object.keys(bundle)) {
3099
3197
  if (compiledCellPath(path) !== null)
3100
3198
  continue;
3101
- if (!isBundleModuleCandidate(path))
3199
+ if (!isBundleModuleCandidate(path) && !path.endsWith('.cjs'))
3102
3200
  continue;
3103
3201
  const src = bundle[path];
3104
3202
  // hardening-r5: binary cells are not ESM. Skip — looksLikeEsm +
3105
3203
  // esbuild.transform expect strings.
3106
3204
  if (typeof src !== 'string')
3107
3205
  continue;
3108
- if (bundleTypescriptLoader(path) === null) {
3109
- const esm = looksLikeEsm(src);
3110
- if (!esm)
3206
+ if (path.endsWith('.cjs')) {
3207
+ if (!mayHaveDynamicImport(src))
3111
3208
  continue;
3112
3209
  }
3210
+ else if (bundleTypescriptLoader(path) === null && !looksLikeEsm(path, src) && !mayHaveDynamicImport(src))
3211
+ continue;
3113
3212
  candidates.push(path);
3114
3213
  }
3214
+ const settle = (cell, outcome) => {
3215
+ if ('error' in outcome) {
3216
+ // Infrastructure failure is not source code. Abort before the bundle
3217
+ // or immutable loader image can cache a throw-on-require diagnostic.
3218
+ if (outcome.transient)
3219
+ throw new Error(`esbuild transform unavailable for ${cell.path}: ${outcome.error}`);
3220
+ // esbuild's verdict on this source is cached with it; a host that could
3221
+ // not run the transform this time has no verdict to cache.
3222
+ const shim = esbuildDiagnosticShim(cell.path, outcome.error);
3223
+ bundle[cell.target] = shim;
3224
+ __esmTransformCacheSet(cell.key, shim);
3225
+ failed++;
3226
+ return;
3227
+ }
3228
+ const code = bindImportMetaResolve(outcome.code, cell.absUrl);
3229
+ bundle[cell.target] = code;
3230
+ __esmTransformCacheSet(cell.key, code);
3231
+ transformed++;
3232
+ };
3233
+ const transformCells = async (cells) => {
3234
+ let outcomes;
3235
+ try {
3236
+ outcomes = await esbuild.transformMany(cells.map((cell) => cell.request));
3237
+ }
3238
+ catch (e) {
3239
+ // Publishing a whole-bundle diagnostic would poison the next launch
3240
+ // even when the per-cell transform cache correctly rejected it.
3241
+ throw new Error(`esbuild transform service unavailable: ${errorText(e)}`, { cause: e });
3242
+ }
3243
+ cells.forEach((cell, i) => settle(cell, outcomes[i]));
3244
+ };
3245
+ const batch = [];
3115
3246
  for (const path of candidates) {
3116
3247
  const original = bundle[path];
3117
3248
  if (typeof original !== 'string')
3118
3249
  continue;
3119
- const src = bundleTypescriptLoader(path) === null ? rewriteProvidedCommonJsModules(original) : original;
3250
+ const loader = bundleTypescriptLoader(path);
3120
3251
  // A TypeScript source keeps its bytes; its emit lands beside it.
3121
- const target = bundleTypescriptLoader(path) === null ? path : compiledCellKey(path);
3122
- // esbuild-wasm runs inside this isolate, so a transform is computation
3123
- // like any other pass — the await above it yields nothing on its own.
3124
- if (pacer)
3125
- await pacer.spend(src.length);
3126
- // `import.meta.url` substitution mirrors the sibling fix at
3127
- // src/runtime/runtime-registry.ts:383-389 (framework-gaps-fix P5).
3128
- // Without `define`, esbuild's CJS transform reduces `import.meta.url`
3129
- // to undefined (single-pass) or preserves it literally — only to
3130
- // SyntaxError at `new Function(...)` parse time (two-pass via
3131
- // EsbuildService.transform's async-IIFE wrap → "Cannot use
3132
- // 'import.meta' outside a module"). The substitution value is the
3133
- // real `file:///<absolute-path>` URL — exactly what real Node returns
3134
- // for an ESM module at that path.
3135
- //
3136
- // Note: cache key now incorporates the path because the transformed
3137
- // output is path-specific (the URL literal is baked in). Two files
3138
- // with identical source but different paths would otherwise share a
3139
- // cache entry and the second file would get the first file's URL.
3252
+ const target = loader === null ? path : compiledCellKey(path);
3253
+ // Source is transformed once per path; import.meta reads metadata from
3254
+ // each evaluation's module object, including its query and fragment.
3255
+ // The source URL still keys transforms and supplies the static parent
3256
+ // for rewritten dynamic imports and diagnostics.
3140
3257
  const absUrl = 'file:///' + path.replace(/^\/+/, '');
3141
- const key = __cacheKey(src + '\0' + absUrl);
3142
- const cached = __esmTransformCache.get(key);
3143
- if (cached) {
3258
+ // Keyed on the staged bytes, so a cell the pre-pass fails has a key too.
3259
+ const key = __cacheKey(original + '\0' + absUrl);
3260
+ const cached = __esmTransformCacheGet(key);
3261
+ if (cached !== undefined) {
3144
3262
  bundle[target] = cached;
3145
3263
  transformed++;
3146
3264
  continue;
3147
3265
  }
3266
+ // Every cell's dynamic import() is the process's: the transform keeps
3267
+ // them, and the facet rewrites each to the process's ESM loader.
3268
+ const moduleMetadata = !path.endsWith('.cjs') && (loader !== null || looksLikeEsm(path, original));
3269
+ const cellFor = (code, rewriteOnly = false) => ({
3270
+ path,
3271
+ target,
3272
+ key,
3273
+ absUrl,
3274
+ request: {
3275
+ code,
3276
+ options: rewriteOnly
3277
+ ? { rewriteOnly: true, dynamicImportParent: absUrl, moduleMetadata }
3278
+ : { loader: loader ?? 'js', format: 'cjs', target: 'esnext', define: importMetaDefines(absUrl, true), dynamicImportParent: absUrl, moduleMetadata },
3279
+ },
3280
+ });
3281
+ let src;
3148
3282
  try {
3149
- const transformOptions = {
3150
- loader: bundleTypescriptLoader(path) ?? 'js',
3151
- format: 'cjs',
3152
- target: 'esnext',
3153
- define: importMetaDefines(absUrl),
3154
- };
3155
- const bounded = transformOptions.loader === 'js' && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
3156
- ? rewriteBundledEsmToCjs(src, absUrl)
3157
- : null;
3158
- const t = bounded ?? (isolatedTransform && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
3159
- ? await isolatedTransform(src, transformOptions)
3160
- : await esbuild.transform(src, transformOptions));
3161
- const code = bindImportMetaResolve(t.code, absUrl);
3162
- bundle[target] = code;
3163
- __esmTransformCache.set(key, code);
3164
- transformed++;
3283
+ src = loader === null ? rewriteProvidedCommonJsModules(original) : original;
3165
3284
  }
3166
3285
  catch (e) {
3167
- // framework-fixes-F4 (2026-05-12): replace the source with a
3168
- // JS-valid module that throws an INFORMATIVE Error when require'd.
3169
- //
3170
- // Pre-fix this catch swallowed the failure silently. The pre-
3171
- // compile loop then re-saw the ESM source and emitted the
3172
- // SyntaxError "Cannot use import statement outside a module" into
3173
- // __compileFailures. __loadModule surfaced that as the user error
3174
- // — but it told the user nothing about WHY esbuild rejected the
3175
- // file (was it TLA at module scope? a TypeScript file mistyped
3176
- // as .js? an unsupported syntax? our esbuild service not init?).
3177
- //
3178
- // Post-fix the replacement source is parseable CJS that, when
3179
- // require()'d, throws a real Error carrying the esbuild reason.
3180
- // The pre-compile loop succeeds, the file lands in
3181
- // __compiledModules, and the failure surfaces at require time
3182
- // with FULL diagnostic context. Net effect for users: the same
3183
- // upstream tool still fails, but they now see WHY.
3184
- // Probe: tests/behavioral/npm-create/new/create-astro-diagnostic.mjs
3185
- // asserts the user-visible error contains "esbuild transform" so
3186
- // future diagnostic regressions are caught.
3187
- const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
3188
- const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
3189
- // Build a valid CJS module that throws on load. Use IIFE to keep
3190
- // module-scope flat (no TLA, no top-level await). Esbuild-cache
3191
- // this so repeated submits don't re-run the same transform.
3192
- const diagnosticSrc = '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
3193
- '(function () { throw new Error(' + escapedReason + '); })();\n';
3194
- bundle[target] = diagnosticSrc;
3195
- __esmTransformCache.set(key, diagnosticSrc);
3196
- failed++;
3286
+ // The pre-pass cannot read this cell: a verdict on it alone, like esbuild's.
3287
+ settle(cellFor(original), { error: errorText(e) });
3288
+ continue;
3289
+ }
3290
+ // CommonJS already: only its dynamic import() calls change.
3291
+ const cell = path.endsWith('.cjs') || (loader === null && !looksLikeEsm(path, src)) ? cellFor(src, true) : cellFor(src);
3292
+ if (cell.request.options?.rewriteOnly !== true && loader === null && src.length >= BUNDLED_ESM_REWRITE_MIN_BYTES) {
3293
+ // The bounded rewrite is computation in this isolate, however large.
3294
+ if (pacer)
3295
+ await pacer.spend(src.length);
3296
+ let rewritten;
3297
+ try {
3298
+ rewritten = rewriteBundledEsmToCjs(src, absUrl, true);
3299
+ }
3300
+ catch (e) {
3301
+ rewritten = { error: errorText(e) };
3302
+ }
3303
+ if (rewritten && 'error' in rewritten) {
3304
+ settle(cell, rewritten);
3305
+ continue;
3306
+ }
3307
+ if (rewritten) {
3308
+ // Its declarations are CommonJS now; what import() calls remain go to
3309
+ // the facet like any cell's.
3310
+ if (!mayHaveDynamicImport(rewritten.code) && !rewritten.code.includes('import.meta')) {
3311
+ settle(cell, rewritten);
3312
+ continue;
3313
+ }
3314
+ const rest = cellFor(rewritten.code, true);
3315
+ if (esbuild.transformsInIsolate) {
3316
+ await transformCells([rest]);
3317
+ continue;
3318
+ }
3319
+ batch.push(rest);
3320
+ continue;
3321
+ }
3197
3322
  }
3323
+ if (esbuild.transformsInIsolate) {
3324
+ if (pacer)
3325
+ await pacer.spend(src.length);
3326
+ await transformCells([cell]);
3327
+ continue;
3328
+ }
3329
+ batch.push(cell);
3198
3330
  }
3331
+ if (batch.length > 0)
3332
+ await transformCells(batch);
3199
3333
  return { transformed, failed };
3200
3334
  }
3201
3335
  /**
@@ -3214,7 +3348,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
3214
3348
  * behaviour for code paths that don't have esbuild handy).
3215
3349
  *
3216
3350
  */
3217
- export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes) {
3351
+ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, maxBundleBytes, learnedFor) {
3218
3352
  // This build accumulates raw VFS contents in the supervisor heap, and did it
3219
3353
  // with nothing watching: the estimator read 9.4 MiB while these bytes were
3220
3354
  // resetting the DO three times. Take the budget the enrichment passes are
@@ -3224,20 +3358,20 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
3224
3358
  const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
3225
3359
  prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
3226
3360
  try {
3227
- return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform, maxBundleBytes);
3361
+ return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, maxBundleBytes, learnedFor);
3228
3362
  }
3229
3363
  finally {
3230
3364
  prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
3231
3365
  lease.release();
3232
3366
  }
3233
3367
  }
3234
- async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes = VFS_BUNDLE_MAX_BYTES) {
3368
+ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, maxBundleBytes = VFS_BUNDLE_MAX_BYTES, learnedFor) {
3235
3369
  // Read the cursor BEFORE the walk: a mutation that lands while the bundle
3236
3370
  // is being assembled must be reported as invalidated, not silently missed.
3237
- const admitted = await vfs.authority.acquire(null, 0);
3371
+ const admitted = await vfs.acquire(null, 0);
3238
3372
  const cursor = { epoch: admitted.epoch, rev: admitted.rev };
3239
3373
  // 1. Static reachable-set walk from entry.
3240
- const prefetch = (await prefetchForRequire(vfs, entryCode || '', cwd, scriptPath, maxBundleBytes));
3374
+ const prefetch = (await prefetchForRequire(requireFsOverBridge(vfs), entryCode || '', cwd, scriptPath, maxBundleBytes, pacer?.spend.bind(pacer)));
3241
3375
  if ('kind' in prefetch) {
3242
3376
  // A required closure larger than the bound can never launch as a
3243
3377
  // snapshot. Surface it as the process's own failure rather than a
@@ -3245,7 +3379,16 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
3245
3379
  throw new ClosureBoundExceededError(prefetch);
3246
3380
  }
3247
3381
  const bundle = { ...prefetch.bundle };
3248
- const requiredPaths = new Set(Object.keys(prefetch.bundle));
3382
+ const closurePaths = new Set(Object.keys(prefetch.bundle).filter((path) => !prefetch.speculative.has(path)));
3383
+ // Evidence is kept apart from the closure: an observed subpath is not the
3384
+ // closure choosing that corner of a package, and only the closure can fail
3385
+ // a launch. The room evidence may fill is what the closure leaves under the
3386
+ // bound, this session's own misses first, then what other sessions learned.
3387
+ const observedPaths = new Set();
3388
+ const learnedPaths = new Set();
3389
+ let room = maxBundleBytes;
3390
+ for (const path of closurePaths)
3391
+ room -= _bundleCellRawBytes(bundle[path]);
3249
3392
  let truncated = false;
3250
3393
  const budgetState = { totalBytes: 0, fileCount: 0 };
3251
3394
  // Each enrichment pass below re-scans the bundle accumulated so far, so a
@@ -3267,14 +3410,23 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
3267
3410
  // admitted first, ahead of every guess, and they join the required set
3268
3411
  // rather than the evictable one, because a file evicted here misses
3269
3412
  // again on the next run and the loop never closes.
3270
- const observedAdd = (await addObservedReads(vfs, observedReads, bundle, requiredPaths, budgetState));
3271
- void observedAdd;
3413
+ // Other sessions' misses in the packages this closure can load
3414
+ // (read-profile.ts) are evidence of the same kind: a learned module joins
3415
+ // the module map with its imports, as this session's own misses do.
3416
+ const learned = learnedFor ? await learnedFor(Object.keys(bundle)).catch(() => []) : [];
3417
+ const own = await addObservedReads(vfs, observedReads, bundle, observedPaths, budgetState, Math.max(0, room), pacer);
3418
+ room -= own.bytes;
3419
+ const learnedOnly = new Set(learned.filter((path) => !observedReads?.has(path) && !observedPaths.has(path)));
3420
+ await addObservedReads(vfs, learnedOnly, bundle, learnedPaths, budgetState, Math.max(0, room), pacer);
3421
+ for (const path of observedPaths)
3422
+ learnedPaths.delete(path);
3272
3423
  await paceAfterPass();
3273
3424
  // 2. Greedy oversample — every installed pkg's pkg.json + main.
3274
3425
  // Catches dynamic-require / `bindings()` / plugin-loader cases the
3275
3426
  // regex prefetch misses. Its budget is independent from the complete
3276
3427
  // static require closure, which is correctness-critical.
3277
- const greedy = (await greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths));
3428
+ const independentBeforeGroups = new Set(Object.keys(bundle));
3429
+ const greedy = (await greedyAddMainEntries(vfs, cwd, bundle, budgetState, closurePaths, { maxBundleBytes, pacer }));
3278
3430
  await paceAfterPass();
3279
3431
  // 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
3280
3432
  // bundle .js/.mjs/.cjs source for the canonical jsdom shape:
@@ -3349,31 +3501,17 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
3349
3501
  // ESM files (e.g. tldts/dist/es6/index.js, @remix-run/react/dist/esm,
3350
3502
  // @tailwindcss/vite, react-remove-scroll, astro) silently fail
3351
3503
  // `new Function` at facet startup and surface as the misleading
3352
- // "file was not pre-bundled" at request time.
3504
+ // "not in this launch's module map" at request time.
3353
3505
  if (esbuild) {
3354
- try {
3355
- await transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform);
3356
- // Recount bytes after the transform — CJS rebuilds can be larger
3357
- // OR smaller than the ESM source. We don't try to thread totalBytes
3358
- // through the transform because the eviction loop below recomputes
3359
- // the encoded size from scratch anyway.
3360
- }
3361
- catch (e) {
3362
- // framework-fixes-F4 (2026-05-12): the prior catch was silent.
3363
- // Replace every detected-ESM source with the SAME diagnostic
3364
- // shim transformEsmInBundle's per-file catch installs, so the
3365
- // user sees "esbuild transform failed (service-level): <reason>"
3366
- // instead of a bare "Cannot use import statement..." parse
3367
- // error with no context.
3368
- const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
3369
- _markBundleEsmAsFailed(bundle, `esbuild service unavailable: ${reason}`);
3370
- }
3506
+ // Transient failures propagate through the launch failure path before
3507
+ // serialization/cache/LOADER publication. Per-source verdicts still use
3508
+ // the lazy diagnostic cells installed by transformEsmInBundle.
3509
+ await transformEsmInBundle(bundle, esbuild, pacer);
3371
3510
  }
3372
3511
  else {
3373
- // framework-fixes-F4 (2026-05-12): no esbuild service available at
3374
- // all (lazy-init failed or never wired). Same diagnostic-shim
3375
- // treatment so users see WHY the ESM file couldn't be transformed.
3376
- _markBundleEsmAsFailed(bundle, 'esbuild service not initialized (likely lazy-init failure)');
3512
+ // No esbuild service was given: the ESM cells stage as diagnostics that
3513
+ // say so, rather than as source `new Function` rejects without a reason.
3514
+ _markBundleEsmAsFailed(bundle, 'no esbuild service was given to this launch');
3377
3515
  }
3378
3516
  for (const path of Object.keys(bundle)) {
3379
3517
  if (compiledCellPath(path) === null && (!isBundleModuleCandidate(path) || bundleTypescriptLoader(path) !== null))
@@ -3382,61 +3520,165 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
3382
3520
  if (typeof source !== 'string')
3383
3521
  continue;
3384
3522
  await pacer?.spend(source.length);
3385
- bundle[path] = rewriteProvidedCommonJsModules(source);
3523
+ try {
3524
+ bundle[path] = rewriteProvidedCommonJsModules(source);
3525
+ }
3526
+ catch {
3527
+ // Unparseable, so not a module and no bundled records to bind: data such as a LICENSE.
3528
+ }
3386
3529
  }
3387
3530
  await paceAfterPass();
3388
- // 3. Manifest pass — UNCHANGED from W2.5b. Decouples directory shape
3389
- // from content cap so fs.readdirSync remains honest even if the
3390
- // content for a given file was capped out.
3391
- await paceAfterPass();
3392
- const manifest = (await buildManifest(vfs, cwd, scriptPath));
3393
- await paceAfterPass();
3394
- // 4. JSON-encoded-size guard, measured in UTF-8 bytes (not UTF-16 code
3395
- // units) because that is what workerd charges against the per-module
3396
- // text-size budget. Only OPTIONAL enrichment is evictable, largest
3397
- // first; the manifest and the static require closure stay.
3531
+ // 4. The snapshot's size guard, in the unit the session DO's memory was
3532
+ // measured in and the require walk counts: raw bytes, a cell's UTF-8
3533
+ // length (VFS_BUNDLE_MAX_BYTES, platform/limits.ts, unless the caller
3534
+ // set its own). The per-module text limit is met by side modules and is
3535
+ // not a reason to evict. Only
3536
+ // OPTIONAL enrichment is evictable; the static require closure stays, and
3537
+ // a closure past the bound by itself fails the launch by name.
3398
3538
  //
3399
3539
  // Evicting an enrichment file is a real loss — the sync fs reads it
3400
3540
  // exists for cannot fall back to the supervisor — so the paths that
3401
3541
  // went are named rather than silently dropped.
3402
- const size = encodedBundleSize(bundle, manifest);
3403
- if (size.bytes > BUNDLE_MAX_ENCODED_BYTES) {
3542
+ const size = encodedBundleSize(bundle);
3543
+ const rawBytes = new Map();
3544
+ let rawTotal = 0;
3545
+ for (const [path, cell] of Object.entries(bundle)) {
3546
+ const bytes = _bundleCellRawBytes(cell);
3547
+ rawBytes.set(path, bytes);
3548
+ rawTotal += bytes;
3549
+ }
3550
+ if (rawTotal > maxBundleBytes) {
3551
+ // Only the walk's closure can fail a launch, and the path it names is the
3552
+ // one whose bytes (with its compiled cell) crossed the bound, in walk order.
3553
+ let closureBytes = 0;
3554
+ let crossedAt = null;
3555
+ for (const path of closurePaths) {
3556
+ closureBytes += (rawBytes.get(path) ?? 0) + (rawBytes.get(compiledCellKey(path)) ?? 0);
3557
+ if (crossedAt === null && closureBytes > maxBundleBytes)
3558
+ crossedAt = path;
3559
+ }
3560
+ if (crossedAt !== null) {
3561
+ throw new ClosureBoundExceededError({
3562
+ kind: 'closure-exceeds-bound',
3563
+ entry: scriptPath ?? 'entry code',
3564
+ bytesSeen: closureBytes,
3565
+ bound: maxBundleBytes,
3566
+ lastPath: crossedAt,
3567
+ });
3568
+ }
3404
3569
  // A compiled cell goes with its source: required when the source is.
3405
- const evictable = Object.keys(bundle)
3406
- .filter((path) => !requiredPaths.has(compiledCellPath(path) ?? path))
3407
- .sort((a, b) => _bundleCellLength(bundle[b]) - _bundleCellLength(bundle[a]));
3408
- const evicted = [];
3409
- for (const k of evictable) {
3410
- if (size.bytes <= BUNDLE_MAX_ENCODED_BYTES)
3570
+ //
3571
+ // Order matters as much as the bound. Largest-first alone ranks a cell by
3572
+ // what it costs and never by what losing it costs: an admitted module and
3573
+ // the sibling it imports are both "optional", and shedding the sibling
3574
+ // leaves a module in the bundle that cannot load. On `astro dev` the
3575
+ // snapshot breached its bound by 591 files, and among the largest were
3576
+ // modules while 3,291 declaration files (never a require target) stayed.
3577
+ //
3578
+ // Unloadable cells first, then enrichment largest-first, then dynamic-import
3579
+ // subtrees last-discovered first, so a CLI's own `import()` deferral is shed last.
3580
+ const speculativeRank = new Map();
3581
+ for (const path of prefetch.speculative)
3582
+ speculativeRank.set(path, speculativeRank.size);
3583
+ const rankOf = (path) => speculativeRank.get(compiledCellPath(path) ?? path) ?? -1;
3584
+ // Evidence goes after all enrichment: what others learned before this
3585
+ // session's own misses, each tier last-admitted first.
3586
+ const tierOf = (path) => {
3587
+ const source = compiledCellPath(path) ?? path;
3588
+ return closurePaths.has(source) ? 3 : observedPaths.has(source) ? 2 : learnedPaths.has(source) ? 1 : 0;
3589
+ };
3590
+ const members = new Set();
3591
+ for (const group of greedy.groups)
3592
+ for (const path of group.members)
3593
+ members.add(path);
3594
+ const sources = new Set(Object.keys(bundle).map(path => compiledCellPath(path) ?? path));
3595
+ const independent = new Set([...sources].filter(path => !members.has(path) || independentBeforeGroups.has(path)));
3596
+ const keptGroups = new Set(greedy.groups);
3597
+ const weight = (path) => (rawBytes.get(path) ?? 0) + (rawBytes.get(compiledCellKey(path)) ?? 0);
3598
+ const unitBytes = new Map();
3599
+ for (const group of greedy.groups) {
3600
+ let total = 0;
3601
+ for (const member of group.members)
3602
+ if (!independent.has(member))
3603
+ total += weight(member);
3604
+ unitBytes.set(group.root, total);
3605
+ if (pacer)
3606
+ await pacer.spend(group.members.size * 256);
3607
+ }
3608
+ const lastAdmittedFirst = (tier) => [...tier].reverse().filter(path => independent.has(path));
3609
+ const enrichment = [...independent, ...greedy.groups.map(group => group.root)]
3610
+ .filter(path => tierOf(path) === 0)
3611
+ .sort((a, b) => {
3612
+ const loadable = (_isLoadableModuleCell(a) ? 1 : 0) - (_isLoadableModuleCell(b) ? 1 : 0);
3613
+ if (loadable !== 0)
3614
+ return loadable;
3615
+ const rankA = rankOf(a);
3616
+ const rankB = rankOf(b);
3617
+ if (rankA !== rankB)
3618
+ return rankA < 0 ? -1 : rankB < 0 ? 1 : rankB - rankA;
3619
+ return (unitBytes.get(b) ?? weight(b)) - (unitBytes.get(a) ?? weight(a));
3620
+ });
3621
+ const evictable = [...enrichment, ...lastAdmittedFirst(learnedPaths), ...lastAdmittedFirst(observedPaths)];
3622
+ async function retainedPaths() {
3623
+ const retained = new Set(independent);
3624
+ for (const group of keptGroups) {
3625
+ let work = 0;
3626
+ for (const path of group.members) {
3627
+ retained.add(path);
3628
+ work += path.length + 256;
3629
+ }
3630
+ if (pacer)
3631
+ await pacer.spend(work);
3632
+ }
3633
+ for (const source of [...retained]) {
3634
+ const compiled = compiledCellKey(source);
3635
+ if (bundle[compiled] !== undefined)
3636
+ retained.add(compiled);
3637
+ }
3638
+ return retained;
3639
+ }
3640
+ let retained = await retainedPaths();
3641
+ for (const path of evictable) {
3642
+ if (rawTotal <= maxBundleBytes)
3411
3643
  break;
3412
- evicted.push([k, _bundleCellLength(bundle[k])]);
3644
+ if (independent.delete(path)) {
3645
+ for (const group of keptGroups)
3646
+ if (group.members.has(path))
3647
+ keptGroups.delete(group);
3648
+ }
3649
+ else {
3650
+ for (const group of keptGroups)
3651
+ if (group.root === path)
3652
+ keptGroups.delete(group);
3653
+ }
3654
+ retained = await retainedPaths();
3655
+ rawTotal = 0;
3656
+ for (const path of retained)
3657
+ rawTotal += rawBytes.get(path) ?? 0;
3658
+ }
3659
+ const evicted = [];
3660
+ for (const k of Object.keys(bundle)) {
3661
+ if (retained.has(k))
3662
+ continue;
3663
+ evicted.push([k, rawBytes.get(k) ?? 0]);
3413
3664
  delete bundle[k];
3414
3665
  size.remove(k);
3415
3666
  }
3416
3667
  if (evicted.length > 0) {
3417
3668
  truncated = true;
3418
- console.warn(`[facet-manager] prefetch snapshot exceeded ${BUNDLE_MAX_ENCODED_BYTES} encoded `
3419
- + `bytes; evicted ${evicted.length} optional file(s). They still exist and `
3669
+ console.warn(`[facet-manager] prefetch snapshot exceeded ${maxBundleBytes} bytes; `
3670
+ + `evicted ${evicted.length} optional file(s). They still exist and `
3420
3671
  + `async reads still return them; synchronous reads raise EAGAIN: `
3421
3672
  + `${describeBundleCells(evicted)}`);
3422
3673
  }
3423
3674
  }
3424
3675
  const fileCount = Object.keys(bundle).length;
3425
- const metadata = (await buildVfsMetadata(vfs, manifest, bundle));
3426
- (await addUnreadableDenialCells(vfs, bundle, metadata));
3427
- await paceAfterPass();
3428
- // Denial cells land after the eviction pass, so account for them before
3429
- // deciding where the bundle has to live.
3430
- for (const [path, cell] of Object.entries(bundle))
3431
- size.add(path, cell);
3432
3676
  const bundleSideModulesRequired = size.bytes > BUNDLE_MAX_ENCODED_BYTES;
3433
3677
  // Suppress lint: `greedy.added` is observed only via diagnostics.
3434
3678
  void greedy;
3435
3679
  const wasmImages = (await collectClosureWasmImages(vfs, bundle, binSiblingAdd.wasmPaths));
3436
3680
  return {
3437
3681
  bundle,
3438
- manifest,
3439
- metadata,
3440
3682
  cursor,
3441
3683
  reachableCount: fileCount,
3442
3684
  truncated,
@@ -3518,24 +3760,27 @@ export class FacetManager {
3518
3760
  // spawn created as an OS-child of the attach TUI. When the attach process
3519
3761
  // exits (reported / killed), its serve facet is torn down with it.
3520
3762
  _pairedServeFacet = new Map();
3763
+ // The bundle a resident pid booted from, so the misses it reports at exit
3764
+ // stage into the next launch of the same entry, as a one-shot's do.
3765
+ residentBundleKeys = new Map();
3766
+ /** Per resident pid: the shared read profile's entries its launch staged, settled at its exit. */
3767
+ residentProfileOffers = new Map();
3521
3768
  /**
3522
- * W3.5 Fix B: lazily-created EsbuildService for the ESM→CJS pre-pass
3523
- * over the prefetch bundle. Created on first exec where vfs is set;
3524
- * shared across subsequent execs (warm wasm). Optional setter
3525
- * `setEsbuildService` lets NimbusSession share its existing instance
3526
- * to avoid double-init.
3769
+ * The esbuild the bundle's ESM→CJS pass transforms with. composeFacetManager
3770
+ * sets it: the host's own, or one whose transforms run in the session's
3771
+ * esbuild facet. Never one of this isolate: esbuild-wasm's heap only grows.
3527
3772
  */
3528
3773
  esbuild = null;
3529
3774
  /**
3530
3775
  * Prefetch-bundle cache. buildPrefetchBundle does a full VFS reachable-set
3531
3776
  * walk + greedy oversample + esbuild ESM→CJS pass on EVERY foreground
3532
3777
  * exec — dominant wall-clock on large node_modules. This memoizes the
3533
- * result (including the serialized facet bundle + manifest) keyed on
3778
+ * result (including the serialized facet bundle) keyed on
3534
3779
  * (bundleProfile, cwd, scriptPath, entryCode identity).
3535
3780
  *
3536
3781
  * Correctness watermark: the GLOBAL SqliteVFS revision. buildPrefetchBundle
3537
3782
  * reads from paths that can lie anywhere in the VFS (addEntryAbsPathReads
3538
- * pulls absolute-path literals like /tmp/x; buildManifest walks from '/'),
3783
+ * pulls absolute-path literals like /tmp/x),
3539
3784
  * so a cwd-scoped subtree revision cannot guarantee invalidation. The
3540
3785
  * global revision bumps on ANY write, so the cache invalidates on every
3541
3786
  * mutation that could change any file the bundle reads — provably
@@ -3556,6 +3801,35 @@ export class FacetManager {
3556
3801
  * already deliver for the case that matters: running the command again.
3557
3802
  */
3558
3803
  residencyProfiles = new Map();
3804
+ /**
3805
+ * Misses shared across sessions per installed package (read-profile.ts),
3806
+ * kept in the npm tarball cache bucket (NPM_TARBALL_CACHE) beside the
3807
+ * tarballs. Unbound, a miss is learned for this session only
3808
+ * (residencyProfiles).
3809
+ */
3810
+ readProfile = null;
3811
+ /** Read-profile changes dropped after losing every write race. */
3812
+ readProfileConflicts = 0;
3813
+ /** Per module path: its static references at a revision (see _closureStaticRefs). */
3814
+ staticRefsMemo = new Map();
3815
+ /** Modules whose references are remembered: a few programs' closures. */
3816
+ static STATIC_REFS_MEMO_MAX = 16_384;
3817
+ /**
3818
+ * What the prefetch cache holds right now, for /api/_diag/memory: each
3819
+ * entry's key, the revision it was built at, and its retained bytes, next
3820
+ * to the filesystem's live revision and the residency profiles. A launch
3821
+ * that rebuilds where a hit was expected is explained by exactly these:
3822
+ * the revision moved, the key changed, or a miss profile dropped the entry.
3823
+ */
3824
+ prefetchCacheDiag() {
3825
+ return {
3826
+ revision: this.vfs ? this.vfs.revision() : null,
3827
+ entries: [...this.prefetchBundleCache].map(([key, entry]) => ({
3828
+ key, revision: entry.revision, bytes: entry.bytes,
3829
+ })),
3830
+ residencyProfiles: [...this.residencyProfiles].map(([key, paths]) => ({ key, paths: [...paths] })),
3831
+ };
3832
+ }
3559
3833
  /** In-flight request-driven durable-app ensures, single-flight per port. */
3560
3834
  ensureInflight = new Map();
3561
3835
  /** Per-pid chain of journal-row amendments; see `_amendRow`. */
@@ -3593,6 +3867,12 @@ export class FacetManager {
3593
3867
  ? Reflect.get(env, 'NIMBUS_DEBUG')
3594
3868
  : undefined;
3595
3869
  this.debugEnabled = debugVar === '1' || debugVar === 'true';
3870
+ const profiles = ((typeof env === 'object' || typeof env === 'function') && env !== null)
3871
+ ? Reflect.get(env, 'NPM_TARBALL_CACHE')
3872
+ : undefined;
3873
+ this.readProfile = profiles && typeof profiles === 'object'
3874
+ ? new ReadProfile(profiles, Date.now, () => { this.readProfileConflicts++; })
3875
+ : null;
3596
3876
  this.launchJournal = new FencedWork(ctx.storage, {
3597
3877
  generationBase: () => this.processes.pidBase,
3598
3878
  waitUntil: (promise) => this.ctx.waitUntil(promise),
@@ -3617,8 +3897,31 @@ export class FacetManager {
3617
3897
  // Rooted on waitUntil: the hook fires synchronously inside whatever turn
3618
3898
  // ended the process, and the delete must not be a floating promise there.
3619
3899
  this.processes.setOnTerminal((pid) => {
3900
+ this.residentBundleKeys.delete(pid);
3901
+ this.residentProfileOffers.delete(pid);
3620
3902
  this.ctx.waitUntil(this.trackLaunchTask(this._onResidentTerminal(pid)));
3621
3903
  });
3904
+ this.processes.setDefaultSignalAction((pid, code, signal) => this._endBySignal(pid, code, signal));
3905
+ }
3906
+ /**
3907
+ * A signal's default action: the process ends with 128+signo whether its
3908
+ * facet is still being built (the launch stops at its next ownership gate)
3909
+ * or already booted (its resources are released like a kill).
3910
+ */
3911
+ _endBySignal(pid, code, signal) {
3912
+ if (this.processes.get(pid)?.state !== 'running')
3913
+ return;
3914
+ this.portRegistry.unregisterByPid(pid);
3915
+ this.releaseProcessRpcResources(pid);
3916
+ this.revokeProcessVfsWriters(pid);
3917
+ this.processes.exit(pid, code);
3918
+ this.processes.markExit(pid, code, signal);
3919
+ this.processes.closeInput(pid);
3920
+ try {
3921
+ this.hooks.onExternalExit?.(pid, code, signal);
3922
+ }
3923
+ catch { }
3924
+ this._teardownPairedServeFacet(pid);
3622
3925
  }
3623
3926
  /**
3624
3927
  * The process is over. Every end-of-life passes through here: a clean
@@ -3757,12 +4060,42 @@ export class FacetManager {
3757
4060
  }
3758
4061
  return vfs;
3759
4062
  }
4063
+ /** Give the bundle's ESM→CJS pass the host's esbuild, as composeFacetManager does. */
4064
+ setEsbuildService(esbuild) { this.esbuild = esbuild; }
3760
4065
  /**
3761
- * W3.5 Fix B: hand the FacetManager a pre-warmed EsbuildService for
3762
- * the ESM→CJS bundle pre-pass. NimbusSession already lazy-creates one
3763
- * for the user-shell `node` runtime; sharing avoids paying init twice.
4066
+ * The entry script as the facet compiles it: each dynamic `import()` routed
4067
+ * to the process's ESM loader, with the entry's own URL as the parent (Node
4068
+ * names `-e` code `<cwd>/[eval]` and stdin `<cwd>/[stdin]`). The parse runs
4069
+ * in the esbuild facet like every cell's, and is cached by content. The
4070
+ * module-map walk reads the script as written, before this.
3764
4071
  */
3765
- setEsbuildService(esbuild) { this.esbuild = esbuild; }
4072
+ async _entryDynamicImports(code, filename, cwd) {
4073
+ if (!mayHaveDynamicImport(code))
4074
+ return code;
4075
+ if (this.esbuild === null)
4076
+ throw new Error('entry dynamic import requires the transform service');
4077
+ const base = cwd.replace(/\/+$/, '') || '/';
4078
+ const path = filename === undefined || filename === '<eval>'
4079
+ ? `${base}/[eval]`
4080
+ : filename === '[stdin]' ? `${base}/[stdin]` : filename;
4081
+ const parentUrl = 'file:///' + path.replace(/^\/+/, '');
4082
+ const key = __cacheKey(code + '\0' + parentUrl + '\0entry');
4083
+ const cached = __esmTransformCacheGet(key);
4084
+ if (cached !== undefined)
4085
+ return cached;
4086
+ const [outcome] = await this.esbuild.transformMany([
4087
+ { code, options: { rewriteOnly: true, dynamicImportParent: parentUrl } },
4088
+ ]);
4089
+ // A failed entry rewrite must not reach a Worker Loader as native host
4090
+ // import(), or become an immutable cached image. Preserve the reported
4091
+ // reason for permanent source errors as well as transient failures.
4092
+ if (outcome === undefined)
4093
+ throw new Error('entry transform service returned no outcome');
4094
+ if ('error' in outcome)
4095
+ throw new Error(`entry dynamic import transform failed: ${outcome.error}`);
4096
+ __esmTransformCacheSet(key, outcome.code);
4097
+ return outcome.code;
4098
+ }
3766
4099
  /**
3767
4100
  * The pacer every launch is built under: the session's alarm-driven turn
3768
4101
  * pump, the deployment's chunk bound, and the one check a suspended launch
@@ -3820,36 +4153,211 @@ export class FacetManager {
3820
4153
  return modules;
3821
4154
  if (!this.filesystem)
3822
4155
  throw new Error('Process filesystem authority is not initialized');
3823
- const vfs = new ExecutionFs(this.filesystem.bind({ pid: entry.pid, cred: entry.cred }));
4156
+ const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
3824
4157
  for (const image of wasmImports) {
3825
- let bytes;
3826
- try {
3827
- bytes = (await vfs.readFile(stripLeadingSlashes(image.vfsPath)));
3828
- }
3829
- catch {
4158
+ const bytes = await filesOf(vfs).readBytes(stripLeadingSlashes(image.vfsPath)).catch(() => null);
4159
+ if (bytes === null)
3830
4160
  continue;
3831
- }
3832
4161
  modules[image.moduleName] = {
3833
4162
  wasm: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
3834
4163
  };
3835
4164
  }
3836
4165
  return modules;
3837
4166
  }
4167
+ /**
4168
+ * Which contents a resident process holds from its first instruction,
4169
+ * beyond its module map: data-plan.ts over this process's view of the
4170
+ * namespace. A path the plan leaves out is still named and stat-able; a
4171
+ * synchronous read of it is the one honest miss.
4172
+ */
4173
+ async _planResidentData(entry, vfsState, cwd, home, pacer) {
4174
+ if (!this.vfs || !this.filesystem)
4175
+ return { paths: [], storageBytes: 0 };
4176
+ const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
4177
+ const started = Date.now();
4178
+ const trace = (what) => {
4179
+ if (!this.debugEnabled)
4180
+ return;
4181
+ this.processes.appendOutput(entry.pid, 'stderr', `[nimbus-debug] data plan: ${what} at ${Date.now() - started} ms\n`);
4182
+ };
4183
+ trace('static references');
4184
+ const refs = await this._closureStaticRefs(vfs, vfsState.bundlePaths ?? [], pacer);
4185
+ trace(`${refs.length} modules name paths; learned reads`);
4186
+ const learned = await this._learnedReads(vfsState, entry.cred);
4187
+ trace(`${learned.length} learned; listing`);
4188
+ let pages = 0;
4189
+ let names = 0;
4190
+ const plan = await planFacetData({
4191
+ list: async (after) => {
4192
+ const page = await vfs.list(after, FS_LIST_PAGE_LIMIT);
4193
+ names += page.entries.length;
4194
+ trace(`page ${++pages}: ${page.entries.length} entries`);
4195
+ return { entries: page.entries, next: page.next };
4196
+ },
4197
+ readText: async (path) => {
4198
+ try {
4199
+ return await filesOf(vfs).readFileString(path);
4200
+ }
4201
+ catch {
4202
+ return null;
4203
+ }
4204
+ },
4205
+ // A missing or unreadable component is not a link: the lookup ends there.
4206
+ readlink: async (path) => {
4207
+ try {
4208
+ return await vfs.readlink(path);
4209
+ }
4210
+ catch {
4211
+ return null;
4212
+ }
4213
+ },
4214
+ stat: async (path) => {
4215
+ const st = await filesOf(vfs).stat(path).catch(() => null);
4216
+ return st && { kind: st.type, size: st.size };
4217
+ },
4218
+ }, {
4219
+ cwd,
4220
+ home: home || '/home/user',
4221
+ closure: vfsState.bundlePaths ?? [],
4222
+ refs,
4223
+ learned,
4224
+ spend: (units) => pacer.spend(units),
4225
+ });
4226
+ trace(`planned ${plan.paths.length}`);
4227
+ // What the facet's store will hold (N18), in the cost its store charges
4228
+ // itself (facet-resident-store's __residentCellCost): the planned bytes on
4229
+ // overflow pages (1% over), a head and chunk rows per file, a namespace
4230
+ // row per name, and slack for the last write's page.
4231
+ const rows = plan.paths.length * 2 + Math.ceil(plan.bytes / RESIDENT_CHUNK_BYTES) + names;
4232
+ // And the module map, which the store adopts at boot: the process's code,
4233
+ // costed when the map was built (its cells are released once serialized).
4234
+ const moduleBytes = vfsState.moduleStorageBytes ?? moduleMapStorageBytes(vfsState.bundle);
4235
+ const storageBytes = Math.ceil(plan.bytes * 1.01) + rows * LEDGER_ROW_BYTES + moduleBytes + 65_536;
4236
+ return { paths: plan.paths, storageBytes };
4237
+ }
4238
+ /**
4239
+ * Paths earlier launches of the same build missed in this session. Other
4240
+ * sessions' misses (the shared read profile) join the module map instead,
4241
+ * in _buildProcessBundle, where a learned module brings its imports.
4242
+ */
4243
+ async _learnedReads(vfsState, _cred) {
4244
+ return vfsState.bundleKey ? [...(this.residencyProfiles.get(vfsState.bundleKey) ?? [])] : [];
4245
+ }
4246
+ /**
4247
+ * The installed packages a closure can load: every package the lockfiles
4248
+ * pin whose node_modules directory is on the resolution path of the cwd or
4249
+ * of a closure file. A first miss is, by definition, in a package the
4250
+ * closure did not already load, so its package's root is found this way.
4251
+ */
4252
+ _profileRoots(closure, cwd) {
4253
+ const sql = this.ctx.storage.sql;
4254
+ if (!sql)
4255
+ return [];
4256
+ const integrities = new NpmCache(sql).installedIntegrities();
4257
+ if (integrities.size === 0)
4258
+ return [];
4259
+ const dirs = new Set();
4260
+ const ancestors = (dir) => {
4261
+ for (let d = dir;;) {
4262
+ const nm = d === '' ? 'node_modules' : `${d}/node_modules`;
4263
+ if (dirs.has(nm))
4264
+ return;
4265
+ dirs.add(nm);
4266
+ if (d === '')
4267
+ return;
4268
+ d = d.slice(0, Math.max(0, d.lastIndexOf('/')));
4269
+ }
4270
+ };
4271
+ ancestors(cwd.replace(/^\/+|\/+$/g, ''));
4272
+ for (const path of closure) {
4273
+ const k = path.replace(/^\/+/, '');
4274
+ ancestors(k.slice(0, Math.max(0, k.lastIndexOf('/'))));
4275
+ }
4276
+ return [...integrities.keys()].filter((root) => {
4277
+ const i = root.lastIndexOf('/node_modules/');
4278
+ const parent = i === -1 ? (root.startsWith('node_modules/') ? 'node_modules' : null) : root.slice(0, i + '/node_modules'.length);
4279
+ return parent !== null && dirs.has(parent);
4280
+ });
4281
+ }
4282
+ /**
4283
+ * A package directory's identity for the shared read profile: the tarball
4284
+ * integrity the session's lockfiles pin, or, for a package no lockfile
4285
+ * pins (a link, a git or file dependency), the content key of its
4286
+ * package.json as this credential reads it.
4287
+ */
4288
+ _packageIdentity(cred) {
4289
+ const sql = this.ctx.storage.sql;
4290
+ const integrities = sql ? new NpmCache(sql).installedIntegrities() : new Map();
4291
+ const view = this.vfs?.as(cred) ?? null;
4292
+ return (root) => {
4293
+ const integrity = integrities.get(root);
4294
+ if (integrity !== undefined)
4295
+ return integrity;
4296
+ if (view === null)
4297
+ return null;
4298
+ try {
4299
+ return `pkgjson:${view.contentKey(root + '/package.json')}`;
4300
+ }
4301
+ catch {
4302
+ return null;
4303
+ }
4304
+ };
4305
+ }
4306
+ /**
4307
+ * What the closure's JavaScript names by a foldable path (static-fs-refs.ts),
4308
+ * read from the VFS as written rather than from the module map, whose ESM
4309
+ * cells were rewritten and lost their import.meta. Each module is parsed once
4310
+ * per revision of it, in this session.
4311
+ */
4312
+ async _closureStaticRefs(vfs, paths, pacer) {
4313
+ const out = [];
4314
+ for (const path of paths) {
4315
+ if (!/\.(?:c|m)?js$/.test(path))
4316
+ continue;
4317
+ let rev;
4318
+ try {
4319
+ rev = await vfs.revision(path);
4320
+ }
4321
+ catch {
4322
+ continue;
4323
+ }
4324
+ const memo = this.staticRefsMemo.get(path);
4325
+ let refs;
4326
+ if (memo && memo.rev === rev) {
4327
+ refs = memo.refs;
4328
+ }
4329
+ else {
4330
+ let source;
4331
+ try {
4332
+ source = await filesOf(vfs).readFileString(path);
4333
+ }
4334
+ catch {
4335
+ continue;
4336
+ }
4337
+ await pacer.spend(source.length);
4338
+ refs = findStaticFsReferences(source, '/' + path);
4339
+ this.staticRefsMemo.delete(path);
4340
+ this.staticRefsMemo.set(path, { rev, refs });
4341
+ for (const oldest of this.staticRefsMemo.keys()) {
4342
+ if (this.staticRefsMemo.size <= FacetManager.STATIC_REFS_MEMO_MAX)
4343
+ break;
4344
+ this.staticRefsMemo.delete(oldest);
4345
+ }
4346
+ }
4347
+ if (refs.exact.length + refs.listed.length + refs.patterns.length + refs.cwdRelative.length + refs.resolves.length > 0) {
4348
+ out.push(refs);
4349
+ }
4350
+ }
4351
+ return out;
4352
+ }
3838
4353
  async _buildProcessBundle(entry, spec, pacer) {
3839
4354
  if (!this.vfs) {
3840
- return { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
3841
- }
3842
- // W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so ESM
3843
- // source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
3844
- // @tailwindcss/vite, react-remove-scroll, astro) get transformed to CJS
3845
- // before they hit the facet's `new Function` pre-compile loop. Lazy-create
3846
- // one if NimbusSession didn't share its own.
3847
- if (!this.esbuild)
3848
- this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
4355
+ return { bundle: {}, reachableCount: 0, truncated: false };
4356
+ }
3849
4357
  this.imageStore.ensureDir();
3850
4358
  if (!this.filesystem)
3851
4359
  throw new Error('Process filesystem authority is not initialized');
3852
- const vfs = new ExecutionFs(this.filesystem.bind({ pid: entry.pid, cred: entry.cred }));
4360
+ const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
3853
4361
  const { cred } = entry;
3854
4362
  const profile = spec.bundleProfile ?? DEFAULT_FACET_BUNDLE_PROFILE;
3855
4363
  const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
@@ -3878,16 +4386,41 @@ export class FacetManager {
3878
4386
  this.prefetchBundleCache.set(key, cached);
3879
4387
  return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
3880
4388
  }
3881
- const vfsState = await buildPrefetchBundle(vfs, spec.scriptPath, spec.cwd, spec.entryCode, this.esbuild, profile, this.residencyProfiles.get(key), pacer, this.hooks.transformLargeEsm);
4389
+ const readProfile = this.readProfile;
4390
+ const offered = [];
4391
+ const learnedFor = readProfile === null ? undefined : async (closure) => {
4392
+ const roots = this._profileRoots(closure, spec.cwd);
4393
+ if (roots.length === 0)
4394
+ return [];
4395
+ offered.push(...await readProfile.lookup(roots, this._packageIdentity(cred), READ_PROFILE_LAUNCH_BYTES));
4396
+ return offered.map((entry) => entry.path);
4397
+ };
4398
+ const vfsState = await buildPrefetchBundle(vfs, spec.scriptPath, spec.cwd, spec.entryCode, this.esbuild ?? undefined, profile, this.residencyProfiles.get(key), pacer, undefined, learnedFor);
4399
+ if (offered.length > 0) {
4400
+ const staged = [];
4401
+ const unresolved = [];
4402
+ for (const entry of offered) {
4403
+ if (vfsState.bundle[entry.path] !== undefined) {
4404
+ staged.push(entry);
4405
+ continue;
4406
+ }
4407
+ // Not staged: over the budget, or nothing a regular file answers (then it is pruned).
4408
+ const stat = await Promise.resolve(vfs.stat('/' + entry.path, { followSymlinks: false })).catch(() => null);
4409
+ if (stat === null || stat.type !== 'file')
4410
+ unresolved.push(entry.path);
4411
+ }
4412
+ vfsState.profileOffer = { staged, unresolved };
4413
+ }
3882
4414
  vfsState.bundleKey = key;
3883
- vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired, pacer);
3884
- vfsState.serializedManifest = JSON.stringify(vfsState.manifest);
3885
- vfsState.serializedMetadata = JSON.stringify(vfsState.metadata);
3886
- // Two more serializations of the same weight as the passes before them.
3887
- await pacer.spend(vfsState.serializedManifest.length + vfsState.serializedMetadata.length);
3888
- // The only consumer of the raw cells past this point is a single boolean,
3889
- // so answer it now rather than hold ~17 MB (pi) to answer it later.
4415
+ vfsState.bundlePaths = Object.keys(vfsState.bundle);
4416
+ // The only consumer of the raw cells past serialization is a single
4417
+ // boolean, so it is answered first; the serialization then consumes them.
3890
4418
  vfsState.usesNodeSqlite = bundleUsesNodeSqlite(spec.entryCode, vfsState.bundle);
4419
+ // And what the module map costs the facet's store, which adopts it at boot
4420
+ // (N18): taken now, while the cells exist, for every launch this state
4421
+ // serves (a cache hit included).
4422
+ vfsState.moduleStorageBytes = moduleMapStorageBytes(vfsState.bundle);
4423
+ vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired, pacer, { consume: true });
3891
4424
  vfsState.cacheHit = false;
3892
4425
  // Serialization is total: bundleSource/serializedManifest/serializedMetadata
3893
4426
  // carry every byte the raw cells and objects do, and both generators read
@@ -4023,7 +4556,43 @@ export class FacetManager {
4023
4556
  hasResidentProcess(pid) {
4024
4557
  return this.processRpcResources.has(pid);
4025
4558
  }
4026
- noteProcessReportedExit(pid, exitCode) {
4559
+ noteProcessReportedExit(pid, exitCode, residencyMisses, evidence) {
4560
+ // Filed before the exit marks the table: the terminal hook forgets the key.
4561
+ this._recordResidencyMisses(this.residentBundleKeys.get(pid), residencyMisses);
4562
+ const exiting = this.processes.get(pid);
4563
+ const offer = this.residentProfileOffers.get(pid);
4564
+ this.residentProfileOffers.delete(pid);
4565
+ if (this.readProfile && exiting) {
4566
+ // The shared profile learns only what this supervisor served the process
4567
+ // after a miss, as the process's own credential sees it (read-profile.ts).
4568
+ const profile = this.readProfile;
4569
+ const identity = this._packageIdentity(exiting.cred);
4570
+ const view = this.vfs?.as(exiting.cred) ?? null;
4571
+ const served = evidence?.served ?? new Set();
4572
+ // Observers and vouchers are principals the router verified, read off
4573
+ // this session's Durable Object name; an anonymous session only reads.
4574
+ const principal = profilePrincipal(sessionIdentity({ ctx: this.ctx })?.tenantSegment);
4575
+ this.ctx.waitUntil((async () => {
4576
+ const tag = principal === null ? null : await principalTag(principal);
4577
+ if (tag !== null && view !== null && residencyMisses && residencyMisses.length > 0) {
4578
+ const found = await verifiedEvidence(residencyMisses, served, async (path) => {
4579
+ try {
4580
+ const st = view.lstat(path.replace(/^\/+/, ''));
4581
+ return { type: st.type, size: st.size };
4582
+ }
4583
+ catch {
4584
+ return null;
4585
+ }
4586
+ });
4587
+ if (found.length > 0)
4588
+ await profile.observe(found, tag, identity);
4589
+ }
4590
+ if (tag !== null && offer !== undefined) {
4591
+ const unread = evidence?.profileUnread ? new Set(evidence.profileUnread) : null;
4592
+ await profile.settle(offer.staged, unread, served, new Set(offer.unresolved), tag);
4593
+ }
4594
+ })().catch(() => undefined));
4595
+ }
4027
4596
  this.portRegistry.unregisterByPid(pid);
4028
4597
  this.processes.exit(pid, exitCode);
4029
4598
  const tracked = this.processRpcResources.get(pid);
@@ -4129,7 +4698,7 @@ export class FacetManager {
4129
4698
  }
4130
4699
  const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
4131
4700
  const diagSink = diagOn
4132
- ? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0, manifestBytes: 0, metadataBytes: 0 }
4701
+ ? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0 }
4133
4702
  : undefined;
4134
4703
  const abortController = new AbortController();
4135
4704
  // Ctrl-C / kill on a user program has to end the in-flight run, not just
@@ -4140,7 +4709,11 @@ export class FacetManager {
4140
4709
  const onShellAbort = () => abortController.abort();
4141
4710
  opts.signal?.addEventListener('abort', onShellAbort, { once: true });
4142
4711
  try {
4143
- const result = await this._execViaLoader(code, opts, entry, vfsState, abortController.signal, diagSink);
4712
+ // A one-shot holds its module map and what it was seen to read (both in
4713
+ // the bundle), and plans nothing beyond them: a data plan is chosen from
4714
+ // a listing of the namespace, and a one-shot takes no listing of its own
4715
+ // (§2.8: the principal's image is where that comes from).
4716
+ const result = await this._execViaLoader(code, opts, entry, vfsState, [], abortController.signal, diagSink);
4144
4717
  this._flushVfsWrites(result, entry.pid);
4145
4718
  this._recordResidencyMisses(vfsState.bundleKey, result.residencyMisses);
4146
4719
  this.processes.exit(entry.pid, result.exitCode);
@@ -4156,8 +4729,7 @@ export class FacetManager {
4156
4729
  drainPasses: result.diag?.drainPasses ?? 0,
4157
4730
  moduleMapBytes: diagSink.moduleMapBytes,
4158
4731
  bundleBytes: diagSink.bundleBytes,
4159
- manifestBytes: diagSink.manifestBytes,
4160
- metadataBytes: diagSink.metadataBytes,
4732
+ namespaceRefusals: result.diag?.namespaceRefusals ?? 0,
4161
4733
  rpcWrites: result.diag?.rpcWrites ?? 0,
4162
4734
  fsRpcReads: result.diag?.fsRpcReads ?? 0,
4163
4735
  cacheHit: vfsState.cacheHit ?? false,
@@ -4228,13 +4800,13 @@ export class FacetManager {
4228
4800
  }
4229
4801
  }
4230
4802
  // ── One-shot dynamic Worker entrypoint ────────────────────────────────
4231
- async _execViaLoader(code, opts, entry, vfsState, signal, diagSink) {
4803
+ async _execViaLoader(code, opts, entry, vfsState, dataPlan, signal, diagSink) {
4232
4804
  // Answered by _buildProcessBundle while the raw cells were still in
4233
4805
  // hand; re-deriving it here is what forced them to be retained.
4234
4806
  const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
4235
- const [sqliteModules, shims] = await Promise.all([
4807
+ const [sqliteModules, sources] = await Promise.all([
4236
4808
  this.sqliteModuleEntry(usesSqlite),
4237
- fetchNodeShimsCode(this.env),
4809
+ fetchNodeFacetSources(this.env),
4238
4810
  ]);
4239
4811
  const writerId = crypto.randomUUID();
4240
4812
  let writerActivated = false;
@@ -4250,6 +4822,7 @@ export class FacetManager {
4250
4822
  captureOutput: !!opts.captureOutput,
4251
4823
  cred: { ...entry.cred, groups: [...entry.cred.groups] },
4252
4824
  vfsCursor: vfsState.cursor,
4825
+ dataPlan,
4253
4826
  ...(diagSink ? { diag: true } : {}),
4254
4827
  });
4255
4828
  try {
@@ -4270,7 +4843,8 @@ export class FacetManager {
4270
4843
  // the map, and compiled by the loader like the sqlite sidecar.
4271
4844
  const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
4272
4845
  const wasmModules = (await this._wasmModulesByValue(entry, wasmImports));
4273
- const generatedWorker = await generateEntrypointCode(code, vfsState, usesSqlite, shims, wasmImports);
4846
+ const entryCode = await this._entryDynamicImports(code, opts.filename, opts.cwd || '/home/user');
4847
+ const generatedWorker = await generateEntrypointCode(entryCode, vfsState, usesSqlite, sources, wasmImports);
4274
4848
  if (diagSink) {
4275
4849
  diagSink.moduleMapBytes = _encodedSourceBytes(generatedWorker.code);
4276
4850
  for (const source of Object.values(generatedWorker.modules)) {
@@ -4285,8 +4859,6 @@ export class FacetManager {
4285
4859
  for (const source of Object.values(vfsState.bundleSource?.modules ?? {})) {
4286
4860
  diagSink.bundleBytes += _encodedSourceBytes(source);
4287
4861
  }
4288
- diagSink.manifestBytes = _encodedSourceBytes(vfsState.serializedManifest ?? '');
4289
- diagSink.metadataBytes = _encodedSourceBytes(vfsState.serializedMetadata ?? '');
4290
4862
  }
4291
4863
  if (!vfsState.cacheRetained)
4292
4864
  releaseGeneratedSources(vfsState);
@@ -4353,7 +4925,7 @@ export class FacetManager {
4353
4925
  // the Worker-Loader cache-miss path (with SUPERVISOR bound to THIS call's
4354
4926
  // context, which stays open for the whole run), never in this DO.
4355
4927
  const writerId = crypto.randomUUID();
4356
- const supervisor = { doId: this.ctx.id.toString(), pid: staged.pid, writerId };
4928
+ const supervisor = { ...supervisorBindingProps(this.ctx, staged.pid), writerId };
4357
4929
  const ctxExports = getNimbusCtxExports();
4358
4930
  let entrypoint;
4359
4931
  let writerActivated = false;
@@ -4434,10 +5006,10 @@ export class FacetManager {
4434
5006
  // other writes flush live through the SUPERVISOR RPC bridge.
4435
5007
  if (this.vfs)
4436
5008
  this.imageStore.ensureDir();
4437
- const processVfs = this.filesystem ? new ExecutionFs(this.filesystem.bind({ pid: entry.pid, cred: entry.cred })) : null;
5009
+ const processVfs = this.filesystem ? this.filesystem.bind({ pid: entry.pid, cred: entry.cred }) : null;
4438
5010
  const vfsState = processVfs
4439
- ? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE, undefined, undefined, this.hooks.transformLargeEsm)
4440
- : { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
5011
+ ? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE)
5012
+ : { bundle: {}, reachableCount: 0, truncated: false };
4441
5013
  const vfsBundle = _serializeBundleForFacet(vfsState.bundle);
4442
5014
  assertStagedBundleFitsRpcPayload(vfsBundle, vfsState.bundle);
4443
5015
  const stageSpec = {
@@ -4448,8 +5020,6 @@ export class FacetManager {
4448
5020
  cwd: opts.cwd,
4449
5021
  stdin: opts.stdin ?? '',
4450
5022
  vfsBundle,
4451
- vfsManifest: JSON.stringify(vfsState.manifest),
4452
- vfsMetadata: JSON.stringify(vfsState.metadata),
4453
5023
  vfsCursor: serializeFacetVfsCursor(vfsState.cursor),
4454
5024
  };
4455
5025
  return { pid, command, stageSpec };
@@ -4617,7 +5187,8 @@ export class FacetManager {
4617
5187
  // The handle's route target resolves the RUNNING facet wherever it is
4618
5188
  // hosted; binding it for the pid before the port is announced is what
4619
5189
  // lets the shim's listen()→SUPERVISOR.registerPort back-fill.
4620
- this.portRegistry.bindFacetStub(pid, handle.routeTarget);
5190
+ // The opencode runner dispatches through node-shims' __nimbusServeHttp, which strips the ACQUIRE.
5191
+ this.portRegistry.bindFacetStub(pid, handle.routeTarget, { deliversAcquire: true });
4621
5192
  this.trackProcessRpcResources(pid, [handle], { releaseOnReportExit: false });
4622
5193
  resourcesTracked = true;
4623
5194
  this.ctx.waitUntil(handle.done
@@ -4755,10 +5326,6 @@ export class FacetManager {
4755
5326
  await Promise.allSettled([...this.launchTasks]);
4756
5327
  await Promise.all(this.rowAmendments.values());
4757
5328
  }
4758
- /** Whether any launch is suspended waiting for a turn. */
4759
- get hasPendingLaunchTurns() {
4760
- return this.launchPump.hasPending;
4761
- }
4762
5329
  /** Allocate a free loopback port for a resident server facet (from 4096 up). */
4763
5330
  _allocateLoopbackPort() {
4764
5331
  for (let port = 4096; port < 4096 + 4096; port++) {
@@ -5145,7 +5712,14 @@ export class FacetManager {
5145
5712
  async _residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName, launchEnv) {
5146
5713
  const diagOn = isExecDiagEnabled();
5147
5714
  const __bundleStart = diagOn ? Date.now() : 0;
5715
+ if (this.debugEnabled)
5716
+ this.processes.appendOutput(entry.pid, 'stderr', '[nimbus-debug] launch: building the module map\n');
5148
5717
  const vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd, entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
5718
+ const planStart = Date.now();
5719
+ const { paths: dataPlan, storageBytes } = await this._planResidentData(entry, vfsState, cwd, opts.env?.HOME, pacer);
5720
+ if (this.debugEnabled) {
5721
+ this.processes.appendOutput(entry.pid, 'stderr', `[nimbus-debug] data plan: ${dataPlan.length} paths in ${Date.now() - planStart} ms (${pacer.chunks} turns so far)\n`);
5722
+ }
5149
5723
  const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
5150
5724
  // The launch-time overlay (`$PORT`, `$NIMBUS_APP` under a reservation)
5151
5725
  // rides on top of the recipe's env and is never journalled: it is
@@ -5166,13 +5740,13 @@ export class FacetManager {
5166
5740
  : spawnEnv;
5167
5741
  // Answered by _buildProcessBundle while the raw cells were still in hand.
5168
5742
  const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
5169
- const [sqliteModules, shims] = await Promise.all([
5743
+ const [sqliteModules, sources] = await Promise.all([
5170
5744
  this.sqliteModuleEntry(usesSqlite),
5171
- fetchNodeShimsCode(this.env),
5745
+ fetchNodeFacetSources(this.env),
5172
5746
  ]);
5173
5747
  // Each image is read by path when the facet loads, never by value here.
5174
5748
  const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
5175
- let generatedWorker = await generateLongRunningNodeCode(code, vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite, shims, pacer);
5749
+ let generatedWorker = await generateLongRunningNodeCode(await this._entryDynamicImports(code, opts.filename, cwd), vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite, sources, pacer);
5176
5750
  // Sized here, while the map is still in hand. Reading these after the load
5177
5751
  // would itself be what keeps the map alive, and the whole point of the
5178
5752
  // scoping below is that nothing does.
@@ -5184,10 +5758,13 @@ export class FacetManager {
5184
5758
  }
5185
5759
  moduleMapBytes = _encodedSourceBytes(generatedWorker.code) + bundleBytes;
5186
5760
  }
5187
- const manifestBytes = diagOn ? _encodedSourceBytes(vfsState.serializedManifest ?? '') : 0;
5188
- const metadataBytes = diagOn ? _encodedSourceBytes(vfsState.serializedMetadata ?? '') : 0;
5189
5761
  const cacheHit = vfsState.cacheHit ?? false;
5190
5762
  const vfsCursor = vfsState.cursor;
5763
+ if (vfsState.bundleKey)
5764
+ this.residentBundleKeys.set(entry.pid, vfsState.bundleKey);
5765
+ const profileOffer = vfsState.profileOffer;
5766
+ if (profileOffer !== undefined)
5767
+ this.residentProfileOffers.set(entry.pid, profileOffer);
5191
5768
  // The map is generated; the state's only remaining job is its cursor.
5192
5769
  // Released here, before the boot — releasing afterwards keeps the copy
5193
5770
  // alive for exactly the window that was resetting the isolate. A state
@@ -5209,6 +5786,8 @@ export class FacetManager {
5209
5786
  };
5210
5787
  generatedWorker = undefined;
5211
5788
  const vfsTextModules = await this.imageStore.materialize(entry.pid, drainSources(sources), pacer);
5789
+ if (this.debugEnabled)
5790
+ this.processes.appendOutput(entry.pid, 'stderr', '[nimbus-debug] launch: images stored, starting the facet\n');
5212
5791
  // Last gate before the facet exists. A launch now spans many turns, so
5213
5792
  // a kill can land anywhere inside it; booting a process the table has
5214
5793
  // already exited would leave a facet nothing owns, running against a
@@ -5218,7 +5797,12 @@ export class FacetManager {
5218
5797
  // The attached-TTY runner holds startProcess open for the process's
5219
5798
  // life; the server/watch runner returns once it is up.
5220
5799
  startContract: opts.attachedTty ? 'lifetime' : 'boot',
5221
- startArgs: { vfsCursor },
5800
+ startArgs: {
5801
+ vfsCursor, dataPlan,
5802
+ ...(profileOffer !== undefined && profileOffer.staged.length > 0 ? { profileStaged: profileOffer.staged.map((e) => e.path) } : {}),
5803
+ ...(this.debugEnabled ? { diag: true } : {}),
5804
+ },
5805
+ storageBytes,
5222
5806
  // A resident whose declared port is reserved binds the owner's
5223
5807
  // durable slot — the same store a durable worker spawn takes — so the
5224
5808
  // reservation's durability reaches this process's storage too.
@@ -5238,6 +5822,11 @@ export class FacetManager {
5238
5822
  },
5239
5823
  },
5240
5824
  });
5825
+ // Ended while its facet was being created (a signal's default action).
5826
+ if (this.processes.get(entry.pid)?.state !== 'running') {
5827
+ handle.kill();
5828
+ return;
5829
+ }
5241
5830
  if (diagOn) {
5242
5831
  recordExecTelemetry({
5243
5832
  command,
@@ -5254,8 +5843,7 @@ export class FacetManager {
5254
5843
  // when it stayed inline it rides inside worker.js and shows up only
5255
5844
  // in the total.
5256
5845
  bundleBytes,
5257
- manifestBytes,
5258
- metadataBytes,
5846
+ namespaceRefusals: 0,
5259
5847
  rpcWrites: 0,
5260
5848
  fsRpcReads: 0,
5261
5849
  cacheHit,
@@ -5266,7 +5854,8 @@ export class FacetManager {
5266
5854
  }
5267
5855
  this.trackProcessRpcResources(entry.pid, [handle], { releaseOnReportExit: !opts.attachedTty });
5268
5856
  resourcesTracked = true;
5269
- this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
5857
+ // A node-shims resident: its __nimbusServeHttp takes the delivered ACQUIRE off each request.
5858
+ this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget, { deliversAcquire: true });
5270
5859
  if (opts.attachedTty) {
5271
5860
  this.ctx.waitUntil(handle.done
5272
5861
  .catch((e) => {
@@ -5385,6 +5974,7 @@ export class FacetManager {
5385
5974
  let record;
5386
5975
  let durableFacetName;
5387
5976
  let launchEnv;
5977
+ const foreground = opts.foreground ? this._holdForeground(entry.pid, opts.foreground) : null;
5388
5978
  try {
5389
5979
  if (opts.durable) {
5390
5980
  // A durable spawn on a declared port starts only when a reservation the
@@ -5502,7 +6092,7 @@ export class FacetManager {
5502
6092
  this.trackProcessRpcResources(entry.pid, [handle]);
5503
6093
  resourcesTracked = true;
5504
6094
  this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
5505
- const boot = await handle.booted();
6095
+ const boot = foreground ? await Promise.race([handle.booted(), foreground.interrupted]) : await handle.booted();
5506
6096
  if (record && this.launchJournal.has(entry.pid)) {
5507
6097
  // Booted and running: the launch proved itself, so the resident
5508
6098
  // starts its running life with a fresh re-drive budget.
@@ -5545,9 +6135,38 @@ export class FacetManager {
5545
6135
  this.releaseProcessRpcResources(entry.pid);
5546
6136
  else
5547
6137
  handle?.kill();
5548
- this._failLaunch(entry.pid, 'long-running worker boot failed: ' + errorMessage(e));
6138
+ // An interrupt already killed the process; it did not fail.
6139
+ if (!opts.foreground?.signal.aborted)
6140
+ this._failLaunch(entry.pid, 'long-running worker boot failed: ' + errorMessage(e));
5549
6141
  throw e;
5550
6142
  }
6143
+ finally {
6144
+ foreground?.release();
6145
+ }
6146
+ }
6147
+ _holdForeground(pid, launch) {
6148
+ this.processes.setForeground(pid, true);
6149
+ const unsubscribe = this.processes.subscribeLogs(pid, (chunk) => launch.write(chunk.stream, chunk.data));
6150
+ let onAbort = () => { };
6151
+ const interrupted = new Promise((_, reject) => {
6152
+ onAbort = () => {
6153
+ this.kill(pid);
6154
+ reject(new DOMException('the launching command was interrupted', 'AbortError'));
6155
+ };
6156
+ });
6157
+ interrupted.catch(() => { });
6158
+ if (launch.signal.aborted)
6159
+ onAbort();
6160
+ else
6161
+ launch.signal.addEventListener('abort', onAbort, { once: true });
6162
+ return {
6163
+ interrupted,
6164
+ release: () => {
6165
+ launch.signal.removeEventListener('abort', onAbort);
6166
+ unsubscribe();
6167
+ this.processes.setForeground(pid, false);
6168
+ },
6169
+ };
5551
6170
  }
5552
6171
  /**
5553
6172
  * A resident process announcing it bound `port`.
@@ -5788,18 +6407,23 @@ export class FacetManager {
5788
6407
  catch { }
5789
6408
  }
5790
6409
  }
5791
- /** Kill a running process by PID. */
5792
- kill(pid) {
6410
+ /**
6411
+ * Kill a running process by PID. Given the signal that ends it (a name
6412
+ * without `SIG`), it exits with that signal's status, 128+signo, and its
6413
+ * exit names `SIG<name>`; without one it is SIGKILL's 137, `killed`.
6414
+ */
6415
+ kill(pid, signal) {
5793
6416
  const entry = this.processes.get(pid);
5794
6417
  if (!entry || entry.state !== 'running')
5795
6418
  return false;
6419
+ const code = signal === undefined ? 137 : exitCodeForSignal(signal);
5796
6420
  this.portRegistry.unregisterByPid(pid);
5797
6421
  this.releaseProcessRpcResources(pid);
5798
6422
  this.revokeProcessVfsWriters(pid);
5799
- const result = this.processes.kill(pid);
6423
+ const result = this.processes.kill(pid, code);
5800
6424
  if (result) {
5801
6425
  try {
5802
- this.hooks.onExternalExit?.(pid, 137, 'killed');
6426
+ this.hooks.onExternalExit?.(pid, code, signal === undefined ? 'killed' : `SIG${signal}`);
5803
6427
  }
5804
6428
  catch { }
5805
6429
  }
@@ -5959,5 +6583,5 @@ export class FacetManager {
5959
6583
  }
5960
6584
  return this.portRegistry.has(port);
5961
6585
  }
5962
- get stats() { return this.processes.stats; }
6586
+ get stats() { return { ...this.processes.stats, readProfileConflicts: this.readProfileConflicts }; }
5963
6587
  }