@nimbus-sh/worker 0.1.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.
- package/README.md +219 -0
- package/dist/_shared/bytes.d.ts +18 -0
- package/dist/_shared/bytes.d.ts.map +1 -0
- package/dist/_shared/bytes.js +17 -0
- package/dist/_shared/cache-stats.d.ts +146 -0
- package/dist/_shared/cache-stats.d.ts.map +1 -0
- package/dist/_shared/cache-stats.js +162 -0
- package/dist/_shared/exports-resolver.d.ts +76 -0
- package/dist/_shared/exports-resolver.d.ts.map +1 -0
- package/dist/_shared/exports-resolver.js +284 -0
- package/dist/_shared/install-phase.d.ts +47 -0
- package/dist/_shared/install-phase.d.ts.map +1 -0
- package/dist/_shared/install-phase.js +1 -0
- package/dist/_shared/real-node-imports.d.ts +38 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -0
- package/dist/_shared/real-node-imports.js +46 -0
- package/dist/_shared/retry.d.ts +93 -0
- package/dist/_shared/retry.d.ts.map +1 -0
- package/dist/_shared/retry.js +161 -0
- package/dist/_shared/session-id.d.ts +39 -0
- package/dist/_shared/session-id.d.ts.map +1 -0
- package/dist/_shared/session-id.js +127 -0
- package/dist/_shared/session-router.d.ts +104 -0
- package/dist/_shared/session-router.d.ts.map +1 -0
- package/dist/_shared/session-router.js +165 -0
- package/dist/_shared/w7-frame.d.ts +93 -0
- package/dist/_shared/w7-frame.d.ts.map +1 -0
- package/dist/_shared/w7-frame.js +402 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/middleware.d.ts +85 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +150 -0
- package/dist/auth/token.d.ts +96 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +226 -0
- package/dist/auth/types.d.ts +141 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +114 -0
- package/dist/bindings/d1.d.ts +102 -0
- package/dist/bindings/d1.d.ts.map +1 -0
- package/dist/bindings/d1.js +549 -0
- package/dist/bindings/kv.d.ts +82 -0
- package/dist/bindings/kv.d.ts.map +1 -0
- package/dist/bindings/kv.js +290 -0
- package/dist/bindings/r2.d.ts +137 -0
- package/dist/bindings/r2.d.ts.map +1 -0
- package/dist/bindings/r2.js +379 -0
- package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
- package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
- package/dist/cirrus-npm-cjs.generated.js +41 -0
- package/dist/cirrus-plugin-react.generated.d.ts +34 -0
- package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
- package/dist/cirrus-plugin-react.generated.js +33 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +116 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
- package/dist/esbuild-wasm-bundle.generated.js +40 -0
- package/dist/facets/cirrus-real.d.ts +147 -0
- package/dist/facets/cirrus-real.d.ts.map +1 -0
- package/dist/facets/cirrus-real.js +981 -0
- package/dist/facets/inner-do-registry.d.ts +41 -0
- package/dist/facets/inner-do-registry.d.ts.map +1 -0
- package/dist/facets/inner-do-registry.js +51 -0
- package/dist/facets/manager.d.ts +314 -0
- package/dist/facets/manager.d.ts.map +1 -0
- package/dist/facets/manager.js +2681 -0
- package/dist/facets/process.d.ts +318 -0
- package/dist/facets/process.d.ts.map +1 -0
- package/dist/facets/process.js +567 -0
- package/dist/facets/real-vite-fs-shim.d.ts +151 -0
- package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
- package/dist/facets/real-vite-fs-shim.js +1279 -0
- package/dist/facets/real-vite-hmr.d.ts +142 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -0
- package/dist/facets/real-vite-hmr.js +476 -0
- package/dist/facets/vite-dev-server.d.ts +293 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -0
- package/dist/facets/vite-dev-server.js +2580 -0
- package/dist/facets/wasm-swap-registry.d.ts +261 -0
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
- package/dist/facets/wasm-swap-registry.js +551 -0
- package/dist/facets/ws-terminal.d.ts +107 -0
- package/dist/facets/ws-terminal.d.ts.map +1 -0
- package/dist/facets/ws-terminal.js +207 -0
- package/dist/git/commands.d.ts +13 -0
- package/dist/git/commands.d.ts.map +1 -0
- package/dist/git/commands.js +633 -0
- package/dist/git/network-facet.d.ts +61 -0
- package/dist/git/network-facet.d.ts.map +1 -0
- package/dist/git/network-facet.js +700 -0
- package/dist/git-bundle.generated.d.ts +16 -0
- package/dist/git-bundle.generated.d.ts.map +1 -0
- package/dist/git-bundle.generated.js +15 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-facet.js +103 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-pool.js +161 -0
- package/dist/loaders/fanout-pool.d.ts +195 -0
- package/dist/loaders/fanout-pool.d.ts.map +1 -0
- package/dist/loaders/fanout-pool.js +326 -0
- package/dist/loaders/generated-workers.d.ts +26 -0
- package/dist/loaders/generated-workers.d.ts.map +1 -0
- package/dist/loaders/generated-workers.js +25 -0
- package/dist/loaders/index.d.ts +15 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +13 -0
- package/dist/loaders/loader-pool.d.ts +273 -0
- package/dist/loaders/loader-pool.d.ts.map +1 -0
- package/dist/loaders/loader-pool.js +624 -0
- package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
- package/dist/loaders/npm-resolve-preamble.js +237 -0
- package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
- package/dist/loaders/pre-bundle-preamble.js +114 -0
- package/dist/loaders/vendor/codegen.d.ts +24 -0
- package/dist/loaders/vendor/codegen.d.ts.map +1 -0
- package/dist/loaders/vendor/codegen.js +44 -0
- package/dist/loaders/vendor/errors.d.ts +24 -0
- package/dist/loaders/vendor/errors.d.ts.map +1 -0
- package/dist/loaders/vendor/errors.js +46 -0
- package/dist/loaders/vendor/index.d.ts +16 -0
- package/dist/loaders/vendor/index.d.ts.map +1 -0
- package/dist/loaders/vendor/index.js +11 -0
- package/dist/loaders/vendor/pool.d.ts +88 -0
- package/dist/loaders/vendor/pool.d.ts.map +1 -0
- package/dist/loaders/vendor/pool.js +353 -0
- package/dist/loaders/vendor/primitives.d.ts +11 -0
- package/dist/loaders/vendor/primitives.d.ts.map +1 -0
- package/dist/loaders/vendor/primitives.js +33 -0
- package/dist/loaders/vendor/serialize.d.ts +3 -0
- package/dist/loaders/vendor/serialize.d.ts.map +1 -0
- package/dist/loaders/vendor/serialize.js +25 -0
- package/dist/loaders/vendor/types.d.ts +60 -0
- package/dist/loaders/vendor/types.d.ts.map +1 -0
- package/dist/loaders/vendor/types.js +4 -0
- package/dist/npm/cache.d.ts +136 -0
- package/dist/npm/cache.d.ts.map +1 -0
- package/dist/npm/cache.js +429 -0
- package/dist/npm/install-batch-facet.d.ts +129 -0
- package/dist/npm/install-batch-facet.d.ts.map +1 -0
- package/dist/npm/install-batch-facet.js +598 -0
- package/dist/npm/install-facet.d.ts +29 -0
- package/dist/npm/install-facet.d.ts.map +1 -0
- package/dist/npm/install-facet.js +9 -0
- package/dist/npm/installer.d.ts +239 -0
- package/dist/npm/installer.d.ts.map +1 -0
- package/dist/npm/installer.js +2022 -0
- package/dist/npm/npx-install.d.ts +74 -0
- package/dist/npm/npx-install.d.ts.map +1 -0
- package/dist/npm/npx-install.js +302 -0
- package/dist/npm/pre-bundle-facet.d.ts +168 -0
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
- package/dist/npm/pre-bundle-facet.js +653 -0
- package/dist/npm/r2-cache.d.ts +286 -0
- package/dist/npm/r2-cache.d.ts.map +1 -0
- package/dist/npm/r2-cache.js +506 -0
- package/dist/npm/resolve-facet.d.ts +202 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -0
- package/dist/npm/resolve-facet.js +833 -0
- package/dist/npm/resolve-one-facet.d.ts +204 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -0
- package/dist/npm/resolve-one-facet.js +437 -0
- package/dist/npm/resolver.d.ts +109 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/resolver.js +899 -0
- package/dist/npm/tarball-stream.d.ts +85 -0
- package/dist/npm/tarball-stream.d.ts.map +1 -0
- package/dist/npm/tarball-stream.js +205 -0
- package/dist/npm/tarball.d.ts +51 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/npm/tarball.js +185 -0
- package/dist/observability/diag-counters.d.ts +292 -0
- package/dist/observability/diag-counters.d.ts.map +1 -0
- package/dist/observability/diag-counters.js +302 -0
- package/dist/observability/heap-estimate.d.ts +166 -0
- package/dist/observability/heap-estimate.d.ts.map +1 -0
- package/dist/observability/heap-estimate.js +192 -0
- package/dist/observability/heavy-alloc-coord.d.ts +95 -0
- package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
- package/dist/observability/heavy-alloc-coord.js +149 -0
- package/dist/observability/oom-classify.d.ts +56 -0
- package/dist/observability/oom-classify.d.ts.map +1 -0
- package/dist/observability/oom-classify.js +118 -0
- package/dist/observability/oom-discriminator.d.ts +192 -0
- package/dist/observability/oom-discriminator.d.ts.map +1 -0
- package/dist/observability/oom-discriminator.js +233 -0
- package/dist/real-vite-bundle.generated.d.ts +46 -0
- package/dist/real-vite-bundle.generated.d.ts.map +1 -0
- package/dist/real-vite-bundle.generated.js +53 -0
- package/dist/replica/routing.d.ts +171 -0
- package/dist/replica/routing.d.ts.map +1 -0
- package/dist/replica/routing.js +288 -0
- package/dist/replica/suspension.d.ts +36 -0
- package/dist/replica/suspension.d.ts.map +1 -0
- package/dist/replica/suspension.js +49 -0
- package/dist/router/index.d.ts +156 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +220 -0
- package/dist/runtime/assets-loader.d.ts +71 -0
- package/dist/runtime/assets-loader.d.ts.map +1 -0
- package/dist/runtime/assets-loader.js +128 -0
- package/dist/runtime/barrel-detect.d.ts +46 -0
- package/dist/runtime/barrel-detect.d.ts.map +1 -0
- package/dist/runtime/barrel-detect.js +73 -0
- package/dist/runtime/barrel-synthesizer.d.ts +181 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
- package/dist/runtime/barrel-synthesizer.js +541 -0
- package/dist/runtime/bun-repl.d.ts +46 -0
- package/dist/runtime/bun-repl.d.ts.map +1 -0
- package/dist/runtime/bun-repl.js +326 -0
- package/dist/runtime/bun-runner.d.ts +66 -0
- package/dist/runtime/bun-runner.d.ts.map +1 -0
- package/dist/runtime/bun-runner.js +219 -0
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +1033 -0
- package/dist/runtime/comment-strip.d.ts +37 -0
- package/dist/runtime/comment-strip.d.ts.map +1 -0
- package/dist/runtime/comment-strip.js +74 -0
- package/dist/runtime/esbuild-service.d.ts +201 -0
- package/dist/runtime/esbuild-service.d.ts.map +1 -0
- package/dist/runtime/esbuild-service.js +1444 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
- package/dist/runtime/esbuild-wasm-bytes.js +131 -0
- package/dist/runtime/framework-detect.d.ts +71 -0
- package/dist/runtime/framework-detect.d.ts.map +1 -0
- package/dist/runtime/framework-detect.js +178 -0
- package/dist/runtime/long-running-handle.d.ts +133 -0
- package/dist/runtime/long-running-handle.d.ts.map +1 -0
- package/dist/runtime/long-running-handle.js +192 -0
- package/dist/runtime/node-repl.d.ts +36 -0
- package/dist/runtime/node-repl.d.ts.map +1 -0
- package/dist/runtime/node-repl.js +293 -0
- package/dist/runtime/node-runner.d.ts +111 -0
- package/dist/runtime/node-runner.d.ts.map +1 -0
- package/dist/runtime/node-runner.js +194 -0
- package/dist/runtime/node-shims.d.ts +24 -0
- package/dist/runtime/node-shims.d.ts.map +1 -0
- package/dist/runtime/node-shims.js +3019 -0
- package/dist/runtime/package-manager.d.ts +118 -0
- package/dist/runtime/package-manager.d.ts.map +1 -0
- package/dist/runtime/package-manager.js +420 -0
- package/dist/runtime/port-registry.d.ts +80 -0
- package/dist/runtime/port-registry.d.ts.map +1 -0
- package/dist/runtime/port-registry.js +166 -0
- package/dist/runtime/process-logs-api.d.ts +102 -0
- package/dist/runtime/process-logs-api.d.ts.map +1 -0
- package/dist/runtime/process-logs-api.js +253 -0
- package/dist/runtime/process-logs.d.ts +311 -0
- package/dist/runtime/process-logs.d.ts.map +1 -0
- package/dist/runtime/process-logs.js +656 -0
- package/dist/runtime/process-table.d.ts +65 -0
- package/dist/runtime/process-table.d.ts.map +1 -0
- package/dist/runtime/process-table.js +107 -0
- package/dist/runtime/project-detect.d.ts +21 -0
- package/dist/runtime/project-detect.d.ts.map +1 -0
- package/dist/runtime/project-detect.js +46 -0
- package/dist/runtime/python-repl.d.ts +66 -0
- package/dist/runtime/python-repl.d.ts.map +1 -0
- package/dist/runtime/python-repl.js +563 -0
- package/dist/runtime/python-runner.d.ts +50 -0
- package/dist/runtime/python-runner.d.ts.map +1 -0
- package/dist/runtime/python-runner.js +870 -0
- package/dist/runtime/repl-session.d.ts +160 -0
- package/dist/runtime/repl-session.d.ts.map +1 -0
- package/dist/runtime/repl-session.js +393 -0
- package/dist/runtime/require-resolver.d.ts +52 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -0
- package/dist/runtime/require-resolver.js +690 -0
- package/dist/runtime/router-basename.d.ts +36 -0
- package/dist/runtime/router-basename.d.ts.map +1 -0
- package/dist/runtime/router-basename.js +310 -0
- package/dist/runtime/ruby-repl.d.ts +43 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -0
- package/dist/runtime/ruby-repl.js +229 -0
- package/dist/runtime/ruby-runner.d.ts +62 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +737 -0
- package/dist/runtime/runtime-catalog.d.ts +99 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/runtime/runtime-catalog.js +183 -0
- package/dist/runtime/runtime-registry.d.ts +136 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +318 -0
- package/dist/runtime/streams.d.ts +13 -0
- package/dist/runtime/streams.d.ts.map +1 -0
- package/dist/runtime/streams.js +402 -0
- package/dist/runtime/wasi-instance.d.ts +190 -0
- package/dist/runtime/wasi-instance.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.js +2044 -0
- package/dist/runtime/wasm-runner.d.ts +117 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +705 -0
- package/dist/session/bindings.d.ts +168 -0
- package/dist/session/bindings.d.ts.map +1 -0
- package/dist/session/bindings.js +524 -0
- package/dist/session/ctx-exports.d.ts +16 -0
- package/dist/session/ctx-exports.d.ts.map +1 -0
- package/dist/session/ctx-exports.js +22 -0
- package/dist/session/diag.d.ts +62 -0
- package/dist/session/diag.d.ts.map +1 -0
- package/dist/session/diag.js +118 -0
- package/dist/session/fs-watch.d.ts +132 -0
- package/dist/session/fs-watch.d.ts.map +1 -0
- package/dist/session/fs-watch.js +255 -0
- package/dist/session/helpers.d.ts +150 -0
- package/dist/session/helpers.d.ts.map +1 -0
- package/dist/session/helpers.js +472 -0
- package/dist/session/hibernation.d.ts +145 -0
- package/dist/session/hibernation.d.ts.map +1 -0
- package/dist/session/hibernation.js +385 -0
- package/dist/session/init-phases.d.ts +87 -0
- package/dist/session/init-phases.d.ts.map +1 -0
- package/dist/session/init-phases.js +137 -0
- package/dist/session/init.d.ts +46 -0
- package/dist/session/init.d.ts.map +1 -0
- package/dist/session/init.js +3579 -0
- package/dist/session/keys.d.ts +56 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +55 -0
- package/dist/session/nimbus-session.d.ts +434 -0
- package/dist/session/nimbus-session.d.ts.map +1 -0
- package/dist/session/nimbus-session.js +1084 -0
- package/dist/session/programmatic.d.ts +105 -0
- package/dist/session/programmatic.d.ts.map +1 -0
- package/dist/session/programmatic.js +326 -0
- package/dist/session/replica-routes.d.ts +68 -0
- package/dist/session/replica-routes.d.ts.map +1 -0
- package/dist/session/replica-routes.js +77 -0
- package/dist/session/routes.d.ts +29 -0
- package/dist/session/routes.d.ts.map +1 -0
- package/dist/session/routes.js +1331 -0
- package/dist/session/rpc.d.ts +323 -0
- package/dist/session/rpc.d.ts.map +1 -0
- package/dist/session/rpc.js +808 -0
- package/dist/session/state-store.d.ts +210 -0
- package/dist/session/state-store.d.ts.map +1 -0
- package/dist/session/state-store.js +459 -0
- package/dist/session/supervisor-rpc.d.ts +269 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -0
- package/dist/session/supervisor-rpc.js +437 -0
- package/dist/session/ws-hibernation-config.d.ts +64 -0
- package/dist/session/ws-hibernation-config.d.ts.map +1 -0
- package/dist/session/ws-hibernation-config.js +89 -0
- package/dist/session/ws.d.ts +95 -0
- package/dist/session/ws.d.ts.map +1 -0
- package/dist/session/ws.js +395 -0
- package/dist/shell/features.d.ts +410 -0
- package/dist/shell/features.d.ts.map +1 -0
- package/dist/shell/features.js +1885 -0
- package/dist/shell/unix-commands.d.ts +15 -0
- package/dist/shell/unix-commands.d.ts.map +1 -0
- package/dist/shell/unix-commands.js +3703 -0
- package/dist/stubs/lifo-ui.d.ts +10 -0
- package/dist/stubs/lifo-ui.d.ts.map +1 -0
- package/dist/stubs/lifo-ui.js +1 -0
- package/dist/tailwind-play.generated.d.ts +19 -0
- package/dist/tailwind-play.generated.d.ts.map +1 -0
- package/dist/tailwind-play.generated.js +18 -0
- package/dist/vfs/dev-provider.d.ts +64 -0
- package/dist/vfs/dev-provider.d.ts.map +1 -0
- package/dist/vfs/dev-provider.js +191 -0
- package/dist/vfs/events.d.ts +51 -0
- package/dist/vfs/events.d.ts.map +1 -0
- package/dist/vfs/events.js +127 -0
- package/dist/vfs/path.d.ts +30 -0
- package/dist/vfs/path.d.ts.map +1 -0
- package/dist/vfs/path.js +41 -0
- package/dist/vfs/seed-project.d.ts +62 -0
- package/dist/vfs/seed-project.d.ts.map +1 -0
- package/dist/vfs/seed-project.js +1045 -0
- package/dist/vfs/sqlite-vfs.d.ts +435 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
- package/dist/vfs/sqlite-vfs.js +1670 -0
- package/dist/vfs/symlink-registry.d.ts +64 -0
- package/dist/vfs/symlink-registry.d.ts.map +1 -0
- package/dist/vfs/symlink-registry.js +137 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
- package/dist/wrangler/nimbus-wrangler.js +931 -0
- package/package.json +77 -0
- package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
- package/public/_assets/esbuild-0.24.2.wasm +0 -0
- package/public/_assets/real-vite-bundle.js +101478 -0
- package/public/_assets/rollup.wasm +0 -0
- package/public/favicon.svg +8 -0
- package/public/index.html +565 -0
- package/public/s/index.html +2379 -0
- package/scripts/bundle-esbuild-wasm.mjs +244 -0
- package/scripts/bundle-facet-workers.mjs +137 -0
- package/scripts/bundle-git.mjs +118 -0
- package/scripts/bundle-npm-cjs.mjs +363 -0
- package/scripts/bundle-plugin-react.mjs +480 -0
- package/scripts/bundle-real-vite.mjs +959 -0
- package/scripts/bundle-runtime.mjs +600 -0
- package/scripts/bundle-tailwind-play.mjs +130 -0
- package/scripts/patch-lifo-ui.mjs +131 -0
|
@@ -0,0 +1,2681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* facets/manager.ts — Lifecycle for "user node script as a child DO".
|
|
3
|
+
*
|
|
4
|
+
* `node script.js` from the shell prompt has to run somewhere isolated
|
|
5
|
+
* — same memory bound as the supervisor (128 MiB) but separate so a
|
|
6
|
+
* runaway script can't take the supervisor down. The script is also
|
|
7
|
+
* stateful: `fs.readFileSync` writes inside the script need to flow
|
|
8
|
+
* back to the VFS, and `http.createServer` registers a port the
|
|
9
|
+
* preview iframe later proxies to. That makes a stateless Worker
|
|
10
|
+
* Loader insufficient — we need a child DO with its own SQLite for
|
|
11
|
+
* port registration and exit reporting.
|
|
12
|
+
*
|
|
13
|
+
* The pattern:
|
|
14
|
+
* 1. LOADER.get(codeHash, makeConfig) — dynamic worker w/ user script
|
|
15
|
+
* 2. worker.getDurableObjectClass('NodeProcess') — class from that worker
|
|
16
|
+
* 3. ctx.facets.get(`proc-${pid}`, {class}) — child DO Facet
|
|
17
|
+
* 4. facet.run(argsJson) — RPC executes the script
|
|
18
|
+
* 5. ctx.facets.delete(name) — cleanup, even on throw
|
|
19
|
+
*
|
|
20
|
+
* The codeHash includes `ctx.id.toString()` to scope the LOADER cache
|
|
21
|
+
* per-supervisor-DO. Without that, two sessions executing identical
|
|
22
|
+
* code would share an isolate, and the warm slot's baked-in
|
|
23
|
+
* env.SUPERVISOR stub would point at whichever DO instantiated it
|
|
24
|
+
* first — every other session's stdout would silently route to the
|
|
25
|
+
* wrong terminal. (Same cross-session-slot-sharing fix b225db3
|
|
26
|
+
* applied to install-time facets.)
|
|
27
|
+
*
|
|
28
|
+
* `_execViaLoader` is the local-dev fallback: when the runtime doesn't
|
|
29
|
+
* support `ctx.facets.get`, fall back to `LOADER.load()` +
|
|
30
|
+
* `getEntrypoint().fetch()` with a plain fetch-handler shape. The
|
|
31
|
+
* supervisor's environment is the same; only the lifecycle primitive
|
|
32
|
+
* differs.
|
|
33
|
+
*/
|
|
34
|
+
import { generateShimsCode } from '../runtime/node-shims.js';
|
|
35
|
+
import { getRealNodeImportsCode } from '../_shared/real-node-imports.js';
|
|
36
|
+
import { getCtxExports } from '../session/ctx-exports.js';
|
|
37
|
+
import { prefetchForRequire } from '../runtime/require-resolver.js';
|
|
38
|
+
import { recordFailure, getLastRpcFrame, getLastFacetId } from '../observability/oom-discriminator.js';
|
|
39
|
+
import { classifyError } from '../observability/oom-classify.js';
|
|
40
|
+
import { EsbuildService } from '../runtime/esbuild-service.js';
|
|
41
|
+
import { CF_COMPAT_DATE, FACET_TIMEOUT_MS, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, BUNDLE_MAX_ENCODED_BYTES, } from '../constants.js';
|
|
42
|
+
/**
|
|
43
|
+
* Detect & restore a Uint8Array that's been JSON-mangled to a
|
|
44
|
+
* {"0":n,"1":n,...} object during the result-envelope round-trip.
|
|
45
|
+
* String inputs and already-Uint8Array inputs pass through unchanged.
|
|
46
|
+
*
|
|
47
|
+
* Heuristic: a plain object whose keys are dense non-negative integers
|
|
48
|
+
* starting at 0 and whose values are byte-sized integers is treated as
|
|
49
|
+
* a serialized Uint8Array. False-positive risk is negligible because
|
|
50
|
+
* (a) only `__vfsWrites` cells reach this path and (b) the only types
|
|
51
|
+
* that ever land in `__vfsWrites` are string and Uint8Array.
|
|
52
|
+
*/
|
|
53
|
+
function _reviveVfsWriteCell(v) {
|
|
54
|
+
if (typeof v === 'string')
|
|
55
|
+
return v;
|
|
56
|
+
if (v instanceof Uint8Array)
|
|
57
|
+
return v;
|
|
58
|
+
if (v && typeof v === 'object') {
|
|
59
|
+
const o = v;
|
|
60
|
+
const keys = Object.keys(o);
|
|
61
|
+
if (keys.length === 0)
|
|
62
|
+
return new Uint8Array(0);
|
|
63
|
+
// Quick bail-out: not all keys are non-negative integers.
|
|
64
|
+
let maxIdx = -1;
|
|
65
|
+
for (const k of keys) {
|
|
66
|
+
const n = Number(k);
|
|
67
|
+
if (!Number.isInteger(n) || n < 0)
|
|
68
|
+
return String(v);
|
|
69
|
+
if (n > maxIdx)
|
|
70
|
+
maxIdx = n;
|
|
71
|
+
}
|
|
72
|
+
// Dense check: keys.length === maxIdx + 1
|
|
73
|
+
if (keys.length !== maxIdx + 1)
|
|
74
|
+
return String(v);
|
|
75
|
+
const out = new Uint8Array(keys.length);
|
|
76
|
+
for (let i = 0; i < keys.length; i++) {
|
|
77
|
+
const b = o[String(i)];
|
|
78
|
+
if (typeof b !== 'number' || b < 0 || b > 255)
|
|
79
|
+
return String(v);
|
|
80
|
+
out[i] = b;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
return String(v);
|
|
85
|
+
}
|
|
86
|
+
// ── Code generators ─────────────────────────────────────────────────────
|
|
87
|
+
const SHIMS = generateShimsCode();
|
|
88
|
+
/**
|
|
89
|
+
* Static `import * as __real_X from 'node:X'` block. Prepended to both
|
|
90
|
+
* facet-code templates (NodeProcess + LOADER.load fallback) so the
|
|
91
|
+
* SHIMS string can forward to workerd's real `node:*` builtins.
|
|
92
|
+
* See src/_shared/real-node-imports.ts for the rationale + matrix.
|
|
93
|
+
*/
|
|
94
|
+
const REAL_NODE_IMPORTS = getRealNodeImportsCode();
|
|
95
|
+
/** Simple hash for deduplicating identical code across invocations. */
|
|
96
|
+
function hashCode(s) {
|
|
97
|
+
let h = 0;
|
|
98
|
+
for (let i = 0; i < s.length; i++)
|
|
99
|
+
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
100
|
+
return 'code-' + (h >>> 0).toString(36);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate vite dev server facet code.
|
|
104
|
+
* Long-running dynamic worker that serves files via SUPERVISOR RPC.
|
|
105
|
+
* Transforms TS/TSX/JSX via SUPERVISOR.transform() (esbuild in supervisor).
|
|
106
|
+
*/
|
|
107
|
+
function generateViteFacetCode(root, basePath) {
|
|
108
|
+
const safeRoot = JSON.stringify(root);
|
|
109
|
+
const safeBase = JSON.stringify(basePath);
|
|
110
|
+
return `
|
|
111
|
+
const ROOT = ${safeRoot};
|
|
112
|
+
const BASE = ${safeBase};
|
|
113
|
+
|
|
114
|
+
const MIME = {
|
|
115
|
+
'.html':'text/html;charset=utf-8', '.js':'application/javascript;charset=utf-8',
|
|
116
|
+
'.mjs':'application/javascript;charset=utf-8', '.ts':'application/javascript;charset=utf-8',
|
|
117
|
+
'.tsx':'application/javascript;charset=utf-8', '.jsx':'application/javascript;charset=utf-8',
|
|
118
|
+
'.css':'text/css;charset=utf-8', '.json':'application/json;charset=utf-8',
|
|
119
|
+
'.svg':'image/svg+xml', '.png':'image/png', '.jpg':'image/jpeg',
|
|
120
|
+
'.ico':'image/x-icon', '.woff2':'font/woff2', '.txt':'text/plain;charset=utf-8',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const HMR_CLIENT = '<script type="module">window.addEventListener("message",e=>{if(e.data?.type==="nimbus-hmr"){if(e.data.event==="full-reload")location.reload();if(e.data.event==="css-update")document.querySelectorAll("link[rel=stylesheet]").forEach(l=>{l.href=l.href.split("?")[0]+"?t="+Date.now();})}});console.log("[nimbus-hmr] connected");</script>';
|
|
124
|
+
|
|
125
|
+
function ext(p) { const i = p.lastIndexOf('.'); return i > 0 ? p.substring(i) : ''; }
|
|
126
|
+
function strip(p) { return p.replace(/^\\/+/,''); }
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
async fetch(request, workerEnv) {
|
|
130
|
+
const sup = workerEnv?.SUPERVISOR;
|
|
131
|
+
const url = new URL(request.url);
|
|
132
|
+
let pathname = url.pathname;
|
|
133
|
+
const headers = { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' };
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// / or /index.html → serve HTML with HMR + path rewriting
|
|
137
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
138
|
+
const htmlPath = strip(ROOT + '/index.html');
|
|
139
|
+
let html = sup ? await sup.readFile(htmlPath) : null;
|
|
140
|
+
if (!html) return new Response('<!DOCTYPE html><html><body><h1>No index.html</h1></body></html>', { headers: {...headers, 'Content-Type': 'text/html;charset=utf-8'} });
|
|
141
|
+
// Inject HMR client
|
|
142
|
+
if (html.includes('</head>')) html = html.replace('</head>', HMR_CLIENT + '</head>');
|
|
143
|
+
else html = HMR_CLIENT + html;
|
|
144
|
+
// Rewrite absolute paths to include basePath
|
|
145
|
+
if (BASE && BASE !== '/') {
|
|
146
|
+
html = html.replace(/(\\s(?:src|href|action)=)(["'])(\\/((?!\\/)[^"']*))(\\2)/gi, (m,attr,q,path) => {
|
|
147
|
+
if (path.startsWith(BASE+'/') || path === BASE) return m;
|
|
148
|
+
return attr + q + BASE + path + q;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Detect importmap (skip bare import rewriting if present)
|
|
152
|
+
const hasImportmap = html.includes('"importmap"');
|
|
153
|
+
return new Response(html, { headers: {...headers, 'Content-Type': 'text/html;charset=utf-8'}, status: 200 });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Static/transformed file serving
|
|
157
|
+
const vfsPath = strip(ROOT + pathname);
|
|
158
|
+
const e = ext(pathname);
|
|
159
|
+
|
|
160
|
+
// TS/TSX/JSX → transform via SUPERVISOR.transform()
|
|
161
|
+
if (e === '.ts' || e === '.tsx' || e === '.jsx') {
|
|
162
|
+
let code = sup ? await sup.readFile(vfsPath) : null;
|
|
163
|
+
if (!code) return new Response('Not found: ' + pathname, { status: 404, headers });
|
|
164
|
+
// Detect JSX framework
|
|
165
|
+
const hasPreact = code.includes('from "preact"') || code.includes("from 'preact'");
|
|
166
|
+
const loader = e === '.tsx' ? 'tsx' : e === '.jsx' ? 'jsx' : 'ts';
|
|
167
|
+
if (sup) {
|
|
168
|
+
try {
|
|
169
|
+
const result = await sup.transform(code, loader);
|
|
170
|
+
if (result) code = result.code;
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
return new Response(code, { headers: {...headers, 'Content-Type': 'application/javascript;charset=utf-8'} });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// CSS
|
|
177
|
+
if (e === '.css') {
|
|
178
|
+
const css = sup ? await sup.readFile(vfsPath) : null;
|
|
179
|
+
if (!css) return new Response('Not found', { status: 404, headers });
|
|
180
|
+
return new Response(css, { headers: {...headers, 'Content-Type': 'text/css;charset=utf-8'} });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// JS files
|
|
184
|
+
if (e === '.js' || e === '.mjs') {
|
|
185
|
+
const code = sup ? await sup.readFile(vfsPath) : null;
|
|
186
|
+
if (!code) return new Response('Not found', { status: 404, headers });
|
|
187
|
+
return new Response(code, { headers: {...headers, 'Content-Type': 'application/javascript;charset=utf-8'} });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Other files
|
|
191
|
+
if (sup) {
|
|
192
|
+
const content = await sup.readFile(vfsPath);
|
|
193
|
+
if (content) {
|
|
194
|
+
const ct = MIME[e] || 'application/octet-stream';
|
|
195
|
+
return new Response(content, { headers: {...headers, 'Content-Type': ct} });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// SPA fallback
|
|
200
|
+
const accept = request.headers.get('Accept') || '';
|
|
201
|
+
if (!pathname.includes('.') && (accept.includes('text/html') || accept.includes('*/*'))) {
|
|
202
|
+
// Recurse to serve index.html
|
|
203
|
+
return this.fetch(new Request(url.origin + '/', request), workerEnv);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return new Response('Not found: ' + pathname, { status: 404, headers });
|
|
207
|
+
} catch (e) {
|
|
208
|
+
return new Response('Vite facet error: ' + (e?.message || e), { status: 500, headers });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generate dynamic worker code with a DurableObject class.
|
|
216
|
+
* This is used with LOADER.get() + ctx.facets.get() (production path).
|
|
217
|
+
*/
|
|
218
|
+
function generateFacetCode(userCode, vfsState) {
|
|
219
|
+
const safeCode = JSON.stringify(userCode);
|
|
220
|
+
// hardening-r5: route through binary-safe serializer so Uint8Array
|
|
221
|
+
// cells survive the module-text round-trip.
|
|
222
|
+
const safeBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
223
|
+
const safeManifest = JSON.stringify(vfsState.manifest);
|
|
224
|
+
return `
|
|
225
|
+
import { DurableObject } from "cloudflare:workers";
|
|
226
|
+
${REAL_NODE_IMPORTS}
|
|
227
|
+
|
|
228
|
+
const USER_CODE = ${safeCode};
|
|
229
|
+
|
|
230
|
+
// X.5-S: conditional-param-rename wrap for new Function. esbuild ESM→CJS
|
|
231
|
+
// transform output (W3.5 Fix B) preserves any source-level
|
|
232
|
+
// \`const __dirname = path.dirname(fileURLToPath(import.meta.url))\` line
|
|
233
|
+
// (vite's open@10 idiom), which collides at parse time with a hardcoded
|
|
234
|
+
// \`__dirname\` parameter. Rename the param slot to a placeholder when
|
|
235
|
+
// the body declares the same name — slot alignment is preserved
|
|
236
|
+
// (callers pass positional args; dropping would mis-align downstream
|
|
237
|
+
// slots), and the body's own binding becomes the single declarer.
|
|
238
|
+
//
|
|
239
|
+
// Generalised by nuxt-process-redeclare wave (2026-05-11) to also cover
|
|
240
|
+
// the 7 extra-param shim names (\`process\`, \`console\`, \`Buffer\`,
|
|
241
|
+
// \`setTimeout\`, \`setInterval\`, \`clearTimeout\`, \`clearInterval\`).
|
|
242
|
+
// Pre-fix, the two-pass esbuild ESM→CJS path emitted \`const process =
|
|
243
|
+
// (() => { const _m = require("node:process"); ... })();\` in the
|
|
244
|
+
// requires block for \`import process from 'node:process'\` (nuxi.mjs
|
|
245
|
+
// shape). That const declaration sits at the top-level of the wrapped
|
|
246
|
+
// function body and collides with the \`process\` param at parse time:
|
|
247
|
+
// SyntaxError: Identifier 'process' has already been declared.
|
|
248
|
+
// Same class for the other 6 shim names. esbuild's single-pass CJS
|
|
249
|
+
// path is unaffected (it renames the user's binding to \`process$1\`).
|
|
250
|
+
//
|
|
251
|
+
// Regex extended from (const|let|var) to (const|let|var|function|class)
|
|
252
|
+
// as defensive coverage for future shapes — current convertEsmImportsToRequire
|
|
253
|
+
// only emits \`const\` in the requires block, but the broader regex
|
|
254
|
+
// guards against drift.
|
|
255
|
+
//
|
|
256
|
+
// See VERIFY-23417C5 §4 #1, audit/sections/X5S-plan.md §3,
|
|
257
|
+
// .seal-internal/2026-05-11-nuxt-process-redeclare/plan.md §6.
|
|
258
|
+
function __mkCompiledFn(code, extraParams) {
|
|
259
|
+
function renameIfDeclared(name) {
|
|
260
|
+
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "\\\\b", "m");
|
|
261
|
+
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
262
|
+
}
|
|
263
|
+
const baseParams = [
|
|
264
|
+
"exports", "require", "module",
|
|
265
|
+
renameIfDeclared("__filename"),
|
|
266
|
+
renameIfDeclared("__dirname"),
|
|
267
|
+
];
|
|
268
|
+
const renamedExtras = (extraParams || []).map(renameIfDeclared);
|
|
269
|
+
return new Function(...baseParams, ...renamedExtras, code);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const __compiledFn = __mkCompiledFn(USER_CODE, [
|
|
273
|
+
"console", "process", "Buffer",
|
|
274
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
275
|
+
"global", "__args",
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// VFS bundle + manifest + pre-compiled modules — all at module level (startup time).
|
|
279
|
+
// __MODULE_VFS_BUNDLE = capped path→content (workerd module-size budget).
|
|
280
|
+
// __MODULE_VFS_MANIFEST = uncapped path→child-names map for directory shape
|
|
281
|
+
// (W2.5b root-cause fix — see facet-manager.ts:453).
|
|
282
|
+
const __MODULE_VFS_BUNDLE = ${safeBundle};
|
|
283
|
+
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
284
|
+
const __compiledModules = new Map();
|
|
285
|
+
// W3.5 Fix C: stop swallowing pre-compile errors. Record the path → error
|
|
286
|
+
// message so __loadModule can surface the real reason (typically
|
|
287
|
+
// SyntaxError on ESM source) instead of the misleading "file was not
|
|
288
|
+
// pre-bundled" message at request time.
|
|
289
|
+
const __compileFailures = new Map();
|
|
290
|
+
// G2/G3 (runtime-pkg wave): sub-modules receive the SAME shim params
|
|
291
|
+
// as the entry. Pre-fix, sub-module precompiled fns took only
|
|
292
|
+
// (exports, require, module, __filename, __dirname); their references
|
|
293
|
+
// to process, console, Buffer resolved up the V8 scope chain to
|
|
294
|
+
// workerd's GLOBAL bindings — so process.exit() inside any require'd
|
|
295
|
+
// file hit workerd's real process API and produced 'Canceling the
|
|
296
|
+
// request', and console.log bypassed our supervisor streaming.
|
|
297
|
+
// Adding the extras to the param list shadows globals inside every
|
|
298
|
+
// sub-module body. require.main is also threaded so bins doing
|
|
299
|
+
// 'if (require.main === module)' route correctly through the
|
|
300
|
+
// scopedRequire chain (G2 cowsay).
|
|
301
|
+
for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
302
|
+
if (__p.endsWith(".js") || __p.endsWith(".mjs") || __p.endsWith(".cjs")) {
|
|
303
|
+
try {
|
|
304
|
+
__compiledModules.set(__p, __mkCompiledFn(__c, [
|
|
305
|
+
"console", "process", "Buffer",
|
|
306
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
307
|
+
]));
|
|
308
|
+
} catch (__e) {
|
|
309
|
+
__compileFailures.set(__p, __e && __e.message ? __e.message : String(__e));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class __ProcessExit extends Error {
|
|
315
|
+
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export class NodeProcess extends DurableObject {
|
|
319
|
+
async run(argsJson) {
|
|
320
|
+
const args = JSON.parse(argsJson);
|
|
321
|
+
const { argv, env, cwd: _cwd, filename, dirname } = args;
|
|
322
|
+
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
323
|
+
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
324
|
+
const __supervisor = this.env?.SUPERVISOR || null;
|
|
325
|
+
const __pendingIO = [];
|
|
326
|
+
// Fix 6 orphan-detection counters: every supervisor RPC push catches
|
|
327
|
+
// its own rejection (necessary for allSettled semantics), but that
|
|
328
|
+
// previously swallowed the drop silently. Count them so reportExit
|
|
329
|
+
// can include a summary in its tail, and the supervisor's ring
|
|
330
|
+
// buffer has proof that output was lost.
|
|
331
|
+
let __rpcDrops = 0;
|
|
332
|
+
let __rpcDropBytes = 0;
|
|
333
|
+
let __rpcLastError = "";
|
|
334
|
+
const __onRpcDrop = (bytes, e) => {
|
|
335
|
+
__rpcDrops++;
|
|
336
|
+
__rpcDropBytes += bytes | 0;
|
|
337
|
+
if (e) { __rpcLastError = (e && e.message) || String(e); }
|
|
338
|
+
};
|
|
339
|
+
let cwd = _cwd || "/home/user";
|
|
340
|
+
let stdout = "", stderr = "";
|
|
341
|
+
let exitCode = 0;
|
|
342
|
+
const __vfsWrites = {};
|
|
343
|
+
const __vfsDirs = {};
|
|
344
|
+
|
|
345
|
+
${SHIMS}
|
|
346
|
+
|
|
347
|
+
// Override console AND process.stdout/stderr for live SUPERVISOR streaming
|
|
348
|
+
if (__supervisor) {
|
|
349
|
+
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
350
|
+
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
351
|
+
__consoleMod.warn = __consoleMod.error;
|
|
352
|
+
__consoleMod.info = __consoleMod.log;
|
|
353
|
+
__consoleMod.debug = __consoleMod.log;
|
|
354
|
+
// Hook process.stdout/stderr.write for live streaming (libraries use this directly)
|
|
355
|
+
__processMod.stdout.write = (d) => { const s = String(d); stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
356
|
+
__processMod.stderr.write = (d) => { const s = String(d); stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── console-facet (2026-05-12): expose the shimmed console on
|
|
360
|
+
// globalThis so user code can reach it via BOTH \`console.log(...)\`
|
|
361
|
+
// (resolved through the __compiledFn positional 'console' param)
|
|
362
|
+
// AND \`globalThis.console.log(...)\` (resolved through the global
|
|
363
|
+
// scope). Pre-fix, only the positional binding was patched; calls
|
|
364
|
+
// through globalThis hit workerd's native console which writes
|
|
365
|
+
// to the worker log (NOT our supervisor stdout/stderr stream) so
|
|
366
|
+
// the output was lost from the user's perspective.
|
|
367
|
+
//
|
|
368
|
+
// Verbatim repro on prod:
|
|
369
|
+
// $ node -e 'console.log("bare")' -> "bare"
|
|
370
|
+
// $ node -e 'globalThis.console.log("global")' -> (silent)
|
|
371
|
+
// $ node -e 'console.log(globalThis.console === console)' -> false
|
|
372
|
+
//
|
|
373
|
+
// workerd's globalThis.console descriptor is writable+configurable
|
|
374
|
+
// (verified empirically — see smoketest-globalthis-write.mjs in
|
|
375
|
+
// /workspace/.seal-internal/2026-05-12-console-facet/), so a plain
|
|
376
|
+
// assignment is sufficient. Do NOT touch globalThis.process /
|
|
377
|
+
// globalThis.Buffer here — Pyodide / Ruby bootstrap paths detect a
|
|
378
|
+
// Node-shaped env via globalThis.process and would take the wrong
|
|
379
|
+
// code path; see AGENTS.md gotcha #4.
|
|
380
|
+
try { globalThis.console = __consoleMod; } catch { /* readonly fallback */ }
|
|
381
|
+
|
|
382
|
+
// ── Unhandled rejection / uncaught error capture ──────────────────
|
|
383
|
+
//
|
|
384
|
+
// The try/catch around __compiledFn below catches only SYNCHRONOUS
|
|
385
|
+
// exceptions. Async fire-and-forget rejections (e.g.
|
|
386
|
+
// import('./missing').then(({main}) => main()) // no .catch
|
|
387
|
+
// ) fire during the microtask drain and are neither caught nor
|
|
388
|
+
// reported \u2014 pre-fix the facet exits exitCode=0 with empty stderr,
|
|
389
|
+
// and the W5 zero-silent-OOM contract only catches non-zero exits.
|
|
390
|
+
//
|
|
391
|
+
// workerd's globalThis supports the standard WHATWG events:
|
|
392
|
+
// - 'unhandledrejection' \u2014 fired on .reject() with no .catch
|
|
393
|
+
// - 'error' \u2014 uncaught synchronous errors on the
|
|
394
|
+
// global scope (e.g. errors thrown in
|
|
395
|
+
// setTimeout-scheduled callbacks that
|
|
396
|
+
// escape the __compiledFn try/catch)
|
|
397
|
+
//
|
|
398
|
+
// This listener converts each event into a stderr line + bumps
|
|
399
|
+
// exitCode to 1 (only on first event; subsequent events still
|
|
400
|
+
// append stderr). Mirrors real Node's default
|
|
401
|
+
// unhandled-rejection-is-fatal behaviour.
|
|
402
|
+
//
|
|
403
|
+
// See .seal-internal/2026-05-11-unhandled-rejection/audit.md.
|
|
404
|
+
let __unhandledFired = false;
|
|
405
|
+
const __reportUnhandled = (label, reason) => {
|
|
406
|
+
// framework-fixes-F3 (2026-05-12): suppress reports whose reason
|
|
407
|
+
// is a __ProcessExit. process.exit(N) throws __ProcessExit by
|
|
408
|
+
// design (see __processMod.exit in node-shims.ts:2158) to halt
|
|
409
|
+
// user code. The outer try/catch at __compiledFn unwraps it for
|
|
410
|
+
// the SYNCHRONOUS case, but async fire-and-forget paths (e.g.
|
|
411
|
+
// create-remix's deprecation banner that calls process.exit(0)
|
|
412
|
+
// from an async main() with no .catch()) bubble it to the
|
|
413
|
+
// unhandled-rejection listener. Pre-fix that produced a noisy
|
|
414
|
+
// 'Unhandled promise rejection: Error: process.exit(0)' line
|
|
415
|
+
// alongside the expected exit. exitCode is already set correctly
|
|
416
|
+
// by the __processMod.exit shim AND by the catch in main run loop.
|
|
417
|
+
//
|
|
418
|
+
// Concrete repro: npm create remix@latest prints a deprecation
|
|
419
|
+
// banner from upstream + process.exit(0). User sees the banner
|
|
420
|
+
// (the user-facing semantic the upstream tool emits) and exit 0
|
|
421
|
+
// (correct), but ALSO the rejection noise (artifact of our shim
|
|
422
|
+
// shape). This filter removes the noise without changing exit
|
|
423
|
+
// semantics.
|
|
424
|
+
//
|
|
425
|
+
// The filter matches BOTH the class identity (reason instanceof
|
|
426
|
+
// __ProcessExit) AND the message shape (reason.message starts
|
|
427
|
+
// with 'process.exit(') because in async paths the unhandled-
|
|
428
|
+
// rejection event sometimes loses the prototype chain across
|
|
429
|
+
// microtask hops.
|
|
430
|
+
if (reason instanceof __ProcessExit) return;
|
|
431
|
+
if (reason && typeof reason.message === "string" &&
|
|
432
|
+
/^process\\.exit\\(/.test(reason.message)) return;
|
|
433
|
+
const text = reason && reason.stack
|
|
434
|
+
? String(reason.stack)
|
|
435
|
+
: (reason && reason.message ? String(reason.message) : String(reason));
|
|
436
|
+
const line = label + ": " + text + "\\n";
|
|
437
|
+
stderr += line;
|
|
438
|
+
if (__supervisor) {
|
|
439
|
+
try {
|
|
440
|
+
__pendingIO.push(__supervisor.stderr(line).catch((e) =>
|
|
441
|
+
__onRpcDrop(line.length, e)));
|
|
442
|
+
} catch {}
|
|
443
|
+
}
|
|
444
|
+
if (!__unhandledFired) {
|
|
445
|
+
__unhandledFired = true;
|
|
446
|
+
if (exitCode === 0) exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
globalThis.addEventListener("unhandledrejection", (event) => {
|
|
450
|
+
__reportUnhandled("Unhandled promise rejection", event && event.reason);
|
|
451
|
+
});
|
|
452
|
+
globalThis.addEventListener("error", (event) => {
|
|
453
|
+
// ErrorEvent \u2014 uncaught synchronous error on the global scope.
|
|
454
|
+
// The try/catch around __compiledFn captures errors thrown DURING
|
|
455
|
+
// its synchronous execution; this listener catches errors from
|
|
456
|
+
// setTimeout/setInterval/microtask callbacks that escape the
|
|
457
|
+
// entry scope.
|
|
458
|
+
__reportUnhandled("Uncaught", event && (event.error || event.message));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const mod = { exports: {} };
|
|
462
|
+
// G2 (runtime-pkg wave): bins commonly check
|
|
463
|
+
// if (require.main === module) { main(); }
|
|
464
|
+
// — the canonical 'is this file being executed directly?' guard.
|
|
465
|
+
// Pre-fix, __require.main was null; the check was always false;
|
|
466
|
+
// bins like cowsay silently treated themselves as imported and
|
|
467
|
+
// never ran main(). Setting require.main = entry's module here
|
|
468
|
+
// (and to the loaded module on each __loadModule call below)
|
|
469
|
+
// makes the check true exactly when the file is the entry.
|
|
470
|
+
__require.main = mod;
|
|
471
|
+
try {
|
|
472
|
+
__compiledFn(
|
|
473
|
+
mod.exports, __require, mod, filename || "/home/user/script.js", dirname || "/home/user",
|
|
474
|
+
__consoleMod, __processMod, __BufferMod,
|
|
475
|
+
globalThis.setTimeout, globalThis.setInterval, globalThis.clearTimeout, globalThis.clearInterval,
|
|
476
|
+
globalThis, argv || []
|
|
477
|
+
);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (e instanceof __ProcessExit) { exitCode = e.code; }
|
|
480
|
+
else {
|
|
481
|
+
const trace = (e && e.stack) || (e && e.message) || String(e);
|
|
482
|
+
stderr += trace + "\\n";
|
|
483
|
+
exitCode = 1;
|
|
484
|
+
// Stream the trace live so it lands in the supervisor ring buffer.
|
|
485
|
+
// Without this, the error is visible only in the local 'stderr'
|
|
486
|
+
// string which gets zeroed below when __supervisor is attached,
|
|
487
|
+
// leaving the user with an empty prompt after a crash.
|
|
488
|
+
if (__supervisor) {
|
|
489
|
+
try { __pendingIO.push(__supervisor.stderr(trace + "\\n").catch((e2) => __onRpcDrop((trace || "").length + 1, e2))); } catch {}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Drain microtasks
|
|
495
|
+
await new Promise(r => setTimeout(r, 0));
|
|
496
|
+
|
|
497
|
+
// Flush writes via SUPERVISOR RPC if available (live VFS writes)
|
|
498
|
+
const __failedWrites = {};
|
|
499
|
+
if (__supervisor && Object.keys(__vfsWrites).length > 0) {
|
|
500
|
+
for (const [path, content] of Object.entries(__vfsWrites)) {
|
|
501
|
+
__pendingIO.push(
|
|
502
|
+
__supervisor.writeFile(path, content).catch(() => { __failedWrites[path] = content; })
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Await all pending I/O (live stdout, writes)
|
|
508
|
+
if (__pendingIO.length > 0) {
|
|
509
|
+
await Promise.allSettled(__pendingIO);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Fix 6: a second drain pass. If any setTimeout-scheduled callback
|
|
513
|
+
// fired during the first allSettled and pushed more writes, they sit
|
|
514
|
+
// on __pendingIO past the first drain. Yield once more, then re-settle
|
|
515
|
+
// if new items arrived. Capped at one additional round so a run-away
|
|
516
|
+
// setInterval can't hold the facet open forever.
|
|
517
|
+
const __preSecondDrain = __pendingIO.length;
|
|
518
|
+
await new Promise(r => setTimeout(r, 0));
|
|
519
|
+
if (__pendingIO.length > __preSecondDrain) {
|
|
520
|
+
await Promise.allSettled(__pendingIO.slice(__preSecondDrain));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// W8 BLOCKER-1 fix: parent-exit synchronous flush of any live
|
|
524
|
+
// child_process children. Without this, output from spawn-and-forget
|
|
525
|
+
// children (e.g., concurrently 'echo a' 'echo b' from a parent that
|
|
526
|
+
// exits before its data listeners drain) gets dropped between the
|
|
527
|
+
// last cpReadOutput poll and reportExit. __cpDrainAllChildren issues
|
|
528
|
+
// a single cpDrainOutput RPC per live child to flush remaining
|
|
529
|
+
// buffers; idempotent on already-exited children.
|
|
530
|
+
try {
|
|
531
|
+
if (__childProcessMod && typeof __childProcessMod.__cpDrainAllChildren === "function") {
|
|
532
|
+
await __childProcessMod.__cpDrainAllChildren();
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
// Best-effort. Drain failure must not block reportExit.
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Report exit AFTER I/O drains so the ring buffer has everything the
|
|
539
|
+
// facet wrote before the dump fires on the supervisor side.
|
|
540
|
+
//
|
|
541
|
+
// Fix 6: include an orphan-drop tail in the exit report so the
|
|
542
|
+
// supervisor knows how many writes (and bytes) were lost, plus the
|
|
543
|
+
// last RPC error for debugging. Empty string when clean. Pid is
|
|
544
|
+
// intentionally omitted — the supervisor already knows the pid
|
|
545
|
+
// (it's the RPC parameter) and prepends its own banner.
|
|
546
|
+
if (__supervisor) {
|
|
547
|
+
let __tail = "";
|
|
548
|
+
if (__rpcDrops > 0) {
|
|
549
|
+
__tail = "[orphan output: " + __rpcDrops + " dropped RPC write(s), ~" +
|
|
550
|
+
__rpcDropBytes + " bytes lost" +
|
|
551
|
+
(__rpcLastError ? "; last error: " + __rpcLastError : "") + "]\\n";
|
|
552
|
+
}
|
|
553
|
+
try { await __supervisor.reportExit(exitCode, __tail); } catch {}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Return results:
|
|
557
|
+
// - When SUPERVISOR streamed output live, return empty stdout/stderr
|
|
558
|
+
// (prevents double display in the terminal)
|
|
559
|
+
// - Failed writes fall back to the old vfsWrites path for supervisor-side flush
|
|
560
|
+
return JSON.stringify({
|
|
561
|
+
exitCode,
|
|
562
|
+
stdout: __supervisor ? "" : stdout,
|
|
563
|
+
stderr: __supervisor ? "" : stderr,
|
|
564
|
+
vfsWrites: __supervisor ? __failedWrites : __vfsWrites,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
`;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Generate fallback code with a plain fetch handler.
|
|
572
|
+
* Used with LOADER.load() when facets are unavailable (local dev).
|
|
573
|
+
*/
|
|
574
|
+
function generateEntrypointCode(userCode, vfsState) {
|
|
575
|
+
const safeCode = JSON.stringify(userCode);
|
|
576
|
+
// hardening-r5: same binary-safe serializer as generateFacetCode.
|
|
577
|
+
const safeBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
578
|
+
const safeManifest = JSON.stringify(vfsState.manifest);
|
|
579
|
+
return `
|
|
580
|
+
${REAL_NODE_IMPORTS}
|
|
581
|
+
|
|
582
|
+
const USER_CODE = ${safeCode};
|
|
583
|
+
|
|
584
|
+
// X.5-S: conditional-param-rename wrap. Kept byte-equivalent to
|
|
585
|
+
// generateFacetCode's helper so both pre-compile loops see the same
|
|
586
|
+
// diagnostic surface. See generateFacetCode for the rationale and the
|
|
587
|
+
// nuxt-process-redeclare-wave generalisation rationale.
|
|
588
|
+
function __mkCompiledFn(code, extraParams) {
|
|
589
|
+
function renameIfDeclared(name) {
|
|
590
|
+
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "\\\\b", "m");
|
|
591
|
+
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
592
|
+
}
|
|
593
|
+
const baseParams = [
|
|
594
|
+
"exports", "require", "module",
|
|
595
|
+
renameIfDeclared("__filename"),
|
|
596
|
+
renameIfDeclared("__dirname"),
|
|
597
|
+
];
|
|
598
|
+
const renamedExtras = (extraParams || []).map(renameIfDeclared);
|
|
599
|
+
return new Function(...baseParams, ...renamedExtras, code);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const __compiledFn = __mkCompiledFn(USER_CODE, [
|
|
603
|
+
"console", "process", "Buffer",
|
|
604
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
605
|
+
"global", "__args",
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
// VFS bundle + manifest + pre-compiled modules — all at module level (startup time).
|
|
609
|
+
// See generateFacetCode for the rationale; this is the fallback (LOADER.load)
|
|
610
|
+
// path used when facets API is unavailable.
|
|
611
|
+
const __MODULE_VFS_BUNDLE = ${safeBundle};
|
|
612
|
+
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
613
|
+
const __compiledModules = new Map();
|
|
614
|
+
// W3.5 Fix C: keep this template byte-equivalent to generateFacetCode's
|
|
615
|
+
// pre-compile loop so __loadModule sees the same diagnostic surface.
|
|
616
|
+
// G2/G3 (runtime-pkg wave): same extra-params shim as the facet API
|
|
617
|
+
// path above — see comment block there for the rationale.
|
|
618
|
+
const __compileFailures = new Map();
|
|
619
|
+
for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
620
|
+
if (__p.endsWith(".js") || __p.endsWith(".mjs") || __p.endsWith(".cjs")) {
|
|
621
|
+
try {
|
|
622
|
+
__compiledModules.set(__p, __mkCompiledFn(__c, [
|
|
623
|
+
"console", "process", "Buffer",
|
|
624
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
625
|
+
]));
|
|
626
|
+
} catch (__e) {
|
|
627
|
+
__compileFailures.set(__p, __e && __e.message ? __e.message : String(__e));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
class __ProcessExit extends Error {
|
|
633
|
+
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export default {
|
|
637
|
+
async fetch(request, workerEnv) {
|
|
638
|
+
const args = await request.json();
|
|
639
|
+
const { argv, env, cwd: _cwd, filename, dirname } = args;
|
|
640
|
+
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
641
|
+
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
642
|
+
const __supervisor = workerEnv?.SUPERVISOR || null;
|
|
643
|
+
const __pendingIO = [];
|
|
644
|
+
// Fix 6 orphan counters (same as NodeProcess.run) — count RPC writes
|
|
645
|
+
// that get dropped during isolate teardown so reportExit can report them.
|
|
646
|
+
let __rpcDrops = 0;
|
|
647
|
+
let __rpcDropBytes = 0;
|
|
648
|
+
let __rpcLastError = "";
|
|
649
|
+
const __onRpcDrop = (bytes, e) => {
|
|
650
|
+
__rpcDrops++;
|
|
651
|
+
__rpcDropBytes += bytes | 0;
|
|
652
|
+
if (e) { __rpcLastError = (e && e.message) || String(e); }
|
|
653
|
+
};
|
|
654
|
+
let cwd = _cwd || "/home/user";
|
|
655
|
+
let stdout = "", stderr = "";
|
|
656
|
+
let exitCode = 0;
|
|
657
|
+
const __vfsWrites = {};
|
|
658
|
+
const __vfsDirs = {};
|
|
659
|
+
|
|
660
|
+
${SHIMS}
|
|
661
|
+
|
|
662
|
+
// Override console AND process.stdout/stderr for live SUPERVISOR streaming
|
|
663
|
+
if (__supervisor) {
|
|
664
|
+
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
665
|
+
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
666
|
+
__consoleMod.warn = __consoleMod.error;
|
|
667
|
+
__consoleMod.info = __consoleMod.log;
|
|
668
|
+
__consoleMod.debug = __consoleMod.log;
|
|
669
|
+
__processMod.stdout.write = (d) => { const s = String(d); stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
670
|
+
__processMod.stderr.write = (d) => { const s = String(d); stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// console-facet (2026-05-12): mirror the shimmed console onto
|
|
674
|
+
// globalThis so user code can reach it via globalThis.console.log
|
|
675
|
+
// as well as bare console.log. See generateFacetCode for the full
|
|
676
|
+
// rationale (verbatim repro + workerd descriptor empirical check).
|
|
677
|
+
try { globalThis.console = __consoleMod; } catch { /* readonly fallback */ }
|
|
678
|
+
|
|
679
|
+
const mod = { exports: {} };
|
|
680
|
+
// G2 (runtime-pkg wave): see corresponding comment in NodeProcess.run.
|
|
681
|
+
__require.main = mod;
|
|
682
|
+
try {
|
|
683
|
+
__compiledFn(
|
|
684
|
+
mod.exports, __require, mod, filename || "/home/user/script.js", dirname || "/home/user",
|
|
685
|
+
__consoleMod, __processMod, __BufferMod,
|
|
686
|
+
globalThis.setTimeout, globalThis.setInterval, globalThis.clearTimeout, globalThis.clearInterval,
|
|
687
|
+
globalThis, argv || []
|
|
688
|
+
);
|
|
689
|
+
} catch (e) {
|
|
690
|
+
if (e instanceof __ProcessExit) { exitCode = e.code; }
|
|
691
|
+
else {
|
|
692
|
+
const trace = (e && e.stack) || (e && e.message) || String(e);
|
|
693
|
+
stderr += trace + "\\n";
|
|
694
|
+
exitCode = 1;
|
|
695
|
+
if (__supervisor) {
|
|
696
|
+
try { __pendingIO.push(__supervisor.stderr(trace + "\\n").catch((e2) => __onRpcDrop((trace || "").length + 1, e2))); } catch {}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
await new Promise(r => setTimeout(r, 0));
|
|
702
|
+
|
|
703
|
+
const __failedWrites = {};
|
|
704
|
+
if (__supervisor && Object.keys(__vfsWrites).length > 0) {
|
|
705
|
+
for (const [path, content] of Object.entries(__vfsWrites)) {
|
|
706
|
+
__pendingIO.push(__supervisor.writeFile(path, content).catch(() => { __failedWrites[path] = content; }));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (__pendingIO.length > 0) await Promise.allSettled(__pendingIO);
|
|
710
|
+
|
|
711
|
+
// Fix 6: second drain pass for any setTimeout-scheduled writes that
|
|
712
|
+
// landed after the first allSettled. Bounded to one additional round.
|
|
713
|
+
const __preSecondDrain = __pendingIO.length;
|
|
714
|
+
await new Promise(r => setTimeout(r, 0));
|
|
715
|
+
if (__pendingIO.length > __preSecondDrain) {
|
|
716
|
+
await Promise.allSettled(__pendingIO.slice(__preSecondDrain));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// W8 BLOCKER-1 fix: parent-exit synchronous flush of child_process
|
|
720
|
+
// children. See generateFacetCode for the rationale.
|
|
721
|
+
try {
|
|
722
|
+
if (__childProcessMod && typeof __childProcessMod.__cpDrainAllChildren === "function") {
|
|
723
|
+
await __childProcessMod.__cpDrainAllChildren();
|
|
724
|
+
}
|
|
725
|
+
} catch (e) { /* best-effort */ }
|
|
726
|
+
|
|
727
|
+
// Report exit after draining so the ring buffer is complete before
|
|
728
|
+
// the supervisor decides whether to emit a dump. Fix 6: include an
|
|
729
|
+
// orphan-drop tail if RPC writes were lost during teardown.
|
|
730
|
+
if (__supervisor) {
|
|
731
|
+
let __tail = "";
|
|
732
|
+
if (__rpcDrops > 0) {
|
|
733
|
+
__tail = "[orphan output: " + __rpcDrops + " dropped RPC write(s), ~" +
|
|
734
|
+
__rpcDropBytes + " bytes lost" +
|
|
735
|
+
(__rpcLastError ? "; last error: " + __rpcLastError : "") + "]\\n";
|
|
736
|
+
}
|
|
737
|
+
try { await __supervisor.reportExit(exitCode, __tail); } catch {}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return Response.json({
|
|
741
|
+
exitCode,
|
|
742
|
+
stdout: __supervisor ? "" : stdout,
|
|
743
|
+
stderr: __supervisor ? "" : stderr,
|
|
744
|
+
vfsWrites: __supervisor ? __failedWrites : __vfsWrites,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Generate a long-running Node entrypoint.
|
|
752
|
+
*
|
|
753
|
+
* Same core shim/VFS machinery as foreground node execution, but the
|
|
754
|
+
* compiled user entry is booted once and the exported entrypoint keeps
|
|
755
|
+
* serving HTTP requests from the shimmed http.Server registry.
|
|
756
|
+
*/
|
|
757
|
+
function generateLongRunningNodeCode(userCode, vfsState, opts) {
|
|
758
|
+
const safeCode = JSON.stringify(userCode);
|
|
759
|
+
const safeArgs = JSON.stringify({
|
|
760
|
+
argv: opts.argv || [],
|
|
761
|
+
env: opts.env || {},
|
|
762
|
+
cwd: opts.cwd || '/home/user',
|
|
763
|
+
filename: opts.filename || '<script>',
|
|
764
|
+
dirname: opts.dirname || opts.cwd || '/home/user',
|
|
765
|
+
});
|
|
766
|
+
const safeBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
767
|
+
const safeManifest = JSON.stringify(vfsState.manifest);
|
|
768
|
+
return `
|
|
769
|
+
${REAL_NODE_IMPORTS}
|
|
770
|
+
|
|
771
|
+
const USER_CODE = ${safeCode};
|
|
772
|
+
const __NIMBUS_ARGS = ${safeArgs};
|
|
773
|
+
|
|
774
|
+
function __mkCompiledFn(code, extraParams) {
|
|
775
|
+
function renameIfDeclared(name) {
|
|
776
|
+
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "\\\\b", "m");
|
|
777
|
+
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
778
|
+
}
|
|
779
|
+
const baseParams = [
|
|
780
|
+
"exports", "require", "module",
|
|
781
|
+
renameIfDeclared("__filename"),
|
|
782
|
+
renameIfDeclared("__dirname"),
|
|
783
|
+
];
|
|
784
|
+
const renamedExtras = (extraParams || []).map(renameIfDeclared);
|
|
785
|
+
return new Function(...baseParams, ...renamedExtras, code);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const __compiledFn = __mkCompiledFn(USER_CODE, [
|
|
789
|
+
"console", "process", "Buffer",
|
|
790
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
791
|
+
"global", "__args",
|
|
792
|
+
]);
|
|
793
|
+
|
|
794
|
+
const __MODULE_VFS_BUNDLE = ${safeBundle};
|
|
795
|
+
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
796
|
+
const __compiledModules = new Map();
|
|
797
|
+
const __compileFailures = new Map();
|
|
798
|
+
for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
799
|
+
if (__p.endsWith(".js") || __p.endsWith(".mjs") || __p.endsWith(".cjs")) {
|
|
800
|
+
try {
|
|
801
|
+
__compiledModules.set(__p, __mkCompiledFn(__c, [
|
|
802
|
+
"console", "process", "Buffer",
|
|
803
|
+
"setTimeout", "setInterval", "clearTimeout", "clearInterval",
|
|
804
|
+
]));
|
|
805
|
+
} catch (__e) {
|
|
806
|
+
__compileFailures.set(__p, __e && __e.message ? __e.message : String(__e));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
class __ProcessExit extends Error {
|
|
812
|
+
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let __nimbusStarted = false;
|
|
816
|
+
let __nimbusStarting = null;
|
|
817
|
+
let __nimbusRuntime = null;
|
|
818
|
+
|
|
819
|
+
async function __nimbusFlushRuntime() {
|
|
820
|
+
const rt = __nimbusRuntime;
|
|
821
|
+
if (!rt) return;
|
|
822
|
+
if (rt.supervisor && Object.keys(rt.vfsWrites).length > 0) {
|
|
823
|
+
for (const [path, content] of Object.entries(rt.vfsWrites)) {
|
|
824
|
+
rt.pendingIO.push(rt.supervisor.writeFile(path, content).catch(() => {}));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (rt.pendingIO.length > rt.settledIO) {
|
|
828
|
+
const slice = rt.pendingIO.slice(rt.settledIO);
|
|
829
|
+
rt.settledIO = rt.pendingIO.length;
|
|
830
|
+
await Promise.allSettled(slice);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function __nimbusEnsureStarted(workerEnv) {
|
|
835
|
+
if (__nimbusStarted) return;
|
|
836
|
+
if (__nimbusStarting) return __nimbusStarting;
|
|
837
|
+
__nimbusStarting = (async () => {
|
|
838
|
+
const args = __NIMBUS_ARGS;
|
|
839
|
+
const { argv, env, cwd: _cwd, filename, dirname } = args;
|
|
840
|
+
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
841
|
+
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
842
|
+
const __supervisor = workerEnv?.SUPERVISOR || null;
|
|
843
|
+
const __pendingIO = [];
|
|
844
|
+
let __rpcDrops = 0;
|
|
845
|
+
let __rpcDropBytes = 0;
|
|
846
|
+
let __rpcLastError = "";
|
|
847
|
+
const __onRpcDrop = (bytes, e) => {
|
|
848
|
+
__rpcDrops++;
|
|
849
|
+
__rpcDropBytes += bytes | 0;
|
|
850
|
+
if (e) __rpcLastError = (e && e.message) || String(e);
|
|
851
|
+
};
|
|
852
|
+
let cwd = _cwd || "/home/user";
|
|
853
|
+
let stdout = "", stderr = "";
|
|
854
|
+
let exitCode = 0;
|
|
855
|
+
const __vfsWrites = {};
|
|
856
|
+
const __vfsDirs = {};
|
|
857
|
+
|
|
858
|
+
${SHIMS}
|
|
859
|
+
|
|
860
|
+
if (__supervisor) {
|
|
861
|
+
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
862
|
+
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); };
|
|
863
|
+
__consoleMod.warn = __consoleMod.error;
|
|
864
|
+
__consoleMod.info = __consoleMod.log;
|
|
865
|
+
__consoleMod.debug = __consoleMod.log;
|
|
866
|
+
__processMod.stdout.write = (d) => { const s = String(d); stdout += s; __pendingIO.push(__supervisor.stdout(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
867
|
+
__processMod.stderr.write = (d) => { const s = String(d); stderr += s; __pendingIO.push(__supervisor.stderr(s).catch((e) => __onRpcDrop(s.length, e))); return true; };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
try { globalThis.console = __consoleMod; } catch {}
|
|
871
|
+
|
|
872
|
+
const mod = { exports: {} };
|
|
873
|
+
__require.main = mod;
|
|
874
|
+
try {
|
|
875
|
+
__compiledFn(
|
|
876
|
+
mod.exports, __require, mod, filename || "/home/user/script.js", dirname || "/home/user",
|
|
877
|
+
__consoleMod, __processMod, __BufferMod,
|
|
878
|
+
globalThis.setTimeout, globalThis.setInterval, globalThis.clearTimeout, globalThis.clearInterval,
|
|
879
|
+
globalThis, argv || []
|
|
880
|
+
);
|
|
881
|
+
} catch (e) {
|
|
882
|
+
if (e instanceof __ProcessExit) {
|
|
883
|
+
exitCode = e.code;
|
|
884
|
+
} else {
|
|
885
|
+
const trace = (e && e.stack) || (e && e.message) || String(e);
|
|
886
|
+
stderr += trace + "\\n";
|
|
887
|
+
exitCode = 1;
|
|
888
|
+
if (__supervisor) {
|
|
889
|
+
try { __pendingIO.push(__supervisor.stderr(trace + "\\n").catch((e2) => __onRpcDrop((trace || "").length + 1, e2))); } catch {}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
895
|
+
__nimbusRuntime = {
|
|
896
|
+
supervisor: __supervisor,
|
|
897
|
+
pendingIO: __pendingIO,
|
|
898
|
+
settledIO: 0,
|
|
899
|
+
vfsWrites: __vfsWrites,
|
|
900
|
+
};
|
|
901
|
+
await __nimbusFlushRuntime();
|
|
902
|
+
|
|
903
|
+
if (__rpcDrops > 0 && __supervisor) {
|
|
904
|
+
const tail = "[orphan output: " + __rpcDrops + " dropped RPC write(s), ~" +
|
|
905
|
+
__rpcDropBytes + " bytes lost" +
|
|
906
|
+
(__rpcLastError ? "; last error: " + __rpcLastError : "") + "]\\n";
|
|
907
|
+
try { await __supervisor.stderr(tail); } catch {}
|
|
908
|
+
}
|
|
909
|
+
if (exitCode !== 0) {
|
|
910
|
+
if (__supervisor) {
|
|
911
|
+
try { await __supervisor.reportExit(exitCode, stderr || ("exit " + exitCode + "\\n")); } catch {}
|
|
912
|
+
}
|
|
913
|
+
throw new Error(stderr || ("long-running node startup exited " + exitCode));
|
|
914
|
+
}
|
|
915
|
+
__nimbusStarted = true;
|
|
916
|
+
})();
|
|
917
|
+
return __nimbusStarting;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function __nimbusDispatchHttp(req, workerEnv) {
|
|
921
|
+
await __nimbusEnsureStarted(workerEnv);
|
|
922
|
+
if (req.headers.get("X-Nimbus-Internal-Boot") === "1") {
|
|
923
|
+
return new Response(null, { status: 204 });
|
|
924
|
+
}
|
|
925
|
+
const ports = globalThis.__portRegistry;
|
|
926
|
+
const hinted = Number(req.headers.get("X-Nimbus-Port") || 0);
|
|
927
|
+
const server = ports && (ports.get(hinted) || ports.values().next().value);
|
|
928
|
+
if (!server || typeof server._handleRequest !== "function") {
|
|
929
|
+
return new Response("Nimbus: no HTTP server is listening in this process", { status: 502 });
|
|
930
|
+
}
|
|
931
|
+
const url = new URL(req.url);
|
|
932
|
+
const headers = {};
|
|
933
|
+
req.headers.forEach((v, k) => { headers[k] = v; });
|
|
934
|
+
let body = "";
|
|
935
|
+
if (req.method !== "GET" && req.method !== "HEAD") body = await req.text();
|
|
936
|
+
const res = server._handleRequest(url.pathname + url.search, req.method, headers, body);
|
|
937
|
+
if (!res._ended) {
|
|
938
|
+
await new Promise((resolve) => {
|
|
939
|
+
try { res.on("finish", resolve); } catch { resolve(); }
|
|
940
|
+
setTimeout(resolve, 5000);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
await __nimbusFlushRuntime();
|
|
944
|
+
return new Response((res._body || []).join(""), { status: res.statusCode || 200, headers: res.headers || {} });
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export default {
|
|
948
|
+
async fetch(req, env) { return __nimbusDispatchHttp(req, env); },
|
|
949
|
+
async handleHttpRequest(req) { return __nimbusDispatchHttp(req, undefined); },
|
|
950
|
+
};
|
|
951
|
+
`;
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* hardening-r5: read a file from the VFS and decide whether to keep it
|
|
955
|
+
* as a string (valid UTF-8 text — the hot path for source code,
|
|
956
|
+
* package.json, configs) or as Uint8Array bytes (binary — wasm
|
|
957
|
+
* modules, images, sqlite blobs, etc.).
|
|
958
|
+
*
|
|
959
|
+
* Strategy: read bytes, attempt a fatal UTF-8 decode. If decode
|
|
960
|
+
* succeeds the file IS valid UTF-8 and the string round-trips
|
|
961
|
+
* losslessly through JSON; return string. If decode throws (any
|
|
962
|
+
* invalid byte sequence) return Uint8Array.
|
|
963
|
+
*
|
|
964
|
+
* Throws on read errors (caller wraps in try/catch — matches the
|
|
965
|
+
* pre-fix readFileString contract).
|
|
966
|
+
*/
|
|
967
|
+
function _readBundleCell(vfs, path) {
|
|
968
|
+
const bytes = vfs.readFile(path);
|
|
969
|
+
// Bun/workerd both support fatal:true on TextDecoder. Cast to any
|
|
970
|
+
// because TypeScript's lib.dom.d.ts marks ignoreBOM as required when
|
|
971
|
+
// an options object is provided; at runtime both fields are optional.
|
|
972
|
+
try {
|
|
973
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
return bytes;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* hardening-r5: byte-length of a bundle cell for budget accounting.
|
|
981
|
+
* Strings counted as char-length (a slight under-count for non-ASCII
|
|
982
|
+
* but matches the pre-fix behaviour); Uint8Array counted as byteLength.
|
|
983
|
+
*/
|
|
984
|
+
function _bundleCellLength(cell) {
|
|
985
|
+
return typeof cell === 'string' ? cell.length : cell.byteLength;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* hardening-r5: emit a JS expression that, when evaluated inside the
|
|
989
|
+
* facet's module-init context, yields a `Record<string, string |
|
|
990
|
+
* Uint8Array>` with binary cells revived from base64. Strings stay as
|
|
991
|
+
* JSON strings (the hot path). Binary cells become `{ __b64: "..." }`
|
|
992
|
+
* markers and are revived by a tiny inline loop.
|
|
993
|
+
*
|
|
994
|
+
* The output is a SELF-EXECUTING IIFE expression so it can be substituted
|
|
995
|
+
* directly into `const __MODULE_VFS_BUNDLE = ${expr};` template slots.
|
|
996
|
+
*/
|
|
997
|
+
function _serializeBundleForFacet(bundle) {
|
|
998
|
+
const strCells = {};
|
|
999
|
+
const binCells = {};
|
|
1000
|
+
for (const [k, v] of Object.entries(bundle)) {
|
|
1001
|
+
if (typeof v === 'string') {
|
|
1002
|
+
strCells[k] = v;
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
// Uint8Array → base64. btoa requires a binary string; we build it
|
|
1006
|
+
// 8K chars at a time to avoid String.fromCharCode argument-count
|
|
1007
|
+
// limits on large files (~1MB+).
|
|
1008
|
+
let bin = '';
|
|
1009
|
+
const CHUNK = 8192;
|
|
1010
|
+
for (let i = 0; i < v.byteLength; i += CHUNK) {
|
|
1011
|
+
bin += String.fromCharCode.apply(null, Array.from(v.subarray(i, Math.min(i + CHUNK, v.byteLength))));
|
|
1012
|
+
}
|
|
1013
|
+
binCells[k] = btoa(bin);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// The IIFE revives binary cells in-place. atob → binary string →
|
|
1017
|
+
// Uint8Array (Uint8Array.from(str, c=>c.charCodeAt(0))).
|
|
1018
|
+
// Note: when binCells is empty (the overwhelming common case —
|
|
1019
|
+
// source code is all text) the IIFE collapses to a JSON literal,
|
|
1020
|
+
// costing only the IIFE wrapper bytes (~30) per facet boot.
|
|
1021
|
+
return `(function(){const __b=${JSON.stringify(strCells)};const __x=${JSON.stringify(binCells)};for(const __k in __x){__b[__k]=Uint8Array.from(atob(__x[__k]),__c=>__c.charCodeAt(0));}return __b;})()`;
|
|
1022
|
+
}
|
|
1023
|
+
const MANIFEST_MAX_DEPTH = 12;
|
|
1024
|
+
/**
|
|
1025
|
+
* Build the manifest pass — uncapped path→child-names map. UNCHANGED
|
|
1026
|
+
* from W2.5b; this is the W2.5b root-cause fix and continues to keep
|
|
1027
|
+
* fs.readdirSync / fs.statSync honest regardless of which subset of
|
|
1028
|
+
* file CONTENT we ship.
|
|
1029
|
+
*/
|
|
1030
|
+
function buildManifest(vfs, cwd, scriptPath) {
|
|
1031
|
+
const manifest = {};
|
|
1032
|
+
function walk(dirPath, depth = 0) {
|
|
1033
|
+
if (depth > MANIFEST_MAX_DEPTH)
|
|
1034
|
+
return;
|
|
1035
|
+
const stripped = dirPath.replace(/^\/+/, '');
|
|
1036
|
+
if (stripped in manifest)
|
|
1037
|
+
return;
|
|
1038
|
+
let entries;
|
|
1039
|
+
try {
|
|
1040
|
+
entries = vfs.readdir(stripped);
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
manifest[stripped] = entries.map((e) => e.name);
|
|
1046
|
+
for (const entry of entries) {
|
|
1047
|
+
if (entry.type === 'directory') {
|
|
1048
|
+
const childPath = stripped ? stripped + '/' + entry.name : entry.name;
|
|
1049
|
+
walk(childPath, depth + 1);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1054
|
+
walk(cwdStripped, 0);
|
|
1055
|
+
const nmDir = cwdStripped + '/node_modules';
|
|
1056
|
+
if (vfs.exists(nmDir) && vfs.isDirectory(nmDir)) {
|
|
1057
|
+
walk(nmDir, 0);
|
|
1058
|
+
}
|
|
1059
|
+
// ── Bin-target package root (e.g. /tmp/.npx-cache/node_modules/<pkg>/) ──
|
|
1060
|
+
//
|
|
1061
|
+
// When the entry script lives in a node_modules outside cwd (npx-cache
|
|
1062
|
+
// packages, globally-installed bins, etc.), buildManifest's cwd walk
|
|
1063
|
+
// misses the package's sibling files. The bin's index.js gets
|
|
1064
|
+
// require-walked + greedy-added to the BUNDLE, but the MANIFEST
|
|
1065
|
+
// (which is the source of truth for `fs.readdirSync`) was empty for
|
|
1066
|
+
// those paths — so `readdirSync('/tmp/.npx-cache/.../template-X')`
|
|
1067
|
+
// returned [] and `create-vite` scaffolded zero files.
|
|
1068
|
+
//
|
|
1069
|
+
// Walk the innermost `node_modules/<pkg>/` of `scriptPath` so its
|
|
1070
|
+
// entire package tree is enumerable via readdir. Bounded by
|
|
1071
|
+
// MANIFEST_MAX_DEPTH; same depth budget as the cwd walk.
|
|
1072
|
+
if (scriptPath) {
|
|
1073
|
+
const sp = scriptPath.replace(/^\/+/, '');
|
|
1074
|
+
const segs = sp.split('/');
|
|
1075
|
+
let nmIdx = -1;
|
|
1076
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
1077
|
+
if (segs[i] === 'node_modules') {
|
|
1078
|
+
nmIdx = i;
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (nmIdx >= 0) {
|
|
1083
|
+
const isScoped = segs[nmIdx + 1]?.startsWith('@');
|
|
1084
|
+
const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
|
|
1085
|
+
if (pkgEnd <= segs.length) {
|
|
1086
|
+
const pkgRoot = segs.slice(0, pkgEnd).join('/');
|
|
1087
|
+
if (vfs.exists(pkgRoot) && vfs.isDirectory(pkgRoot)) {
|
|
1088
|
+
walk(pkgRoot, 0);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return manifest;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Greedy-oversample every installed package's main entry. The static
|
|
1097
|
+
* prefetch via require-resolver covers the require() chain literally
|
|
1098
|
+
* present in source; greedy oversampling adds a safety net for dynamic
|
|
1099
|
+
* patterns the regex misses (jest/`bindings`/`import-local` style
|
|
1100
|
+
* computed-path requires). Bounded to package.json + 1 main-entry file
|
|
1101
|
+
* per package — sub-agent §Q3 quantified the worst-case cumulative
|
|
1102
|
+
* budget impact (~322 KiB for fastify, ~1.7 MiB for ts-jest).
|
|
1103
|
+
*/
|
|
1104
|
+
// X.5-C: exported so audit/probes/x5c/functional/f2-hash-chunk-greedy.mjs
|
|
1105
|
+
// can verify the hash-chunk + shared/ oversample directly. Pre-X.5-C this
|
|
1106
|
+
// was a file-local helper. Adding the named export is a pure surface
|
|
1107
|
+
// addition — no callers other than buildPrefetchBundle (same file) and
|
|
1108
|
+
// the new probe.
|
|
1109
|
+
export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
1110
|
+
let added = 0;
|
|
1111
|
+
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1112
|
+
const nmDir = cwdStripped + '/node_modules';
|
|
1113
|
+
if (!(vfs.exists(nmDir) && vfs.isDirectory(nmDir)))
|
|
1114
|
+
return { added };
|
|
1115
|
+
const exts = ['', '.js', '.cjs', '.mjs', '/index.js', '/index.cjs'];
|
|
1116
|
+
function addOne(path) {
|
|
1117
|
+
const stripped = path.replace(/^\/+/, '');
|
|
1118
|
+
if (stripped in bundle)
|
|
1119
|
+
return false;
|
|
1120
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1121
|
+
return false;
|
|
1122
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1123
|
+
return false;
|
|
1124
|
+
try {
|
|
1125
|
+
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
1126
|
+
return false;
|
|
1127
|
+
// hardening-r5: preserve binary content as Uint8Array.
|
|
1128
|
+
const content = _readBundleCell(vfs, stripped);
|
|
1129
|
+
const cellLen = _bundleCellLength(content);
|
|
1130
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
1131
|
+
return false;
|
|
1132
|
+
bundle[stripped] = content;
|
|
1133
|
+
budgetState.totalBytes += cellLen;
|
|
1134
|
+
budgetState.fileCount++;
|
|
1135
|
+
added++;
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// X.5-C Fix #2 helper: walk a (possibly nested) exports value and
|
|
1143
|
+
// collect every string-leaf path. unbuild-shaped packages like pathe
|
|
1144
|
+
// nest two deep — `exports."."`.{require,import}.{types,default} —
|
|
1145
|
+
// and the previous one-level loop only caught the inner string leaves
|
|
1146
|
+
// when default was at the top, missing the unbuild shape entirely.
|
|
1147
|
+
function collectExportLeaves(node, out) {
|
|
1148
|
+
if (typeof node === 'string') {
|
|
1149
|
+
out.add(node);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (!node || typeof node !== 'object')
|
|
1153
|
+
return;
|
|
1154
|
+
// Order matters for the "most likely usable" leaf: prefer require
|
|
1155
|
+
// (most CJS-friendly), then default, then node, then import. We add
|
|
1156
|
+
// ALL of them to the candidate set — addPkgEntry will probe each.
|
|
1157
|
+
for (const k of ['require', 'node', 'default', 'import']) {
|
|
1158
|
+
if (k in node)
|
|
1159
|
+
collectExportLeaves(node[k], out);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function addPkgEntry(pkgDir) {
|
|
1163
|
+
addOne(pkgDir + '/package.json');
|
|
1164
|
+
let meta;
|
|
1165
|
+
try {
|
|
1166
|
+
meta = JSON.parse(vfs.readFileString(pkgDir + '/package.json'));
|
|
1167
|
+
}
|
|
1168
|
+
catch {
|
|
1169
|
+
meta = null;
|
|
1170
|
+
}
|
|
1171
|
+
const candidates = new Set();
|
|
1172
|
+
if (meta) {
|
|
1173
|
+
if (typeof meta.main === 'string')
|
|
1174
|
+
candidates.add(meta.main);
|
|
1175
|
+
if (typeof meta.module === 'string')
|
|
1176
|
+
candidates.add(meta.module);
|
|
1177
|
+
const exp = meta.exports;
|
|
1178
|
+
if (typeof exp === 'string')
|
|
1179
|
+
candidates.add(exp);
|
|
1180
|
+
else if (exp && typeof exp === 'object') {
|
|
1181
|
+
const dot = exp['.'];
|
|
1182
|
+
// X.5-C Fix #2: walk nested condition trees recursively. Without
|
|
1183
|
+
// this, packages with two-level exports (pathe, magic-string,
|
|
1184
|
+
// most unbuild-emitted libs) miss their actual entry leaf and
|
|
1185
|
+
// greedyAddMainEntries falls back to /index.js probing — which
|
|
1186
|
+
// doesn't exist for those packages.
|
|
1187
|
+
collectExportLeaves(dot, candidates);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (candidates.size === 0)
|
|
1191
|
+
candidates.add('index.js');
|
|
1192
|
+
for (const rel of candidates) {
|
|
1193
|
+
const norm = rel.replace(/^\.\//, '');
|
|
1194
|
+
const base = pkgDir + '/' + norm;
|
|
1195
|
+
let landed = false;
|
|
1196
|
+
const tries = /\.[a-z]+$/.test(norm) ? [base] : exts.map((e) => base + e);
|
|
1197
|
+
for (const candidate of tries) {
|
|
1198
|
+
if (vfs.exists(candidate.replace(/^\/+/, '')) &&
|
|
1199
|
+
!vfs.isDirectory(candidate.replace(/^\/+/, ''))) {
|
|
1200
|
+
if (addOne(candidate)) {
|
|
1201
|
+
landed = true;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (landed) {
|
|
1207
|
+
// X.5-C Fix #2: when an entry lands, also pull in sibling files
|
|
1208
|
+
// that match unbuild's hash-chunk pattern (`<base>.<hash>.cjs|mjs|js`)
|
|
1209
|
+
// AND walk one level into a `shared/` subdir if the package has
|
|
1210
|
+
// one. The unbuild bundler emits chunked CJS like:
|
|
1211
|
+
// dist/index.cjs (entry)
|
|
1212
|
+
// dist/shared/<base>.<hash>.cjs (chunk required by entry)
|
|
1213
|
+
//
|
|
1214
|
+
// Without this, even when Fix #1 lets the prefetch walker reach
|
|
1215
|
+
// the entry, the package's required chunks land OUTSIDE the
|
|
1216
|
+
// walker's MAX_FILES/MAX_BYTES budget on big trees (nuxt 516
|
|
1217
|
+
// pkgs / 10k+ files). The greedy oversample is the defensive
|
|
1218
|
+
// safety net for hash-chunk reachability.
|
|
1219
|
+
const entryDir = base.replace(/\/[^/]+$/, '');
|
|
1220
|
+
try {
|
|
1221
|
+
const sibs = vfs.readdir(entryDir);
|
|
1222
|
+
for (const sib of sibs) {
|
|
1223
|
+
if (sib.type !== 'file')
|
|
1224
|
+
continue;
|
|
1225
|
+
// Hash-chunk pattern: <name>.<hash>.<cjs|mjs|js>. Hash must
|
|
1226
|
+
// be 6+ chars AND look like a hash, not an English word —
|
|
1227
|
+
// either contain digits/underscore/dash, or contain BOTH
|
|
1228
|
+
// uppercase AND lowercase letters (real bundler hashes are
|
|
1229
|
+
// mixed-case base64-shaped: `BSlhyZSM`, `M-eThtNZ`, ...). This
|
|
1230
|
+
// discriminator keeps us from false-positiving on common
|
|
1231
|
+
// suffixes that happen to be 6+ chars all-lowercase like
|
|
1232
|
+
// `minified`, `modern`, `production`, `compiled`.
|
|
1233
|
+
const hashMatch = sib.name.match(/\.([A-Za-z0-9_-]{6,})\.(cjs|mjs|js)$/);
|
|
1234
|
+
if (!hashMatch)
|
|
1235
|
+
continue;
|
|
1236
|
+
const seg = hashMatch[1];
|
|
1237
|
+
const hasDigitOrDash = /[0-9_-]/.test(seg);
|
|
1238
|
+
const hasMixedCase = /[A-Z]/.test(seg) && /[a-z]/.test(seg);
|
|
1239
|
+
if (!hasDigitOrDash && !hasMixedCase)
|
|
1240
|
+
continue;
|
|
1241
|
+
addOne(entryDir + '/' + sib.name);
|
|
1242
|
+
}
|
|
1243
|
+
// Walk one level into `shared/` — unconditionally, since the
|
|
1244
|
+
// pattern is well-known across unbuild/rolldown/rollup chunked
|
|
1245
|
+
// outputs. Bounded by addOne's budget checks; readdir of a
|
|
1246
|
+
// typical shared/ dir returns 1-5 files.
|
|
1247
|
+
const sharedDir = entryDir + '/shared';
|
|
1248
|
+
const sharedStripped = sharedDir.replace(/^\/+/, '');
|
|
1249
|
+
if (vfs.exists(sharedStripped) && vfs.isDirectory(sharedStripped)) {
|
|
1250
|
+
for (const sh of vfs.readdir(sharedDir)) {
|
|
1251
|
+
if (sh.type !== 'file')
|
|
1252
|
+
continue;
|
|
1253
|
+
if (!/\.(cjs|mjs|js)$/.test(sh.name))
|
|
1254
|
+
continue;
|
|
1255
|
+
addOne(sharedDir + '/' + sh.name);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* unreadable dir — drop sibling oversample, entry
|
|
1260
|
+
file is enough */
|
|
1261
|
+
}
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
for (const pkg of vfs.readdir(nmDir)) {
|
|
1268
|
+
if (pkg.type !== 'directory')
|
|
1269
|
+
continue;
|
|
1270
|
+
const pkgDir = nmDir + '/' + pkg.name;
|
|
1271
|
+
if (pkg.name.startsWith('@')) {
|
|
1272
|
+
try {
|
|
1273
|
+
for (const sub of vfs.readdir(pkgDir)) {
|
|
1274
|
+
if (sub.type === 'directory')
|
|
1275
|
+
addPkgEntry(pkgDir + '/' + sub.name);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
catch { /* ignore */ }
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
addPkgEntry(pkgDir);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
catch { /* ignore */ }
|
|
1286
|
+
return { added };
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* X.5-Z3: scan every JS source already in `bundle` for static
|
|
1290
|
+
* `fs.readFileSync(path.resolve(__dirname, "<rel>"))` shapes and pull
|
|
1291
|
+
* the matched asset files (.css / .html / .htm / .svg / .txt / .json)
|
|
1292
|
+
* into the bundle. The motivating case is jsdom's
|
|
1293
|
+
* `lib/jsdom/living/css/helpers/computed-style.js:16-19`, which loads
|
|
1294
|
+
* `default-stylesheet.css` at module-eval time:
|
|
1295
|
+
*
|
|
1296
|
+
* const defaultStyleSheet = fs.readFileSync(
|
|
1297
|
+
* path.resolve(__dirname, "../../../browser/default-stylesheet.css"),
|
|
1298
|
+
* { encoding: "utf-8" },
|
|
1299
|
+
* );
|
|
1300
|
+
*
|
|
1301
|
+
* The fs shim's `readFileSync` (`src/node-shims.ts:202-215`) consults
|
|
1302
|
+
* only `__vfsBundle` + `__vfsWrites`; runtime asset files that the
|
|
1303
|
+
* require-graph walker doesn't reach (it's bounded to .js/.mjs/.cjs)
|
|
1304
|
+
* are absent from the bundle and ENOENT at runtime. This helper closes
|
|
1305
|
+
* that gap as a sibling of `greedyAddMainEntries` (W2.6a) +
|
|
1306
|
+
* `transformEsmInBundle` (W3.5 Fix B).
|
|
1307
|
+
*
|
|
1308
|
+
* Pattern matched: literal-only, conservative.
|
|
1309
|
+
*
|
|
1310
|
+
* fs.readFileSync(path.resolve(__dirname, "<rel>"), …)
|
|
1311
|
+
* readFileSync(path.resolve(__dirname, "<rel>"), …)
|
|
1312
|
+
*
|
|
1313
|
+
* `<rel>` is a string literal (single, double, OR backtick — provided
|
|
1314
|
+
* the backtick form has no `${}` interpolation). Template-literal,
|
|
1315
|
+
* variable, and concatenation forms are **deliberately skipped** —
|
|
1316
|
+
* they're an unbounded class. Comment-stripped first to avoid
|
|
1317
|
+
* matching the pattern inside `//` / `/* */`.
|
|
1318
|
+
*
|
|
1319
|
+
* Returns the count of asset files added (for diagnostics). Errors
|
|
1320
|
+
* are swallowed: missing assets, unreadable VFS, and non-string
|
|
1321
|
+
* readFile inputs are silent skips.
|
|
1322
|
+
*
|
|
1323
|
+
* Same budget shape as `greedyAddMainEntries` — shares the same
|
|
1324
|
+
* VFS_BUNDLE_MAX_FILES / VFS_BUNDLE_MAX_BYTES caps via the
|
|
1325
|
+
* `budgetState` counter.
|
|
1326
|
+
*/
|
|
1327
|
+
export function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
1328
|
+
let added = 0;
|
|
1329
|
+
// Asset extensions covered. Conservative whitelist — txt/json are
|
|
1330
|
+
// also legit runtime-loaded assets (e.g. mime-db json, license.txt).
|
|
1331
|
+
// .json is already typically reachable via `require('./x.json')` so
|
|
1332
|
+
// it's mostly defensive here.
|
|
1333
|
+
const ASSET_EXT = /\.(css|html|htm|svg|txt|json)$/i;
|
|
1334
|
+
// Match the static-literal shape. The capture groups are:
|
|
1335
|
+
// 1 = the relative path string literal contents (no quote chars).
|
|
1336
|
+
// Shape:
|
|
1337
|
+
// readFileSync( path.resolve( __dirname , "rel" )
|
|
1338
|
+
// fs.readFileSync(path.resolve(__dirname, "rel"), …)
|
|
1339
|
+
// node:path / "node:path" forms also covered by allowing optional
|
|
1340
|
+
// leading `\w+\.` prefix on the resolve target.
|
|
1341
|
+
// Quote chars supported: ' " `. For backtick we additionally check
|
|
1342
|
+
// there's no `${` in the captured body (template-literal interpolation
|
|
1343
|
+
// is rejected).
|
|
1344
|
+
const RX = /(?:\bfs\s*\.)?readFileSync\s*\(\s*(?:[\w$.]+\s*\.\s*)?resolve\s*\(\s*__dirname\s*,\s*(['"`])([^'"`]+)\1\s*[\),]/g;
|
|
1345
|
+
function addOneAsset(absPath) {
|
|
1346
|
+
const stripped = absPath.replace(/^\/+/, '');
|
|
1347
|
+
if (stripped in bundle)
|
|
1348
|
+
return false;
|
|
1349
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1350
|
+
return false;
|
|
1351
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1352
|
+
return false;
|
|
1353
|
+
try {
|
|
1354
|
+
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
1355
|
+
return false;
|
|
1356
|
+
// hardening-r5: preserve binary content as Uint8Array.
|
|
1357
|
+
const content = _readBundleCell(vfs, stripped);
|
|
1358
|
+
const cellLen = _bundleCellLength(content);
|
|
1359
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
1360
|
+
return false;
|
|
1361
|
+
bundle[stripped] = content;
|
|
1362
|
+
budgetState.totalBytes += cellLen;
|
|
1363
|
+
budgetState.fileCount++;
|
|
1364
|
+
added++;
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
catch {
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// Snapshot the keys first — we mutate `bundle` during the loop.
|
|
1372
|
+
const sourceKeys = Object.keys(bundle).filter((k) => k.endsWith('.js') || k.endsWith('.mjs') || k.endsWith('.cjs'));
|
|
1373
|
+
for (const sourcePath of sourceKeys) {
|
|
1374
|
+
const src = bundle[sourcePath];
|
|
1375
|
+
if (!src || src.length === 0)
|
|
1376
|
+
continue;
|
|
1377
|
+
// hardening-r5: skip binary cells (a .js extension on a binary file
|
|
1378
|
+
// is rare but possible — defensive guard prevents .replace() throwing
|
|
1379
|
+
// on a Uint8Array).
|
|
1380
|
+
if (typeof src !== 'string')
|
|
1381
|
+
continue;
|
|
1382
|
+
// Strip line + block comments before regex-matching so the pattern
|
|
1383
|
+
// doesn't fire inside `// fs.readFileSync(...)` etc.
|
|
1384
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1385
|
+
// Quick reject: skip files that don't even contain readFileSync.
|
|
1386
|
+
if (stripped.indexOf('readFileSync') < 0)
|
|
1387
|
+
continue;
|
|
1388
|
+
const sourceDir = sourcePath.includes('/')
|
|
1389
|
+
? sourcePath.substring(0, sourcePath.lastIndexOf('/'))
|
|
1390
|
+
: '';
|
|
1391
|
+
RX.lastIndex = 0;
|
|
1392
|
+
let match;
|
|
1393
|
+
while ((match = RX.exec(stripped)) !== null) {
|
|
1394
|
+
const quote = match[1];
|
|
1395
|
+
const rel = match[2];
|
|
1396
|
+
// Reject template-literal interpolation inside backticks.
|
|
1397
|
+
if (quote === '`' && rel.indexOf('${') >= 0)
|
|
1398
|
+
continue;
|
|
1399
|
+
// Reject any form that looks dynamic (defensive — RX already
|
|
1400
|
+
// requires literal but absolute paths starting with `/` would
|
|
1401
|
+
// bypass the __dirname-relative semantics; allow them since
|
|
1402
|
+
// they're literal and unambiguous).
|
|
1403
|
+
if (!ASSET_EXT.test(rel))
|
|
1404
|
+
continue;
|
|
1405
|
+
// Resolve relative to the source file's directory (the runtime's
|
|
1406
|
+
// __dirname for that source). Match runtime resolution: leading
|
|
1407
|
+
// `./` strips, `..` walks up.
|
|
1408
|
+
let resolved;
|
|
1409
|
+
if (rel.startsWith('/')) {
|
|
1410
|
+
resolved = rel.replace(/^\/+/, '');
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
const parts = (sourceDir + '/' + rel).split('/');
|
|
1414
|
+
const out = [];
|
|
1415
|
+
for (const seg of parts) {
|
|
1416
|
+
if (seg === '' || seg === '.')
|
|
1417
|
+
continue;
|
|
1418
|
+
if (seg === '..') {
|
|
1419
|
+
if (out.length > 0)
|
|
1420
|
+
out.pop();
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
out.push(seg);
|
|
1424
|
+
}
|
|
1425
|
+
resolved = out.join('/');
|
|
1426
|
+
}
|
|
1427
|
+
addOneAsset(resolved);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return { added };
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* X.5-U: scan every JS source already in `bundle` for static
|
|
1434
|
+
* readFileSync of a `__dirname`-relative dotfile or "digest/hash/version/
|
|
1435
|
+
* sha/md5"-shaped sentinel, AND match the SWC/TypeScript-compiled
|
|
1436
|
+
* `(0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, "<rel>"))`
|
|
1437
|
+
* call shape that X.5-Z3's `addStaticReadFileAssets` regex misses.
|
|
1438
|
+
*
|
|
1439
|
+
* Motivating case: ts-jest@29.x's
|
|
1440
|
+
* `package/dist/legacy/config/config-set.js:105`:
|
|
1441
|
+
*
|
|
1442
|
+
* var fs_1 = require("fs");
|
|
1443
|
+
* var path_1 = require("path");
|
|
1444
|
+
* exports.MY_DIGEST = (0, fs_1.readFileSync)(
|
|
1445
|
+
* (0, path_1.resolve)(__dirname, '../../../.ts-jest-digest'), 'utf8');
|
|
1446
|
+
*
|
|
1447
|
+
* The install pipeline writes `.ts-jest-digest` to VFS correctly
|
|
1448
|
+
* (manifest pass at buildManifest enumerates it). But the runtime
|
|
1449
|
+
* fs shim's readFileSync (`src/node-shims.ts:202-215`) consults
|
|
1450
|
+
* `__vfsBundle` only, and none of the existing bundle-population
|
|
1451
|
+
* passes — `prefetchForRequire` (require-graph), `greedyAddMainEntries`
|
|
1452
|
+
* (pkg main entries), `addStaticReadFileAssets` (X.5-Z3, restricted to
|
|
1453
|
+
* `.css|html|svg|txt|json` and to direct `path.resolve(__dirname,…)`)
|
|
1454
|
+
* — picks the dotfile up. Result: ENOENT at facet runtime even though
|
|
1455
|
+
* `fs.readdirSync` and `fs.statSync` both see the file via the manifest.
|
|
1456
|
+
*
|
|
1457
|
+
* Bounded heuristic: filename must EITHER start with `.` (dotfile) OR
|
|
1458
|
+
* match `/digest|hash|version|sha|md5/i` (small-metadata-sentinel
|
|
1459
|
+
* pattern). Without this gate, an unconstrained "match any
|
|
1460
|
+
* __dirname-relative readFileSync filename" would pull arbitrary large
|
|
1461
|
+
* runtime-loaded files (compiled WASM, JSON dictionaries, …) on
|
|
1462
|
+
* packages that read them via this exact shape — bundle bloat with no
|
|
1463
|
+
* payoff. The heuristic narrows to the ts-jest class. Trade-off
|
|
1464
|
+
* documented; future packages outside this shape can extend the
|
|
1465
|
+
* predicate.
|
|
1466
|
+
*
|
|
1467
|
+
* Quote chars supported: `'`, `"`, and backticks WITHOUT `${}`
|
|
1468
|
+
* interpolation. Dynamic specifiers (variable, concatenation,
|
|
1469
|
+
* interpolation) are deliberately skipped.
|
|
1470
|
+
*
|
|
1471
|
+
* Same budget shape as `greedyAddMainEntries` /
|
|
1472
|
+
* `addStaticReadFileAssets` — shares the same VFS_BUNDLE_MAX_FILES /
|
|
1473
|
+
* VFS_BUNDLE_MAX_BYTES caps via `budgetState`. Returns the count of
|
|
1474
|
+
* files added (for diagnostics).
|
|
1475
|
+
*
|
|
1476
|
+
* Errors are swallowed: missing assets, unreadable VFS, and
|
|
1477
|
+
* non-string readFile inputs are silent skips — matches Z3 posture.
|
|
1478
|
+
*/
|
|
1479
|
+
export function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, budgetState) {
|
|
1480
|
+
let added = 0;
|
|
1481
|
+
// The heuristic gate. Filenames matching either branch are eligible.
|
|
1482
|
+
// - Leading `.` covers `.ts-jest-digest`, `.cache-marker`, `.lintstagedrc`-ish
|
|
1483
|
+
// sentinel files. Note: `package.json` etc are NOT dotfiles.
|
|
1484
|
+
// - `digest|hash|version|sha|md5` covers compiled-loose sentinel
|
|
1485
|
+
// filenames like `version.txt`, `git-sha`, `build-hash`, …
|
|
1486
|
+
// (Phase B regression matrix §5: bounded to the actual class.)
|
|
1487
|
+
const FILENAME_GATE = /(^\.[^/]+$|digest|hash|version|sha|md5)/i;
|
|
1488
|
+
// Match shapes:
|
|
1489
|
+
// readFileSync(path.resolve(__dirname, "<rel>")) (X.5-Z3)
|
|
1490
|
+
// fs.readFileSync(path.resolve(__dirname, "<rel>")) (X.5-Z3)
|
|
1491
|
+
// (0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, "<rel>")) (X.5-U new — SWC)
|
|
1492
|
+
// readFileSync(path.join(__dirname, "<rel>")) (X.5-U new — join also)
|
|
1493
|
+
// readFileSync((0, path_1.resolve)(__dirname, "<rel>")) (mixed)
|
|
1494
|
+
//
|
|
1495
|
+
// Strategy: anchor the ENTIRE call on `readFileSync` (with optional
|
|
1496
|
+
// `(0, x.y)` wrap or `x.` prefix), then look for either `resolve` OR
|
|
1497
|
+
// `join` (with optional `(0, x.y)` wrap or `x.` prefix), then
|
|
1498
|
+
// `__dirname` and the literal. Capture group 1 = quote, group 2 =
|
|
1499
|
+
// body.
|
|
1500
|
+
//
|
|
1501
|
+
// The regex is permissive about whitespace + parens because
|
|
1502
|
+
// SWC/TypeScript emit varies (extra parens in some output flags,
|
|
1503
|
+
// tighter spacing in production). Tested against:
|
|
1504
|
+
// ts-jest@29.1.4/dist/legacy/config/config-set.js:105
|
|
1505
|
+
// synth `(0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, "X"))`
|
|
1506
|
+
// plain `fs.readFileSync(path.resolve(__dirname, "X"))`
|
|
1507
|
+
// plain `readFileSync(path.join(__dirname, "X"))`
|
|
1508
|
+
const RX = new RegExp(
|
|
1509
|
+
// optional `(0, ` wrap then `<x.>?readFileSync` or bare `readFileSync`
|
|
1510
|
+
'(?:\\(\\s*0\\s*,\\s*)?(?:[\\w$]+\\s*\\.\\s*)?readFileSync\\s*\\)?\\s*\\(' +
|
|
1511
|
+
// call args: optional outer paren, optional `(0, ` wrap then
|
|
1512
|
+
// `<x.>?(resolve|join)` then required `(`
|
|
1513
|
+
'\\s*(?:\\(\\s*0\\s*,\\s*)?(?:[\\w$]+\\s*\\.\\s*)?(?:resolve|join)\\s*\\)?\\s*\\(' +
|
|
1514
|
+
// required __dirname
|
|
1515
|
+
'\\s*__dirname\\s*,\\s*' +
|
|
1516
|
+
// literal: ' " ` (no ${ for backtick)
|
|
1517
|
+
'([\'"`])([^\'"`]+)\\1', 'g');
|
|
1518
|
+
function addOneAsset(absPath) {
|
|
1519
|
+
const stripped = absPath.replace(/^\/+/, '');
|
|
1520
|
+
if (stripped in bundle)
|
|
1521
|
+
return false;
|
|
1522
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1523
|
+
return false;
|
|
1524
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1525
|
+
return false;
|
|
1526
|
+
try {
|
|
1527
|
+
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
1528
|
+
return false;
|
|
1529
|
+
// hardening-r5: preserve binary content as Uint8Array.
|
|
1530
|
+
const content = _readBundleCell(vfs, stripped);
|
|
1531
|
+
const cellLen = _bundleCellLength(content);
|
|
1532
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
1533
|
+
return false;
|
|
1534
|
+
bundle[stripped] = content;
|
|
1535
|
+
budgetState.totalBytes += cellLen;
|
|
1536
|
+
budgetState.fileCount++;
|
|
1537
|
+
added++;
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
catch {
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
// Snapshot keys; we mutate `bundle` during the loop.
|
|
1545
|
+
const sourceKeys = Object.keys(bundle).filter((k) => k.endsWith('.js') || k.endsWith('.mjs') || k.endsWith('.cjs'));
|
|
1546
|
+
for (const sourcePath of sourceKeys) {
|
|
1547
|
+
const src = bundle[sourcePath];
|
|
1548
|
+
if (!src || src.length === 0)
|
|
1549
|
+
continue;
|
|
1550
|
+
// hardening-r5: skip binary cells (a .js extension on a binary file
|
|
1551
|
+
// is rare but possible — defensive guard prevents .replace() throwing
|
|
1552
|
+
// on a Uint8Array).
|
|
1553
|
+
if (typeof src !== 'string')
|
|
1554
|
+
continue;
|
|
1555
|
+
// Strip line + block comments before regex-matching so the pattern
|
|
1556
|
+
// doesn't fire inside `// fs.readFileSync(...)` etc.
|
|
1557
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1558
|
+
// Quick reject: skip files that don't even contain readFileSync.
|
|
1559
|
+
if (stripped.indexOf('readFileSync') < 0)
|
|
1560
|
+
continue;
|
|
1561
|
+
const sourceDir = sourcePath.includes('/')
|
|
1562
|
+
? sourcePath.substring(0, sourcePath.lastIndexOf('/'))
|
|
1563
|
+
: '';
|
|
1564
|
+
RX.lastIndex = 0;
|
|
1565
|
+
let match;
|
|
1566
|
+
while ((match = RX.exec(stripped)) !== null) {
|
|
1567
|
+
const quote = match[1];
|
|
1568
|
+
const rel = match[2];
|
|
1569
|
+
// Reject template-literal interpolation inside backticks.
|
|
1570
|
+
if (quote === '`' && rel.indexOf('${') >= 0)
|
|
1571
|
+
continue;
|
|
1572
|
+
// Resolve relative to the source file's __dirname (matches runtime).
|
|
1573
|
+
let resolved;
|
|
1574
|
+
if (rel.startsWith('/')) {
|
|
1575
|
+
resolved = rel.replace(/^\/+/, '');
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
const parts = (sourceDir + '/' + rel).split('/');
|
|
1579
|
+
const out = [];
|
|
1580
|
+
for (const seg of parts) {
|
|
1581
|
+
if (seg === '' || seg === '.')
|
|
1582
|
+
continue;
|
|
1583
|
+
if (seg === '..') {
|
|
1584
|
+
if (out.length > 0)
|
|
1585
|
+
out.pop();
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
out.push(seg);
|
|
1589
|
+
}
|
|
1590
|
+
resolved = out.join('/');
|
|
1591
|
+
}
|
|
1592
|
+
// Apply the bounded-heuristic gate on the BASENAME so we don't
|
|
1593
|
+
// overshoot. Z3's `ASSET_EXT` filter overlaps but doesn't cover
|
|
1594
|
+
// dotfiles or no-extension sentinels, which is X.5-U's class.
|
|
1595
|
+
const slash = resolved.lastIndexOf('/');
|
|
1596
|
+
const basename = slash >= 0 ? resolved.slice(slash + 1) : resolved;
|
|
1597
|
+
if (!FILENAME_GATE.test(basename))
|
|
1598
|
+
continue;
|
|
1599
|
+
addOneAsset(resolved);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return { added };
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* G3 (runtime-pkg wave) — bin-target sibling oversample.
|
|
1606
|
+
*
|
|
1607
|
+
* When the entry script lives at `node_modules/<pkg>/...` (typical
|
|
1608
|
+
* shape: cli.js, bin/foo, dist/index.js), bins commonly do
|
|
1609
|
+
* `readFileSync(path.join(__dirname, '<rel>'))` to load assets that
|
|
1610
|
+
* the static walker can't see (computed paths, package-internal
|
|
1611
|
+
* data files, .cow / .pem / .wasm / .ttf / etc.).
|
|
1612
|
+
*
|
|
1613
|
+
* Pre-fix: addStaticReadFileAssets only covers a hardcoded ASSET_EXT
|
|
1614
|
+
* whitelist (.css/.html/.htm/.svg/.txt/.json). Cowsay's `.cow` files
|
|
1615
|
+
* ENOENT at runtime.
|
|
1616
|
+
*
|
|
1617
|
+
* Fix shape: when entry is inside a `node_modules/<pkg>` directory,
|
|
1618
|
+
* walk that pkg dir's contents and pull every regular file under
|
|
1619
|
+
* VFS_BUNDLE_MAX_BYTES, capped at `MAX_PKG_FILES` per-pkg so a
|
|
1620
|
+
* 1000-file barrel package can't blow the bundle budget.
|
|
1621
|
+
*
|
|
1622
|
+
* The cap is intentionally generous (200 files): typical CLI
|
|
1623
|
+
* packages have <50 files; cowsay has ~85 (many .cow files);
|
|
1624
|
+
* commander has ~30. A 200-cap accommodates the long tail without
|
|
1625
|
+
* regressing big packages where the standard prefetch is already
|
|
1626
|
+
* doing the right thing.
|
|
1627
|
+
*
|
|
1628
|
+
* Caller already passed the `cwd` and the `scriptPath`; we only act
|
|
1629
|
+
* if scriptPath is /<...>/node_modules/<pkg>/... — anything else
|
|
1630
|
+
* (user scripts, npx-cache files outside node_modules, eval) is
|
|
1631
|
+
* a no-op.
|
|
1632
|
+
*/
|
|
1633
|
+
function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState) {
|
|
1634
|
+
if (!scriptPath)
|
|
1635
|
+
return { added: 0 };
|
|
1636
|
+
const stripped = scriptPath.replace(/^\/+/, '');
|
|
1637
|
+
// Find the *innermost* node_modules/<pkg> root. Handles scoped
|
|
1638
|
+
// packages (`@org/name`) too.
|
|
1639
|
+
const segs = stripped.split('/');
|
|
1640
|
+
let nmIdx = -1;
|
|
1641
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
1642
|
+
if (segs[i] === 'node_modules') {
|
|
1643
|
+
nmIdx = i;
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
if (nmIdx < 0)
|
|
1648
|
+
return { added: 0 };
|
|
1649
|
+
const isScoped = segs[nmIdx + 1]?.startsWith('@');
|
|
1650
|
+
const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
|
|
1651
|
+
if (pkgEnd > segs.length)
|
|
1652
|
+
return { added: 0 };
|
|
1653
|
+
const pkgRoot = segs.slice(0, pkgEnd).join('/');
|
|
1654
|
+
// npm-create-fix wave (2026-05-12): bumped from 200 to 1000 to cover
|
|
1655
|
+
// multi-template scaffolds. create-vite ships 242 files + 74 dirs (316
|
|
1656
|
+
// visit entries) across 21 template-* subdirs; the 200-cap exhausted
|
|
1657
|
+
// BFS budget before late-alphabetical template files (vanilla, vue, etc.)
|
|
1658
|
+
// were bundled, causing readFileSync ENOENT in the facet and silent
|
|
1659
|
+
// partial scaffolding (only .gitignore + index.html materialized for
|
|
1660
|
+
// `npm create vite@latest test-vite -- --template vanilla`).
|
|
1661
|
+
//
|
|
1662
|
+
// 1000 covers the documented create-* family (create-vite ~316, create-
|
|
1663
|
+
// nuxt ~700, create-react-router ~400). Still well below the global
|
|
1664
|
+
// VFS_BUNDLE_MAX_FILES = 4000 (constants.ts:74) and the
|
|
1665
|
+
// VFS_BUNDLE_MAX_BYTES = 24 MiB content cap, both of which retain the
|
|
1666
|
+
// defense against pathological 4000+ file barrel packages.
|
|
1667
|
+
//
|
|
1668
|
+
// See /workspace/.seal-internal/2026-05-12-npm-create-gap/findings.md
|
|
1669
|
+
// for the prior wave's empirical investigation (243 manifest entries,
|
|
1670
|
+
// only 140/243 readable pre-bump on prod 11df6ca).
|
|
1671
|
+
const MAX_PKG_FILES = 1000;
|
|
1672
|
+
// BFS walk pkgRoot. Skip nested `node_modules` (those are
|
|
1673
|
+
// separate packages with their own walk if/when they become
|
|
1674
|
+
// entry points).
|
|
1675
|
+
let added = 0;
|
|
1676
|
+
let visited = 0;
|
|
1677
|
+
const queue = [pkgRoot];
|
|
1678
|
+
while (queue.length > 0 && visited < MAX_PKG_FILES) {
|
|
1679
|
+
const dir = queue.shift();
|
|
1680
|
+
let entries;
|
|
1681
|
+
try {
|
|
1682
|
+
entries = vfs.readdir(dir);
|
|
1683
|
+
}
|
|
1684
|
+
catch {
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
for (const e of entries) {
|
|
1688
|
+
if (visited >= MAX_PKG_FILES)
|
|
1689
|
+
break;
|
|
1690
|
+
visited++;
|
|
1691
|
+
if (e.name === 'node_modules')
|
|
1692
|
+
continue;
|
|
1693
|
+
if (e.name === '.git')
|
|
1694
|
+
continue;
|
|
1695
|
+
const child = dir + '/' + e.name;
|
|
1696
|
+
if (e.type === 'directory') {
|
|
1697
|
+
queue.push(child);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
// File. Skip if already in bundle (the static walker beat us
|
|
1701
|
+
// to it). Otherwise pull, respecting global budget caps.
|
|
1702
|
+
if (bundle[child] !== undefined)
|
|
1703
|
+
continue;
|
|
1704
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1705
|
+
return { added };
|
|
1706
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1707
|
+
return { added };
|
|
1708
|
+
// hardening-r5: preserve binary content as Uint8Array.
|
|
1709
|
+
let content;
|
|
1710
|
+
try {
|
|
1711
|
+
content = _readBundleCell(vfs, child);
|
|
1712
|
+
}
|
|
1713
|
+
catch {
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const cellLen = _bundleCellLength(content);
|
|
1717
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
1718
|
+
return { added };
|
|
1719
|
+
bundle[child] = content;
|
|
1720
|
+
budgetState.totalBytes += cellLen;
|
|
1721
|
+
budgetState.fileCount++;
|
|
1722
|
+
added++;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
return { added };
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* BUG-SWEEP-R2-2 (2026-05-11): scan entry code (and any already-bundled
|
|
1729
|
+
* .js/.mjs/.cjs sources) for ABSOLUTE-PATH string literals that look
|
|
1730
|
+
* like file reads. For every candidate that exists on the SqliteVFS,
|
|
1731
|
+
* pull it into the bundle so the facet's __vfsBundle can serve it.
|
|
1732
|
+
*
|
|
1733
|
+
* Pre-fix: `node -e 'fs.readFileSync("/home/user/x.txt")'` returned
|
|
1734
|
+
* ENOENT for files the shell could `cat`. The facet's bundle never
|
|
1735
|
+
* included `/home/user/x.txt` because no static scanner matched the
|
|
1736
|
+
* shape (greedy/dotfile/asset all required `path.resolve(__dirname,
|
|
1737
|
+
* "rel")` or `node_modules` pkg-root containment).
|
|
1738
|
+
*
|
|
1739
|
+
* Match policy:
|
|
1740
|
+
* - String literals matching `/[^"`']+/` (slash-prefixed, no quotes
|
|
1741
|
+
* inside), length 2-512 chars.
|
|
1742
|
+
* - Reject paths under prefixes we don't mount (`/proc`, `/sys`,
|
|
1743
|
+
* `/dev`, `/lib`, `/lib64`) — wouldn't resolve.
|
|
1744
|
+
* - Reject paths containing `*` `?` `[` `]` `${` (glob/template).
|
|
1745
|
+
* - File must exist on VFS, be a file (not a dir).
|
|
1746
|
+
* - Defer to budgetState caps so we don't blow VFS_BUNDLE_MAX_BYTES.
|
|
1747
|
+
*
|
|
1748
|
+
* Quick-reject: only files containing readFileSync / createReadStream
|
|
1749
|
+
* / openSync / readFile in source are scanned. Entry code is always
|
|
1750
|
+
* scanned (it's the user's intent).
|
|
1751
|
+
*/
|
|
1752
|
+
function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
1753
|
+
let added = 0;
|
|
1754
|
+
// Capture absolute-path string literals. The path may not contain
|
|
1755
|
+
// the quote char; the surrounding regex strips line/block comments
|
|
1756
|
+
// first to avoid commented-out matches.
|
|
1757
|
+
// Path char set: alnum + dot + dash + slash + underscore. This
|
|
1758
|
+
// excludes spaces, glob chars, template syntax — all dynamic.
|
|
1759
|
+
const RX = /(['"`])(\/[A-Za-z0-9._\-\/]{1,510})\1/g;
|
|
1760
|
+
const REJECT_PREFIX = /^\/(proc|sys|dev|lib|lib64|boot|root)(\/|$)/;
|
|
1761
|
+
function tryAdd(absPath) {
|
|
1762
|
+
if (!absPath || absPath.length < 2 || absPath.length > 512)
|
|
1763
|
+
return;
|
|
1764
|
+
if (REJECT_PREFIX.test(absPath))
|
|
1765
|
+
return;
|
|
1766
|
+
const stripped = absPath.replace(/^\/+/, '');
|
|
1767
|
+
if (stripped in bundle)
|
|
1768
|
+
return;
|
|
1769
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1770
|
+
return;
|
|
1771
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1772
|
+
return;
|
|
1773
|
+
try {
|
|
1774
|
+
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
1775
|
+
return;
|
|
1776
|
+
// hardening-r5: preserve binary content as Uint8Array.
|
|
1777
|
+
const content = _readBundleCell(vfs, stripped);
|
|
1778
|
+
const cellLen = _bundleCellLength(content);
|
|
1779
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
1780
|
+
return;
|
|
1781
|
+
bundle[stripped] = content;
|
|
1782
|
+
budgetState.totalBytes += cellLen;
|
|
1783
|
+
budgetState.fileCount++;
|
|
1784
|
+
added++;
|
|
1785
|
+
}
|
|
1786
|
+
catch { /* swallow — file may be binary, race-deleted, etc. */ }
|
|
1787
|
+
}
|
|
1788
|
+
function scanOne(src) {
|
|
1789
|
+
if (!src)
|
|
1790
|
+
return;
|
|
1791
|
+
// Strip line + block comments so we don't match commented-out reads.
|
|
1792
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1793
|
+
RX.lastIndex = 0;
|
|
1794
|
+
let m;
|
|
1795
|
+
while ((m = RX.exec(stripped)) !== null) {
|
|
1796
|
+
const literal = m[2];
|
|
1797
|
+
// Skip if there's any unsafe char (defensive — RX already
|
|
1798
|
+
// forbids most). The check on the captured group is cheap.
|
|
1799
|
+
if (/[\?\*\[\]\{\}]/.test(literal))
|
|
1800
|
+
continue;
|
|
1801
|
+
tryAdd(literal);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
// Always scan entry code.
|
|
1805
|
+
scanOne(entryCode);
|
|
1806
|
+
// Optionally scan bundled JS sources too — useful for transitive cases
|
|
1807
|
+
// where a require'd module hardcodes an absolute path. Use the same
|
|
1808
|
+
// budget-state so we don't blow caps.
|
|
1809
|
+
const sourceKeys = Object.keys(bundle).filter((k) => k.endsWith('.js') || k.endsWith('.mjs') || k.endsWith('.cjs'));
|
|
1810
|
+
for (const k of sourceKeys) {
|
|
1811
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
1812
|
+
break;
|
|
1813
|
+
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1814
|
+
break;
|
|
1815
|
+
// hardening-r5: scanOne expects string. Binary cells (rare with a
|
|
1816
|
+
// .js extension but possible) are skipped — scanOne would throw on
|
|
1817
|
+
// a Uint8Array .replace() call.
|
|
1818
|
+
const cell = bundle[k];
|
|
1819
|
+
if (typeof cell !== 'string')
|
|
1820
|
+
continue;
|
|
1821
|
+
scanOne(cell);
|
|
1822
|
+
}
|
|
1823
|
+
return { added };
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* W3.5 Fix B helper — detect ESM source by sniffing for top-level
|
|
1827
|
+
* `import` / `export` STATEMENTS. Identifier/property uses (`obj.import`,
|
|
1828
|
+
* `pkg.export`) won't match because we anchor at start-of-line / `^\s*`.
|
|
1829
|
+
*
|
|
1830
|
+
* Comment stripping is intentionally cheap: regex over `//…` and
|
|
1831
|
+
* `/* … */` blocks. Strings containing the patterns won't be touched,
|
|
1832
|
+
* but esbuild produces valid CJS for any input it parses, so a false
|
|
1833
|
+
* positive is harmless (just a wasted transform).
|
|
1834
|
+
*
|
|
1835
|
+
* Misses to be aware of:
|
|
1836
|
+
* - JSX-only files with no import/export — but those wouldn't load via
|
|
1837
|
+
* plain new Function anyway (loader: 'js' rejects JSX).
|
|
1838
|
+
* - Files inside a "type":"module" package that don't use import/export
|
|
1839
|
+
* keywords (rare; nominally CJS-shaped). Accepted limitation, called
|
|
1840
|
+
* out in audit/sections/W3.5-plan.md §6.
|
|
1841
|
+
*/
|
|
1842
|
+
function looksLikeEsm(src) {
|
|
1843
|
+
// Cheap comment strip. Not a full parser — enough to avoid the
|
|
1844
|
+
// common false positives ("// import X" or `/* export */`).
|
|
1845
|
+
const stripped = src.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1846
|
+
// import STATEMENT: catches `^...import...`, `\n...import...`,
|
|
1847
|
+
// `;import...` (post-statement on same line), `}import...` (post-block).
|
|
1848
|
+
// Trailing `[\s{]` catches the no-whitespace minified form `import{...}from"..."`
|
|
1849
|
+
// shipped by @tailwindcss/vite/dist/index.mjs and other minified ESM bundles.
|
|
1850
|
+
// Skips dynamic `import(...)` (which is fine in CJS) because `(` ∉ `[\s{]`.
|
|
1851
|
+
// See audit/sections/X5Z5-plan.md §3 (both relaxations needed: leading
|
|
1852
|
+
// anchor [\n;}] AND trailing [\s{]).
|
|
1853
|
+
const importStmt = /(^|[\n;}])\s*import[\s{]/;
|
|
1854
|
+
// export STATEMENT: same dual relaxation. Trailing `[\s{*]` covers
|
|
1855
|
+
// `export ` (whitespace), `export{` (no-ws minified), and `export*`.
|
|
1856
|
+
const exportStmt = /(^|[\n;}])\s*export[\s{*]/;
|
|
1857
|
+
return importStmt.test(stripped) || exportStmt.test(stripped);
|
|
1858
|
+
}
|
|
1859
|
+
/**
|
|
1860
|
+
* W3.5 Fix B — module-level cache for ESM→CJS transform results, keyed
|
|
1861
|
+
* by content hash. A cheap FNV-1a 32-bit hash is enough (collisions are
|
|
1862
|
+
* astronomically rare for the size of bundles we ship; on collision the
|
|
1863
|
+
* pre-compile would still succeed because the cached result is a valid
|
|
1864
|
+
* CJS rebuild of an equally-valid ESM input).
|
|
1865
|
+
*
|
|
1866
|
+
* Lives at module scope so warm exec invocations hit the cache without
|
|
1867
|
+
* paying the wasm cold-start cost again.
|
|
1868
|
+
*/
|
|
1869
|
+
const __esmTransformCache = new Map();
|
|
1870
|
+
function __cacheKey(src) {
|
|
1871
|
+
// FNV-1a 32-bit. Only used for cache keys, NEVER for content
|
|
1872
|
+
// integrity. The ~30-byte string we return is a hex hash + length —
|
|
1873
|
+
// length disambiguates collisions across the rare 32-bit overlap.
|
|
1874
|
+
let h = 0x811c9dc5;
|
|
1875
|
+
for (let i = 0; i < src.length; i++) {
|
|
1876
|
+
h ^= src.charCodeAt(i);
|
|
1877
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
1878
|
+
}
|
|
1879
|
+
return h.toString(16).padStart(8, '0') + ':' + src.length.toString(16);
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* framework-fixes-F4 (2026-05-12): helper for the "esbuild unavailable
|
|
1883
|
+
* or fatally errored" paths. Walks the bundle for ESM-shaped files and
|
|
1884
|
+
* replaces each with a JS-valid diagnostic shim that throws an
|
|
1885
|
+
* informative Error at require-time. Mirrors the per-file catch in
|
|
1886
|
+
* transformEsmInBundle so the user gets the same actionable error
|
|
1887
|
+
* surface regardless of whether transform failed for the whole batch
|
|
1888
|
+
* or for one file.
|
|
1889
|
+
*
|
|
1890
|
+
* Does NOT touch non-ESM files (which the pre-compile loop handles
|
|
1891
|
+
* fine as-is).
|
|
1892
|
+
*/
|
|
1893
|
+
function _markBundleEsmAsFailed(bundle, reason) {
|
|
1894
|
+
for (const path of Object.keys(bundle)) {
|
|
1895
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs'))
|
|
1896
|
+
continue;
|
|
1897
|
+
const src = bundle[path];
|
|
1898
|
+
if (typeof src !== 'string')
|
|
1899
|
+
continue;
|
|
1900
|
+
if (!looksLikeEsm(src))
|
|
1901
|
+
continue;
|
|
1902
|
+
const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
|
|
1903
|
+
bundle[path] =
|
|
1904
|
+
'// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
1905
|
+
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Transform every ESM-shaped file in the bundle to CJS via esbuild.
|
|
1910
|
+
* Mutates `bundle` in place. Errors are swallowed (the file is left as
|
|
1911
|
+
* ESM source); the facet's pre-compile loop will record the SyntaxError
|
|
1912
|
+
* into __compileFailures and __loadModule will surface it (Fix C).
|
|
1913
|
+
*
|
|
1914
|
+
* Skips:
|
|
1915
|
+
* - .json (esbuild can transform but there's no payoff and it's a
|
|
1916
|
+
* no-op on our pre-compile loop too).
|
|
1917
|
+
* - .cjs (already CJS; transform is a wash).
|
|
1918
|
+
* - files that pass the regex sniff cleanly (heuristic: no top-level
|
|
1919
|
+
* import/export → CJS-shaped already).
|
|
1920
|
+
*
|
|
1921
|
+
* Returns the count of files transformed (for diagnostics).
|
|
1922
|
+
*/
|
|
1923
|
+
async function transformEsmInBundle(bundle, esbuild) {
|
|
1924
|
+
let transformed = 0;
|
|
1925
|
+
let failed = 0;
|
|
1926
|
+
// Snapshot the keys first — esbuild calls await; never iterate-and-mutate.
|
|
1927
|
+
const candidates = [];
|
|
1928
|
+
for (const path of Object.keys(bundle)) {
|
|
1929
|
+
if (!path.endsWith('.js') && !path.endsWith('.mjs'))
|
|
1930
|
+
continue;
|
|
1931
|
+
const src = bundle[path];
|
|
1932
|
+
// hardening-r5: binary cells (rare for .js/.mjs but defensive) are
|
|
1933
|
+
// not ESM. Skip — looksLikeEsm + esbuild.transform expect strings.
|
|
1934
|
+
if (typeof src !== 'string')
|
|
1935
|
+
continue;
|
|
1936
|
+
if (!looksLikeEsm(src))
|
|
1937
|
+
continue;
|
|
1938
|
+
candidates.push(path);
|
|
1939
|
+
}
|
|
1940
|
+
for (const path of candidates) {
|
|
1941
|
+
const src = bundle[path];
|
|
1942
|
+
if (typeof src !== 'string')
|
|
1943
|
+
continue;
|
|
1944
|
+
// `import.meta.url` substitution mirrors the sibling fix at
|
|
1945
|
+
// src/runtime/runtime-registry.ts:383-389 (framework-gaps-fix P5).
|
|
1946
|
+
// Without `define`, esbuild's CJS transform reduces `import.meta.url`
|
|
1947
|
+
// to undefined (single-pass) or preserves it literally — only to
|
|
1948
|
+
// SyntaxError at `new Function(...)` parse time (two-pass via
|
|
1949
|
+
// EsbuildService.transform's async-IIFE wrap → "Cannot use
|
|
1950
|
+
// 'import.meta' outside a module"). The substitution value is the
|
|
1951
|
+
// real `file:///<absolute-path>` URL — exactly what real Node returns
|
|
1952
|
+
// for an ESM module at that path.
|
|
1953
|
+
//
|
|
1954
|
+
// Note: cache key now incorporates the path because the transformed
|
|
1955
|
+
// output is path-specific (the URL literal is baked in). Two files
|
|
1956
|
+
// with identical source but different paths would otherwise share a
|
|
1957
|
+
// cache entry and the second file would get the first file's URL.
|
|
1958
|
+
const absUrl = 'file:///' + path.replace(/^\/+/, '');
|
|
1959
|
+
const key = __cacheKey(src + '\0' + absUrl);
|
|
1960
|
+
const cached = __esmTransformCache.get(key);
|
|
1961
|
+
if (cached) {
|
|
1962
|
+
bundle[path] = cached;
|
|
1963
|
+
transformed++;
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
const t = await esbuild.transform(src, {
|
|
1968
|
+
loader: 'js',
|
|
1969
|
+
format: 'cjs',
|
|
1970
|
+
target: 'esnext',
|
|
1971
|
+
define: {
|
|
1972
|
+
'import.meta.url': JSON.stringify(absUrl),
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
bundle[path] = t.code;
|
|
1976
|
+
__esmTransformCache.set(key, t.code);
|
|
1977
|
+
transformed++;
|
|
1978
|
+
}
|
|
1979
|
+
catch (e) {
|
|
1980
|
+
// framework-fixes-F4 (2026-05-12): replace the source with a
|
|
1981
|
+
// JS-valid module that throws an INFORMATIVE Error when require'd.
|
|
1982
|
+
//
|
|
1983
|
+
// Pre-fix this catch swallowed the failure silently. The pre-
|
|
1984
|
+
// compile loop then re-saw the ESM source and emitted the
|
|
1985
|
+
// SyntaxError "Cannot use import statement outside a module" into
|
|
1986
|
+
// __compileFailures. __loadModule surfaced that as the user error
|
|
1987
|
+
// — but it told the user nothing about WHY esbuild rejected the
|
|
1988
|
+
// file (was it TLA at module scope? a TypeScript file mistyped
|
|
1989
|
+
// as .js? an unsupported syntax? our esbuild service not init?).
|
|
1990
|
+
//
|
|
1991
|
+
// Post-fix the replacement source is parseable CJS that, when
|
|
1992
|
+
// require()'d, throws a real Error carrying the esbuild reason.
|
|
1993
|
+
// The pre-compile loop succeeds, the file lands in
|
|
1994
|
+
// __compiledModules, and the failure surfaces at require time
|
|
1995
|
+
// with FULL diagnostic context. Net effect for users: the same
|
|
1996
|
+
// upstream tool still fails, but they now see WHY.
|
|
1997
|
+
//
|
|
1998
|
+
// Probe: tests/behavioral/npm-create/new/create-astro-diagnostic.mjs
|
|
1999
|
+
// asserts the user-visible error contains "esbuild transform" so
|
|
2000
|
+
// future diagnostic regressions are caught.
|
|
2001
|
+
const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
|
|
2002
|
+
const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
|
|
2003
|
+
// Build a valid CJS module that throws on load. Use IIFE to keep
|
|
2004
|
+
// module-scope flat (no TLA, no top-level await). Esbuild-cache
|
|
2005
|
+
// this so repeated submits don't re-run the same transform.
|
|
2006
|
+
const diagnosticSrc = '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
2007
|
+
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
2008
|
+
bundle[path] = diagnosticSrc;
|
|
2009
|
+
__esmTransformCache.set(key, diagnosticSrc);
|
|
2010
|
+
failed++;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
return { transformed, failed };
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* W2.6a: build the prefetch bundle for FacetManager.exec.
|
|
2017
|
+
*
|
|
2018
|
+
* Replaces the legacy whole-tree-with-cap walk that pre-W2.5b shipped
|
|
2019
|
+
* up to 500 files / 4 MiB of node_modules content (whichever ran out
|
|
2020
|
+
* first) with a static reachable-set walk via require-resolver.ts. The
|
|
2021
|
+
* shipped bundle is now bounded by what the user's require() chain
|
|
2022
|
+
* actually reaches PLUS a greedy oversample of every installed pkg's
|
|
2023
|
+
* package.json + main entry (dynamic-require survival).
|
|
2024
|
+
*
|
|
2025
|
+
* Cap is on the JSON-encoded size of the final payload, not on raw
|
|
2026
|
+
* content byte sum. The dynamic worker module embeds the bundle as
|
|
2027
|
+
* `const __MODULE_VFS_BUNDLE = ${JSON.stringify(bundle)}`, so workerd's
|
|
2028
|
+
* per-module text-size limit applies to the encoded form.
|
|
2029
|
+
*
|
|
2030
|
+
* W3.5: now async to allow the optional ESM→CJS pre-pass via esbuild.
|
|
2031
|
+
* If `esbuild` is not provided, the pass is skipped (preserves prior
|
|
2032
|
+
* behaviour for code paths that don't have esbuild handy).
|
|
2033
|
+
*
|
|
2034
|
+
* See audit/sections/W2.6-plan.md §3 (B1+ verdict + sub-agent review).
|
|
2035
|
+
*/
|
|
2036
|
+
async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild) {
|
|
2037
|
+
// 1. Static reachable-set walk from entry.
|
|
2038
|
+
const prefetch = prefetchForRequire(vfs, entryCode || '', cwd, scriptPath);
|
|
2039
|
+
const bundle = { ...prefetch.bundle };
|
|
2040
|
+
let totalBytes = 0;
|
|
2041
|
+
let fileCount = 0;
|
|
2042
|
+
for (const k of Object.keys(bundle)) {
|
|
2043
|
+
totalBytes += bundle[k].length;
|
|
2044
|
+
fileCount++;
|
|
2045
|
+
}
|
|
2046
|
+
let truncated = prefetch.truncated;
|
|
2047
|
+
// 2. Greedy oversample — every installed pkg's pkg.json + main.
|
|
2048
|
+
// Catches dynamic-require / `bindings()` / plugin-loader cases the
|
|
2049
|
+
// regex prefetch misses. Bounded by VFS_BUNDLE_MAX_BYTES.
|
|
2050
|
+
const budgetState = { totalBytes, fileCount };
|
|
2051
|
+
const greedy = greedyAddMainEntries(vfs, cwd, bundle, budgetState);
|
|
2052
|
+
totalBytes = budgetState.totalBytes;
|
|
2053
|
+
fileCount = budgetState.fileCount;
|
|
2054
|
+
// 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
|
|
2055
|
+
// bundle .js/.mjs/.cjs source for the canonical jsdom shape:
|
|
2056
|
+
//
|
|
2057
|
+
// fs.readFileSync(path.resolve(__dirname, "<rel>.css"), …)
|
|
2058
|
+
//
|
|
2059
|
+
// and pulls the matched asset into the bundle. Without this,
|
|
2060
|
+
// `default-stylesheet.css` (and similar runtime asset reads in
|
|
2061
|
+
// tldts, parse5, lookup-table packages, mime-db, etc.) ENOENT
|
|
2062
|
+
// at facet runtime even though the file is on VFS-disk + in
|
|
2063
|
+
// the manifest. See audit/sections/X5Z3-plan.md §3.
|
|
2064
|
+
const assetAdd = addStaticReadFileAssets(vfs, cwd, bundle, budgetState);
|
|
2065
|
+
void assetAdd;
|
|
2066
|
+
totalBytes = budgetState.totalBytes;
|
|
2067
|
+
fileCount = budgetState.fileCount;
|
|
2068
|
+
// 2.27 X.5-U: dotfile + SWC-shape readFileSync sentinel prefetch.
|
|
2069
|
+
// Sibling of `addStaticReadFileAssets` (X.5-Z3) — same call shape,
|
|
2070
|
+
// different match space. Covers the SWC/TS-compiled
|
|
2071
|
+
// `(0, fs_1.readFileSync)((0, path_1.resolve)(__dirname, "<rel>"))`
|
|
2072
|
+
// pattern AND filenames outside the Z3 ASSET_EXT whitelist
|
|
2073
|
+
// (dotfiles, no-extension sentinels, "digest/hash/version/sha/md5"
|
|
2074
|
+
// shapes). Motivating case: ts-jest's `.ts-jest-digest`. See
|
|
2075
|
+
// audit/sections/X5U-plan.md §4.
|
|
2076
|
+
const dotAdd = addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, budgetState);
|
|
2077
|
+
void dotAdd;
|
|
2078
|
+
totalBytes = budgetState.totalBytes;
|
|
2079
|
+
fileCount = budgetState.fileCount;
|
|
2080
|
+
// 2.30 G3 (runtime-pkg wave): bin-target sibling oversample. Pulls
|
|
2081
|
+
// ALL files under the entry's package root (capped at 200) so
|
|
2082
|
+
// bins like cowsay that readFileSync('cows/X.cow') at runtime
|
|
2083
|
+
// find their data files. Existing greedy/asset/dotfile passes
|
|
2084
|
+
// cover JS sources + a hardcoded ASSET_EXT list; this one
|
|
2085
|
+
// catches custom extensions (.cow, .pem, .ttf, .wasm bundled
|
|
2086
|
+
// as data, etc.) without needing a per-pkg whitelist.
|
|
2087
|
+
// No-op when entry isn't inside node_modules.
|
|
2088
|
+
const binSiblingAdd = addBinTargetSiblings(vfs, scriptPath, bundle, budgetState);
|
|
2089
|
+
void binSiblingAdd;
|
|
2090
|
+
totalBytes = budgetState.totalBytes;
|
|
2091
|
+
fileCount = budgetState.fileCount;
|
|
2092
|
+
// 2.35 BUG-SWEEP-R2-2: absolute-path readFileSync scanner.
|
|
2093
|
+
//
|
|
2094
|
+
// Pre-fix `node -e 'fs.readFileSync("/home/user/er.txt")'` returned
|
|
2095
|
+
// ENOENT even when the file existed on the SqliteVFS (verified via
|
|
2096
|
+
// `cat`/`ls`). Cause: buildPrefetchBundle's existing scanners
|
|
2097
|
+
// (greedy main entries, addStaticReadFileAssets, dotfiles, bin
|
|
2098
|
+
// siblings) all look at INSTALLED packages or relative-resolve
|
|
2099
|
+
// patterns inside the bundle's source files. They never scan the
|
|
2100
|
+
// user's ENTRY CODE for absolute-path string literals, so user
|
|
2101
|
+
// files like /home/user/data.json or /tmp/cache.bin never reached
|
|
2102
|
+
// the facet's __vfsBundle.
|
|
2103
|
+
//
|
|
2104
|
+
// Fix: scan entryCode + every JS source already in the bundle for
|
|
2105
|
+
// any string literal that LOOKS like an absolute path (`/x/y/z`,
|
|
2106
|
+
// optionally inside a readFileSync/createReadStream/open call).
|
|
2107
|
+
// For each candidate that exists on the VFS (and isn't already in
|
|
2108
|
+
// the bundle), pull it in within the byte budget.
|
|
2109
|
+
//
|
|
2110
|
+
// Defenses:
|
|
2111
|
+
// - Only path-shaped strings (starts with `/`, length 1-512).
|
|
2112
|
+
// - VFS file existence + non-dir check before adding.
|
|
2113
|
+
// - Byte/file budget guards identical to other passes.
|
|
2114
|
+
// - Skip paths under known-untrusted prefixes (`/proc`, `/sys`,
|
|
2115
|
+
// `/dev` — we don't have these mounts; they'd never resolve).
|
|
2116
|
+
const absScanAdd = addEntryAbsPathReads(vfs, entryCode || '', bundle, budgetState);
|
|
2117
|
+
void absScanAdd;
|
|
2118
|
+
totalBytes = budgetState.totalBytes;
|
|
2119
|
+
fileCount = budgetState.fileCount;
|
|
2120
|
+
// 2.5 W3.5 Fix B: ESM→CJS transform pass. Walks `bundle`, sniffs each
|
|
2121
|
+
// .js/.mjs for top-level import/export, runs esbuild's CJS transform
|
|
2122
|
+
// on the matches, and replaces the value in-place. Without this,
|
|
2123
|
+
// ESM files (e.g. tldts/dist/es6/index.js, @remix-run/react/dist/esm,
|
|
2124
|
+
// @tailwindcss/vite, react-remove-scroll, astro) silently fail
|
|
2125
|
+
// `new Function` at facet startup and surface as the misleading
|
|
2126
|
+
// "file was not pre-bundled" at request time.
|
|
2127
|
+
if (esbuild) {
|
|
2128
|
+
try {
|
|
2129
|
+
await transformEsmInBundle(bundle, esbuild);
|
|
2130
|
+
// Recount bytes after the transform — CJS rebuilds can be larger
|
|
2131
|
+
// OR smaller than the ESM source. We don't try to thread totalBytes
|
|
2132
|
+
// through the transform because the eviction loop below recomputes
|
|
2133
|
+
// the encoded size from scratch anyway.
|
|
2134
|
+
}
|
|
2135
|
+
catch (e) {
|
|
2136
|
+
// framework-fixes-F4 (2026-05-12): the prior catch was silent.
|
|
2137
|
+
// Replace every detected-ESM source with the SAME diagnostic
|
|
2138
|
+
// shim transformEsmInBundle's per-file catch installs, so the
|
|
2139
|
+
// user sees "esbuild transform failed (service-level): <reason>"
|
|
2140
|
+
// instead of a bare "Cannot use import statement..." parse
|
|
2141
|
+
// error with no context.
|
|
2142
|
+
const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
|
|
2143
|
+
_markBundleEsmAsFailed(bundle, `esbuild service unavailable: ${reason}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
else {
|
|
2147
|
+
// framework-fixes-F4 (2026-05-12): no esbuild service available at
|
|
2148
|
+
// all (lazy-init failed or never wired). Same diagnostic-shim
|
|
2149
|
+
// treatment so users see WHY the ESM file couldn't be transformed.
|
|
2150
|
+
_markBundleEsmAsFailed(bundle, 'esbuild service not initialized (likely lazy-init failure)');
|
|
2151
|
+
}
|
|
2152
|
+
// 3. Manifest pass — UNCHANGED from W2.5b. Decouples directory shape
|
|
2153
|
+
// from content cap so fs.readdirSync remains honest even if the
|
|
2154
|
+
// content for a given file was capped out.
|
|
2155
|
+
const manifest = buildManifest(vfs, cwd, scriptPath);
|
|
2156
|
+
// 4. JSON-encoded-size guard. Pre-check via TextEncoder.encode().length
|
|
2157
|
+
// so we measure UTF-8 bytes (not UTF-16 code units), matching what
|
|
2158
|
+
// workerd accounts against the per-module text-size budget. If the
|
|
2159
|
+
// bundle exceeds the encoded ceiling, evict largest non-manifest
|
|
2160
|
+
// files first (manifest stays — it's needed for readdirSync) and
|
|
2161
|
+
// RECOMPUTE the encoded size after every eviction (sub-agent S2:
|
|
2162
|
+
// naïve `encoded -= len(file) + len(key) + 6` accumulates 2-5% drift
|
|
2163
|
+
// on JS-source-heavy bundles; recomputing is O(n) per eviction but
|
|
2164
|
+
// bundles past the budget are rare and the count of evictions is
|
|
2165
|
+
// bounded by the size of a few large files).
|
|
2166
|
+
const encoder = new TextEncoder();
|
|
2167
|
+
let encoded = encoder.encode(JSON.stringify({ bundle, manifest })).length;
|
|
2168
|
+
if (encoded > BUNDLE_MAX_ENCODED_BYTES) {
|
|
2169
|
+
truncated = true;
|
|
2170
|
+
const keysBySize = Object.keys(bundle).sort((a, b) => bundle[b].length - bundle[a].length);
|
|
2171
|
+
for (const k of keysBySize) {
|
|
2172
|
+
if (encoded <= BUNDLE_MAX_ENCODED_BYTES)
|
|
2173
|
+
break;
|
|
2174
|
+
delete bundle[k];
|
|
2175
|
+
fileCount--;
|
|
2176
|
+
encoded = encoder.encode(JSON.stringify({ bundle, manifest })).length;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
// Suppress lint: `greedy.added` is observed only via diagnostics.
|
|
2180
|
+
void greedy;
|
|
2181
|
+
return { bundle, manifest, reachableCount: fileCount, truncated };
|
|
2182
|
+
}
|
|
2183
|
+
export class FacetManager {
|
|
2184
|
+
ctx;
|
|
2185
|
+
env;
|
|
2186
|
+
processTable;
|
|
2187
|
+
portRegistry;
|
|
2188
|
+
vfs = null;
|
|
2189
|
+
/** Track whether facets API is available (detected on first use). */
|
|
2190
|
+
_hasFacets = null;
|
|
2191
|
+
_facetLogOnce = false;
|
|
2192
|
+
hooks;
|
|
2193
|
+
/**
|
|
2194
|
+
* W3.5 Fix B: lazily-created EsbuildService for the ESM→CJS pre-pass
|
|
2195
|
+
* over the prefetch bundle. Created on first exec where vfs is set;
|
|
2196
|
+
* shared across subsequent execs (warm wasm). Optional setter
|
|
2197
|
+
* `setEsbuildService` lets NimbusSession share its existing instance
|
|
2198
|
+
* to avoid double-init.
|
|
2199
|
+
*/
|
|
2200
|
+
esbuild = null;
|
|
2201
|
+
constructor(ctx, env, processTable, portRegistry, hooks = {}) {
|
|
2202
|
+
this.ctx = ctx;
|
|
2203
|
+
this.env = env;
|
|
2204
|
+
this.processTable = processTable;
|
|
2205
|
+
this.portRegistry = portRegistry;
|
|
2206
|
+
this.hooks = hooks;
|
|
2207
|
+
}
|
|
2208
|
+
setVfs(vfs) { this.vfs = vfs; }
|
|
2209
|
+
/**
|
|
2210
|
+
* W3.5 Fix B: hand the FacetManager a pre-warmed EsbuildService for
|
|
2211
|
+
* the ESM→CJS bundle pre-pass. NimbusSession already lazy-creates one
|
|
2212
|
+
* for the user-shell `node` runtime; sharing avoids paying init twice.
|
|
2213
|
+
*/
|
|
2214
|
+
setEsbuildService(esbuild) { this.esbuild = esbuild; }
|
|
2215
|
+
/**
|
|
2216
|
+
* Execute JS code in a facet (or fallback dynamic worker).
|
|
2217
|
+
*
|
|
2218
|
+
* Strategy:
|
|
2219
|
+
* 1. Try LOADER.get() + ctx.facets.get() (production: warm reuse, own SQLite)
|
|
2220
|
+
* 2. Fallback: LOADER.load() + getEntrypoint().fetch() (local dev)
|
|
2221
|
+
*/
|
|
2222
|
+
async exec(code, opts) {
|
|
2223
|
+
const command = opts.command
|
|
2224
|
+
|| (opts.filename && opts.filename !== '<eval>'
|
|
2225
|
+
? `node ${opts.filename}` : 'node -e ...');
|
|
2226
|
+
let entry;
|
|
2227
|
+
if (opts.skipSpawn && opts.callerPid != null) {
|
|
2228
|
+
// The caller already allocated the PID via processTable.spawn
|
|
2229
|
+
// (with their own user-facing label). Look up the full entry
|
|
2230
|
+
// from the table — _execWithTimeout etc. need the canonical
|
|
2231
|
+
// ProcessEntry shape. Do NOT reap() either: reaping would
|
|
2232
|
+
// clear the caller's just-spawned entry because its startTime
|
|
2233
|
+
// is recent (< 60s) but reap() ALSO drops 'running' entries
|
|
2234
|
+
// older than the threshold; in any case we don't want side
|
|
2235
|
+
// effects when the caller is delegating PID ownership.
|
|
2236
|
+
const found = this.processTable.get(opts.callerPid);
|
|
2237
|
+
if (!found) {
|
|
2238
|
+
throw new Error(`facetMgr.exec skipSpawn: callerPid=${opts.callerPid} not in processTable`);
|
|
2239
|
+
}
|
|
2240
|
+
entry = found;
|
|
2241
|
+
}
|
|
2242
|
+
else {
|
|
2243
|
+
this.processTable.reap();
|
|
2244
|
+
entry = this.processTable.spawn(command, opts.argv || [], opts.cwd || '/home/user');
|
|
2245
|
+
// Short foreground `node -e ...` helpers are quiet by design — only
|
|
2246
|
+
// notify for user-facing `node <file>` invocations, which covers the
|
|
2247
|
+
// real user intent (running scripts, wrangler, etc.).
|
|
2248
|
+
if (opts.filename && opts.filename !== '<eval>') {
|
|
2249
|
+
try {
|
|
2250
|
+
this.hooks.onSpawn?.(entry.pid, command, false);
|
|
2251
|
+
}
|
|
2252
|
+
catch { }
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so
|
|
2256
|
+
// ESM source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
2257
|
+
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to
|
|
2258
|
+
// CJS before they hit the facet's `new Function` pre-compile loop.
|
|
2259
|
+
// Lazy-create one if NimbusSession didn't share its own.
|
|
2260
|
+
if (this.vfs && !this.esbuild) {
|
|
2261
|
+
try {
|
|
2262
|
+
this.esbuild = new EsbuildService(this.vfs);
|
|
2263
|
+
}
|
|
2264
|
+
catch {
|
|
2265
|
+
this.esbuild = null;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
const vfsState = this.vfs
|
|
2269
|
+
? await buildPrefetchBundle(this.vfs, opts.filename, opts.cwd || '/home/user', code, this.esbuild || undefined)
|
|
2270
|
+
: { bundle: {}, manifest: {}, reachableCount: 0, truncated: false };
|
|
2271
|
+
try {
|
|
2272
|
+
let result;
|
|
2273
|
+
// Try facets API first (production path)
|
|
2274
|
+
if (this._hasFacets !== false) {
|
|
2275
|
+
try {
|
|
2276
|
+
result = await this._execWithTimeout(this._execViaFacets(code, opts, entry, vfsState), entry);
|
|
2277
|
+
this._hasFacets = true;
|
|
2278
|
+
this.processTable.exit(entry.pid, result.exitCode);
|
|
2279
|
+
// W5 Lever 5: zero-silent-OOM contract — every non-zero exit
|
|
2280
|
+
// must yield a ring entry.
|
|
2281
|
+
if (result.exitCode !== 0) {
|
|
2282
|
+
this._w5RecordTermination(entry.pid, result.exitCode, 'facet', result.stderr || `exit ${result.exitCode}`);
|
|
2283
|
+
}
|
|
2284
|
+
this._flushVfsWrites(result);
|
|
2285
|
+
return result;
|
|
2286
|
+
}
|
|
2287
|
+
catch (facetErr) {
|
|
2288
|
+
// Detect if it's a facets-unavailable error vs a user code error
|
|
2289
|
+
const isFacetApiError = facetErr?.message?.includes('is not a function') ||
|
|
2290
|
+
facetErr?.message?.includes('facets') ||
|
|
2291
|
+
facetErr?.message?.includes('getDurableObjectClass') ||
|
|
2292
|
+
facetErr?.message?.includes('not available');
|
|
2293
|
+
if (!isFacetApiError) {
|
|
2294
|
+
// User code error — don't fall back. Route through the
|
|
2295
|
+
// external-exit hook so the supervisor's log store records
|
|
2296
|
+
// the exit AND the structured {type:'exit'} notification
|
|
2297
|
+
// fires on the terminal WS (otherwise the tabs UI shows a
|
|
2298
|
+
// stuck "running" dot for facets that crash before they get
|
|
2299
|
+
// a chance to self-report).
|
|
2300
|
+
this.processTable.exit(entry.pid, 1);
|
|
2301
|
+
this._w5RecordTermination(entry.pid, 1, 'facet', 'facet error: ' + (facetErr?.message || String(facetErr)));
|
|
2302
|
+
try {
|
|
2303
|
+
this.hooks.onExternalExit?.(entry.pid, 1, 'facet error: ' + (facetErr?.message || String(facetErr)));
|
|
2304
|
+
}
|
|
2305
|
+
catch { }
|
|
2306
|
+
this._flushVfsWrites({ exitCode: 1, stdout: '', stderr: '', vfsWrites: {} });
|
|
2307
|
+
return { exitCode: 1, stdout: '', stderr: facetErr?.message || String(facetErr) };
|
|
2308
|
+
}
|
|
2309
|
+
// Mark facets as unavailable and fall through
|
|
2310
|
+
if (this._hasFacets === null) {
|
|
2311
|
+
this._hasFacets = false;
|
|
2312
|
+
if (!this._facetLogOnce) {
|
|
2313
|
+
this._facetLogOnce = true;
|
|
2314
|
+
console.log('[nimbus] Facets API unavailable, using LOADER.load() fallback');
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
// Fallback: LOADER.load() (local dev)
|
|
2320
|
+
result = await this._execWithTimeout(this._execViaLoader(code, opts, entry, vfsState), entry);
|
|
2321
|
+
this.processTable.exit(entry.pid, result.exitCode);
|
|
2322
|
+
if (result.exitCode !== 0) {
|
|
2323
|
+
this._w5RecordTermination(entry.pid, result.exitCode, 'facet', result.stderr || `exit ${result.exitCode}`);
|
|
2324
|
+
}
|
|
2325
|
+
this._flushVfsWrites(result);
|
|
2326
|
+
return result;
|
|
2327
|
+
}
|
|
2328
|
+
catch (err) {
|
|
2329
|
+
// If the timeout already fired, it already called onExternalExit
|
|
2330
|
+
// with code 124 and reason "timeout…". Don't clobber that with a
|
|
2331
|
+
// generic exit code 1. (_reportExternalExit's guard separately
|
|
2332
|
+
// prevents double-dump; this stops ProcessTable from showing a
|
|
2333
|
+
// different exit code than the ring buffer's footer.)
|
|
2334
|
+
const timedOut = !!entry.__timedOut;
|
|
2335
|
+
const exitCode = timedOut ? 124 : 1;
|
|
2336
|
+
this.processTable.exit(entry.pid, exitCode);
|
|
2337
|
+
// W5 Lever 5: ring entry on every catch-path exit.
|
|
2338
|
+
this._w5RecordTermination(entry.pid, exitCode, timedOut ? 'rpc' : 'facet', timedOut
|
|
2339
|
+
? 'timeout'
|
|
2340
|
+
: ('facet error: ' + (err?.message || String(err))));
|
|
2341
|
+
// Non-timeout failure: route through external-exit so the log
|
|
2342
|
+
// store marks exit AND the tabs-UI structured event fires. The
|
|
2343
|
+
// timeout path already called onExternalExit from the timeout
|
|
2344
|
+
// handler; _reportExternalExit's getExit() guard dedupes.
|
|
2345
|
+
if (!timedOut) {
|
|
2346
|
+
try {
|
|
2347
|
+
this.hooks.onExternalExit?.(entry.pid, exitCode, 'facet error: ' + (err?.message || String(err)));
|
|
2348
|
+
}
|
|
2349
|
+
catch { }
|
|
2350
|
+
}
|
|
2351
|
+
return { exitCode, stdout: '', stderr: err?.message || String(err) };
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* W5 Lever 5: push a DiagFailure into the OOM ring for every facet
|
|
2356
|
+
* termination with a non-zero exit code. This is the supervisor side
|
|
2357
|
+
* of the zero-silent-OOM contract — the audit/probes/w5/e2e/
|
|
2358
|
+
* oom-stress probe asserts that every termination has a matching
|
|
2359
|
+
* ring entry.
|
|
2360
|
+
*
|
|
2361
|
+
* Classification: parse the reason/stderr for SQLITE_NOMEM, OOM,
|
|
2362
|
+
* clone-refused, rpc_timeout signatures (oom-classify.ts). Code 124
|
|
2363
|
+
* always maps to rpc_timeout regardless of message.
|
|
2364
|
+
*/
|
|
2365
|
+
_w5RecordTermination(pid, exitCode, phase, reason) {
|
|
2366
|
+
try {
|
|
2367
|
+
let cause = classifyError(reason);
|
|
2368
|
+
if (exitCode === 124 && cause === 'unknown')
|
|
2369
|
+
cause = 'rpc_timeout';
|
|
2370
|
+
recordFailure({
|
|
2371
|
+
at: Date.now(),
|
|
2372
|
+
phase,
|
|
2373
|
+
cause,
|
|
2374
|
+
rssEstimateBytes: 0,
|
|
2375
|
+
heapUsedBytes: 0,
|
|
2376
|
+
lruBytes: 0,
|
|
2377
|
+
inFlightBytes: 0,
|
|
2378
|
+
lastRpcFrame: getLastRpcFrame(),
|
|
2379
|
+
lastFacetId: getLastFacetId(),
|
|
2380
|
+
exitCode,
|
|
2381
|
+
pid,
|
|
2382
|
+
message: reason,
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
catch (e) {
|
|
2386
|
+
// Fail-soft: telemetry must never break the exit path.
|
|
2387
|
+
console.warn('[facet-manager/W5] recordFailure threw:', e?.message);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
// ── Strategy 1: Real DO Facets (production) ───────────────────────────
|
|
2391
|
+
async _execViaFacets(code, opts, entry, vfsState) {
|
|
2392
|
+
const workerCode = generateFacetCode(code, vfsState);
|
|
2393
|
+
// Scope the LOADER cache key to this DO. Without the doId prefix,
|
|
2394
|
+
// two sessions that happen to execute identical code + vfs key sets
|
|
2395
|
+
// share a warm isolate — and the isolate's baked-in env.SUPERVISOR
|
|
2396
|
+
// stub was minted against whichever DO instantiated the slot FIRST.
|
|
2397
|
+
// Same cross-session-slot-sharing failure mode that b225db3 fixed
|
|
2398
|
+
// for install-time facets in facet-pool.ts; this path wasn't covered.
|
|
2399
|
+
// Cache key includes manifest keys so a re-install that adds packages
|
|
2400
|
+
// mints a fresh isolate (otherwise the warm slot's baked-in manifest
|
|
2401
|
+
// would still hide newly-installed packages from readdirSync).
|
|
2402
|
+
const codeId = `${this.ctx.id.toString()}:${hashCode(code + JSON.stringify(Object.keys(vfsState.bundle)) + JSON.stringify(Object.keys(vfsState.manifest)))}`;
|
|
2403
|
+
// LOADER.get(id, callback) — creates/reuses a warm dynamic worker
|
|
2404
|
+
// Pass SUPERVISOR binding for facet → supervisor RPC
|
|
2405
|
+
const ctxExports = getCtxExports();
|
|
2406
|
+
const supervisorBinding = ctxExports?.SupervisorRPC
|
|
2407
|
+
? ctxExports.SupervisorRPC({ props: { doId: this.ctx.id.toString(), pid: entry.pid } })
|
|
2408
|
+
: undefined;
|
|
2409
|
+
const worker = this.env.LOADER.get(codeId, async () => ({
|
|
2410
|
+
compatibilityDate: CF_COMPAT_DATE,
|
|
2411
|
+
compatibilityFlags: ['nodejs_compat', 'nodejs_compat_v2'],
|
|
2412
|
+
mainModule: 'runner.js',
|
|
2413
|
+
modules: { 'runner.js': workerCode },
|
|
2414
|
+
...(supervisorBinding ? { env: { SUPERVISOR: supervisorBinding } } : {}),
|
|
2415
|
+
}));
|
|
2416
|
+
// Get the DurableObject class from the dynamic worker
|
|
2417
|
+
const NodeProcessClass = worker.getDurableObjectClass('NodeProcess');
|
|
2418
|
+
// ctx.facets.get(name, callback) — creates a child DO with its own SQLite
|
|
2419
|
+
const facetName = `proc-${entry.pid}`;
|
|
2420
|
+
const facet = this.ctx.facets.get(facetName, async () => ({
|
|
2421
|
+
class: NodeProcessClass,
|
|
2422
|
+
}));
|
|
2423
|
+
// RPC: call the facet's run() method (vfsBundle is embedded in the module code)
|
|
2424
|
+
const argsJson = JSON.stringify({
|
|
2425
|
+
argv: opts.argv || [],
|
|
2426
|
+
env: opts.env || {},
|
|
2427
|
+
cwd: opts.cwd || '/home/user',
|
|
2428
|
+
filename: opts.filename || '<eval>',
|
|
2429
|
+
dirname: opts.dirname || '/home/user',
|
|
2430
|
+
});
|
|
2431
|
+
// Clean up the facet (free SQLite storage) regardless of outcome.
|
|
2432
|
+
// AUDIT.md M3 / STABILITY-AUDIT.md M-S2 / TOP-5-NEXT.md #3 (A3):
|
|
2433
|
+
// prior to this, facets.delete ran only on the success branch.
|
|
2434
|
+
// Timeouts (via _execWithTimeout) and user-code errors (throwing
|
|
2435
|
+
// from facet.run) both left the ctx.facets entry and its per-facet
|
|
2436
|
+
// SQLite storage slot orphaned until DO hibernation. Try/finally
|
|
2437
|
+
// ensures cleanup for every exit path.
|
|
2438
|
+
try {
|
|
2439
|
+
const resultJson = await facet.run(argsJson);
|
|
2440
|
+
const result = JSON.parse(resultJson);
|
|
2441
|
+
return result;
|
|
2442
|
+
}
|
|
2443
|
+
finally {
|
|
2444
|
+
try {
|
|
2445
|
+
this.ctx.facets.delete(facetName);
|
|
2446
|
+
}
|
|
2447
|
+
catch { }
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
// ── Strategy 2: LOADER.load() fallback (local dev) ────────────────────
|
|
2451
|
+
async _execViaLoader(code, opts, entry, vfsState) {
|
|
2452
|
+
const workerCode = generateEntrypointCode(code, vfsState);
|
|
2453
|
+
// Pass SUPERVISOR binding for facet → supervisor RPC
|
|
2454
|
+
const ctxExports = getCtxExports();
|
|
2455
|
+
const supervisorBinding = ctxExports?.SupervisorRPC
|
|
2456
|
+
? ctxExports.SupervisorRPC({ props: { doId: this.ctx.id.toString(), pid: entry.pid } })
|
|
2457
|
+
: undefined;
|
|
2458
|
+
const worker = this.env.LOADER.load({
|
|
2459
|
+
compatibilityDate: CF_COMPAT_DATE,
|
|
2460
|
+
compatibilityFlags: ['nodejs_compat', 'nodejs_compat_v2'],
|
|
2461
|
+
mainModule: 'runner.js',
|
|
2462
|
+
modules: { 'runner.js': workerCode },
|
|
2463
|
+
...(supervisorBinding ? { env: { SUPERVISOR: supervisorBinding } } : {}),
|
|
2464
|
+
});
|
|
2465
|
+
const entrypoint = worker.getEntrypoint();
|
|
2466
|
+
const body = JSON.stringify({
|
|
2467
|
+
argv: opts.argv || [],
|
|
2468
|
+
env: opts.env || {},
|
|
2469
|
+
cwd: opts.cwd || '/home/user',
|
|
2470
|
+
filename: opts.filename || '<eval>',
|
|
2471
|
+
dirname: opts.dirname || '/home/user',
|
|
2472
|
+
});
|
|
2473
|
+
const response = await entrypoint.fetch(new Request('http://facet/run', {
|
|
2474
|
+
method: 'POST',
|
|
2475
|
+
body,
|
|
2476
|
+
}));
|
|
2477
|
+
return await response.json();
|
|
2478
|
+
}
|
|
2479
|
+
/** Flush files written by the script back to the supervisor's VFS. */
|
|
2480
|
+
_flushVfsWrites(result) {
|
|
2481
|
+
if (!this.vfs || !result.vfsWrites)
|
|
2482
|
+
return;
|
|
2483
|
+
for (const [path, content] of Object.entries(result.vfsWrites)) {
|
|
2484
|
+
try {
|
|
2485
|
+
const parts = path.split('/');
|
|
2486
|
+
for (let i = 1; i < parts.length; i++) {
|
|
2487
|
+
const dir = parts.slice(0, i).join('/');
|
|
2488
|
+
if (dir && !this.vfs.exists(dir))
|
|
2489
|
+
this.vfs.mkdir(dir, { recursive: true });
|
|
2490
|
+
}
|
|
2491
|
+
// binary-fs wave: __vfsWrites cells carry string | Uint8Array.
|
|
2492
|
+
// The hot path here is the LIVE SUPERVISOR.writeFile RPC inside
|
|
2493
|
+
// the facet — which preserves Uint8Array via structured-clone.
|
|
2494
|
+
// This `result.vfsWrites` carries only the FAILED-writes residue
|
|
2495
|
+
// (after JSON.parse), where Uint8Array gets serialized as a
|
|
2496
|
+
// {"0":...,"1":...} object. Detect that shape and reconstitute
|
|
2497
|
+
// bytes; otherwise pass through (string for source code, etc.).
|
|
2498
|
+
const restored = _reviveVfsWriteCell(content);
|
|
2499
|
+
this.vfs.writeFile(path, restored);
|
|
2500
|
+
}
|
|
2501
|
+
catch (e) {
|
|
2502
|
+
console.error('[nimbus] VFS write-back failed:', path, e?.message);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
/** Execution timeout. */
|
|
2507
|
+
async _execWithTimeout(promise, entry) {
|
|
2508
|
+
let timer;
|
|
2509
|
+
let timedOut = false;
|
|
2510
|
+
const timeout = new Promise((_, reject) => {
|
|
2511
|
+
timer = setTimeout(() => {
|
|
2512
|
+
timedOut = true;
|
|
2513
|
+
// Try to abort the facet
|
|
2514
|
+
try {
|
|
2515
|
+
this.ctx.facets?.abort(`proc-${entry.pid}`, new Error('timeout'));
|
|
2516
|
+
}
|
|
2517
|
+
catch { }
|
|
2518
|
+
// The facet's `reportExit` never runs — notify the session so
|
|
2519
|
+
// the ring buffer gets marked and any dump fires.
|
|
2520
|
+
try {
|
|
2521
|
+
this.hooks.onExternalExit?.(entry.pid, 124, // conventional timeout exit code
|
|
2522
|
+
`timeout after ${FACET_TIMEOUT_MS / 1000}s`);
|
|
2523
|
+
}
|
|
2524
|
+
catch { }
|
|
2525
|
+
reject(new Error(`Process timed out after ${FACET_TIMEOUT_MS / 1000}s`));
|
|
2526
|
+
}, FACET_TIMEOUT_MS);
|
|
2527
|
+
});
|
|
2528
|
+
// Always clear the timer; otherwise a successful run would still
|
|
2529
|
+
// trigger the timeout callback at FACET_TIMEOUT_MS, spuriously
|
|
2530
|
+
// marking the exit code as 124.
|
|
2531
|
+
try {
|
|
2532
|
+
return await Promise.race([promise, timeout]);
|
|
2533
|
+
}
|
|
2534
|
+
finally {
|
|
2535
|
+
clearTimeout(timer);
|
|
2536
|
+
// Expose the flag so callers (exec's outer catch) can avoid
|
|
2537
|
+
// overriding the exit code that onExternalExit already stamped.
|
|
2538
|
+
entry.__timedOut = timedOut;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Run npm install in a dedicated facet.
|
|
2543
|
+
* All writes go through SUPERVISOR.writeFile (live VFS),
|
|
2544
|
+
* progress streams via SUPERVISOR.stdout.
|
|
2545
|
+
*/
|
|
2546
|
+
/**
|
|
2547
|
+
* Spawn a vite dev server facet.
|
|
2548
|
+
* Returns immediately with the facet stub for HTTP routing.
|
|
2549
|
+
*/
|
|
2550
|
+
spawnVite(root, basePath = '/preview') {
|
|
2551
|
+
const code = generateViteFacetCode(root, basePath);
|
|
2552
|
+
return this.spawn(code, 'vite (' + root + ')', root);
|
|
2553
|
+
}
|
|
2554
|
+
/**
|
|
2555
|
+
* Spawn a long-running Node process with the same shimmed require/fs/http
|
|
2556
|
+
* environment used by foreground `node <script>` execution.
|
|
2557
|
+
*/
|
|
2558
|
+
async spawnNode(code, opts = {}) {
|
|
2559
|
+
this.processTable.reap();
|
|
2560
|
+
const command = opts.command || (opts.filename ? `node ${opts.filename}` : 'node <script>');
|
|
2561
|
+
const cwd = opts.cwd || '/home/user';
|
|
2562
|
+
const entry = this.processTable.spawn(command, opts.argv || [], cwd);
|
|
2563
|
+
this.processTable.setLongRunning(entry.pid);
|
|
2564
|
+
try {
|
|
2565
|
+
this.hooks.onSpawn?.(entry.pid, command, true);
|
|
2566
|
+
}
|
|
2567
|
+
catch { }
|
|
2568
|
+
if (this.vfs && !this.esbuild) {
|
|
2569
|
+
try {
|
|
2570
|
+
this.esbuild = new EsbuildService(this.vfs);
|
|
2571
|
+
}
|
|
2572
|
+
catch {
|
|
2573
|
+
this.esbuild = null;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
const vfsState = this.vfs
|
|
2577
|
+
? await buildPrefetchBundle(this.vfs, opts.filename, cwd, code, this.esbuild || undefined)
|
|
2578
|
+
: { bundle: {}, manifest: {}, reachableCount: 0, truncated: false };
|
|
2579
|
+
const workerCode = generateLongRunningNodeCode(code, vfsState, opts);
|
|
2580
|
+
const ctxExports = getCtxExports();
|
|
2581
|
+
const supervisorBinding = ctxExports?.SupervisorRPC
|
|
2582
|
+
? ctxExports.SupervisorRPC({ props: { doId: this.ctx.id.toString(), pid: entry.pid } })
|
|
2583
|
+
: undefined;
|
|
2584
|
+
try {
|
|
2585
|
+
const worker = this.env.LOADER.load({
|
|
2586
|
+
compatibilityDate: CF_COMPAT_DATE,
|
|
2587
|
+
compatibilityFlags: ['nodejs_compat', 'nodejs_compat_v2'],
|
|
2588
|
+
mainModule: 'worker.js',
|
|
2589
|
+
modules: { 'worker.js': workerCode },
|
|
2590
|
+
...(supervisorBinding ? { env: { SUPERVISOR: supervisorBinding } } : {}),
|
|
2591
|
+
});
|
|
2592
|
+
const facetStub = worker.getEntrypoint();
|
|
2593
|
+
const bootResponse = await facetStub.fetch(new Request('http://nimbus.internal/__nimbus_start', {
|
|
2594
|
+
headers: { 'X-Nimbus-Internal-Boot': '1' },
|
|
2595
|
+
}));
|
|
2596
|
+
if (!bootResponse.ok) {
|
|
2597
|
+
const body = await bootResponse.text().catch(() => '');
|
|
2598
|
+
throw new Error(body || `long-running node boot failed with HTTP ${bootResponse.status}`);
|
|
2599
|
+
}
|
|
2600
|
+
if (opts.port && opts.port > 0 && opts.port < 65536) {
|
|
2601
|
+
this.portRegistry.register(opts.port, entry.pid, facetStub);
|
|
2602
|
+
}
|
|
2603
|
+
return { pid: entry.pid, facetStub };
|
|
2604
|
+
}
|
|
2605
|
+
catch (e) {
|
|
2606
|
+
this.processTable.exit(entry.pid, 1);
|
|
2607
|
+
this._w5RecordTermination(entry.pid, 1, 'facet', 'long-running node boot failed: ' + (e?.message || String(e)));
|
|
2608
|
+
try {
|
|
2609
|
+
this.hooks.onExternalExit?.(entry.pid, 1, 'long-running node boot failed: ' + (e?.message || String(e)));
|
|
2610
|
+
}
|
|
2611
|
+
catch { }
|
|
2612
|
+
throw e;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Spawn a long-running facet process.
|
|
2617
|
+
* Returns immediately with the process entry.
|
|
2618
|
+
* The facet stays alive and can handle HTTP requests via its fetch() method.
|
|
2619
|
+
* Used for: vite dev server, node HTTP servers, etc.
|
|
2620
|
+
*
|
|
2621
|
+
* @param workerCode The dynamic worker code (must export a default fetch handler)
|
|
2622
|
+
* @param command Display name for process listing
|
|
2623
|
+
* @returns Process entry with pid and facet stub
|
|
2624
|
+
*/
|
|
2625
|
+
spawn(workerCode, command, cwd, opts = {}) {
|
|
2626
|
+
this.processTable.reap();
|
|
2627
|
+
const entry = this.processTable.spawn(command, [], cwd);
|
|
2628
|
+
// arch-gaps gap #2: stamp the explicit longRunning flag on the
|
|
2629
|
+
// process_table entry so /api/processes returns longRunning=true
|
|
2630
|
+
// independent of the LONG_RUNNING_CMD_RE heuristic. Vite, wrangler,
|
|
2631
|
+
// node servers, --watch, etc. all flow through this primitive.
|
|
2632
|
+
this.processTable.setLongRunning(entry.pid);
|
|
2633
|
+
// Long-running facets (vite, nimbus-wrangler, node servers) always
|
|
2634
|
+
// get a spawn notification — they're visible and users want to know
|
|
2635
|
+
// the PID for later `logs`/`kill`.
|
|
2636
|
+
try {
|
|
2637
|
+
this.hooks.onSpawn?.(entry.pid, command, true);
|
|
2638
|
+
}
|
|
2639
|
+
catch { }
|
|
2640
|
+
const ctxExports = getCtxExports();
|
|
2641
|
+
const supervisorBinding = ctxExports?.SupervisorRPC
|
|
2642
|
+
? ctxExports.SupervisorRPC({ props: { doId: this.ctx.id.toString(), pid: entry.pid } })
|
|
2643
|
+
: undefined;
|
|
2644
|
+
const worker = this.env.LOADER.load({
|
|
2645
|
+
compatibilityDate: CF_COMPAT_DATE,
|
|
2646
|
+
compatibilityFlags: ['nodejs_compat'],
|
|
2647
|
+
mainModule: 'worker.js',
|
|
2648
|
+
modules: { 'worker.js': workerCode },
|
|
2649
|
+
...(supervisorBinding ? { env: { SUPERVISOR: supervisorBinding } } : {}),
|
|
2650
|
+
});
|
|
2651
|
+
const facetStub = worker.getEntrypoint();
|
|
2652
|
+
if (opts.port && opts.port > 0 && opts.port < 65536) {
|
|
2653
|
+
this.portRegistry.register(opts.port, entry.pid, facetStub);
|
|
2654
|
+
}
|
|
2655
|
+
return { pid: entry.pid, facetStub };
|
|
2656
|
+
}
|
|
2657
|
+
/** Kill a running process by PID. */
|
|
2658
|
+
kill(pid) {
|
|
2659
|
+
const entry = this.processTable.get(pid);
|
|
2660
|
+
if (!entry || entry.state !== 'running')
|
|
2661
|
+
return false;
|
|
2662
|
+
try {
|
|
2663
|
+
this.ctx.facets?.abort(`proc-${entry.pid}`, new Error('SIGKILL'));
|
|
2664
|
+
}
|
|
2665
|
+
catch { }
|
|
2666
|
+
try {
|
|
2667
|
+
this.ctx.facets?.delete(`proc-${entry.pid}`);
|
|
2668
|
+
}
|
|
2669
|
+
catch { }
|
|
2670
|
+
this.portRegistry.unregisterByPid(pid);
|
|
2671
|
+
const result = this.processTable.kill(pid);
|
|
2672
|
+
if (result) {
|
|
2673
|
+
try {
|
|
2674
|
+
this.hooks.onExternalExit?.(pid, 137, 'killed');
|
|
2675
|
+
}
|
|
2676
|
+
catch { }
|
|
2677
|
+
}
|
|
2678
|
+
return result;
|
|
2679
|
+
}
|
|
2680
|
+
get stats() { return this.processTable.stats; }
|
|
2681
|
+
}
|