@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,2580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ViteDevServer v2.0 — lightweight Vite-compatible dev server for Nimbus.
|
|
3
|
+
*
|
|
4
|
+
* Not actual Vite (which is 200+ packages). This is a purpose-built dev server
|
|
5
|
+
* that implements the subset of Vite's behavior needed for serving modern
|
|
6
|
+
* web apps: TS/TSX/JSX transform, bare import rewriting, HMR full-reload,
|
|
7
|
+
* path alias resolution, TailwindCSS Play CDN, CSS-as-JS modules.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Browser iframe → /preview/* → DO fetch() → ViteDevServer.handleRequest()
|
|
11
|
+
* ├── / → serves index.html (injects HMR, Tailwind CDN, <base>)
|
|
12
|
+
* ├── /*.ts,*.tsx,*.jsx → esbuild transform → JS with import rewrites + alias resolution
|
|
13
|
+
* ├── /*.css → serve as text/css (with @import inlining, @tailwind stripping, @apply expansion)
|
|
14
|
+
* ├── /*.css?import → wrap CSS in JS that injects <style> tag
|
|
15
|
+
* ├── /@modules/<pkg> → resolve from node_modules, bundle via esbuild facet (synthetic-entry for barrels)
|
|
16
|
+
* ├── /@vite/client → HMR client script
|
|
17
|
+
* ├── /*.json (as module) → export default { ... }
|
|
18
|
+
* ├── /*.svg,*.png,... (as module) → export default "/preview/path/to/asset"
|
|
19
|
+
* └── /* → serve from VFS as-is (static assets)
|
|
20
|
+
*
|
|
21
|
+
* HMR: VFS events → ViteDevServer detects changes → sends {type:'hmr'}
|
|
22
|
+
* messages through the DO WebSocket → frontend dispatches to iframe.
|
|
23
|
+
*/
|
|
24
|
+
import { getSharedRuntimeExternals, BUNDLER_VERSION } from '../runtime/esbuild-service.js';
|
|
25
|
+
import { NpmCache } from '../npm/cache.js';
|
|
26
|
+
import { countPackageFiles, BARREL_PKG_FILE_THRESHOLD, packageNameFromSpecifier } from '../runtime/barrel-detect.js';
|
|
27
|
+
import { scanNamedImports, buildSyntheticEntry, buildScopedSliceForSynthetic, syntheticEntryPath, } from '../runtime/barrel-synthesizer.js';
|
|
28
|
+
import { resolvePackageEntry, resolveExports } from '../_shared/exports-resolver.js';
|
|
29
|
+
import { injectRouterBasename, shouldProcessForRouter } from '../runtime/router-basename.js';
|
|
30
|
+
import { TAILWIND_PLAY_BUNDLE, TAILWIND_PLAY_VERSION, } from '../tailwind-play.generated.js';
|
|
31
|
+
// ── HMR client code ─────────────────────────────────────────────────────
|
|
32
|
+
// Injected into index.html. Listens for HMR messages from parent window
|
|
33
|
+
// (forwarded by the main Nimbus frontend from the WebSocket).
|
|
34
|
+
const HMR_CLIENT = `<script type="module">
|
|
35
|
+
// Nimbus HMR Client — listens for reload messages from parent
|
|
36
|
+
(function() {
|
|
37
|
+
window.__NIMBUS_HMR = true;
|
|
38
|
+
window.addEventListener('message', function(e) {
|
|
39
|
+
if (e.data && e.data.type === 'nimbus-hmr') {
|
|
40
|
+
if (e.data.event === 'full-reload') {
|
|
41
|
+
console.log('[nimbus-hmr] full reload');
|
|
42
|
+
location.reload();
|
|
43
|
+
} else if (e.data.event === 'css-update') {
|
|
44
|
+
// Reload all stylesheets
|
|
45
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach(function(l) {
|
|
46
|
+
var href = l.href;
|
|
47
|
+
l.href = '';
|
|
48
|
+
l.href = href + (href.includes('?') ? '&' : '?') + 't=' + Date.now();
|
|
49
|
+
});
|
|
50
|
+
// Re-fetch injected style tags
|
|
51
|
+
document.querySelectorAll('style[data-vite-dev-id]').forEach(function(s) {
|
|
52
|
+
var id = s.getAttribute('data-vite-dev-id');
|
|
53
|
+
if (id) {
|
|
54
|
+
fetch(id + '?import&t=' + Date.now())
|
|
55
|
+
.then(function(r) { return r.text(); })
|
|
56
|
+
.then(function(js) { try { new Function(js)(); } catch(e) {} });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
console.log('[nimbus-hmr] connected');
|
|
63
|
+
})();
|
|
64
|
+
</script>`;
|
|
65
|
+
/**
|
|
66
|
+
* Runtime-error overlay injected into the served index.html.
|
|
67
|
+
*
|
|
68
|
+
* Catches:
|
|
69
|
+
* - window 'error' events (uncaught synchronous throws + <script type="module"> load failures)
|
|
70
|
+
* - window 'unhandledrejection' events (promise rejections)
|
|
71
|
+
*
|
|
72
|
+
* Renders a red banner at the top of the page with:
|
|
73
|
+
* - the error message
|
|
74
|
+
* - a hint to run `npm install` when the message looks like a module-resolution error
|
|
75
|
+
* - a Reload button
|
|
76
|
+
*
|
|
77
|
+
* Self-contained, classic <script> (runs synchronously before modules load
|
|
78
|
+
* so listeners are attached before user code begins). Uses a stable overlay
|
|
79
|
+
* id so repeated errors replace the previous banner instead of stacking.
|
|
80
|
+
*
|
|
81
|
+
* Cleared automatically on HMR reload because the whole page is re-rendered
|
|
82
|
+
* (HMR_CLIENT calls location.reload() on 'full-reload' events).
|
|
83
|
+
*/
|
|
84
|
+
const ERROR_OVERLAY_CLIENT = `<script>(()=>{const s=(m)=>{let o=document.getElementById('nimbus-error-overlay');if(!o){o=document.createElement('div');o.id='nimbus-error-overlay';o.style.cssText='position:fixed;top:0;left:0;right:0;background:#b91c1c;color:white;padding:16px;font-family:monospace;font-size:13px;z-index:999999;max-height:50vh;overflow:auto;box-shadow:0 4px 12px rgba(0,0,0,.3)';document.body&&document.body.appendChild(o)}o.innerHTML='<strong>Preview crashed</strong><pre style="white-space:pre-wrap;margin:8px 0 0">'+String(m||'').replace(/</g,'<')+'</pre>'+(/does not provide an export|Failed to resolve/.test(m||'')?'<div style="margin-top:8px;font-size:12px">Hint: run <code>npm install</code> in your project.</div>':'')+'<button onclick="location.reload()" style="margin-top:8px;padding:4px 12px;background:white;color:#b91c1c;border:0;border-radius:4px;cursor:pointer">Reload</button>'};window.addEventListener('error',e=>s((e.message||'')+' @ '+String(e.filename||'').replace(location.origin,'')+':'+e.lineno));window.addEventListener('unhandledrejection',e=>s(String(e.reason?.message||e.reason||'(rejection)').substring(0,500)))})();</script>`;
|
|
85
|
+
// ── MIME types ───────────────────────────────────────────────────────────
|
|
86
|
+
const MIME_TYPES = {
|
|
87
|
+
'.html': 'text/html; charset=utf-8',
|
|
88
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
89
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
90
|
+
'.cjs': 'application/javascript; charset=utf-8',
|
|
91
|
+
'.ts': 'application/javascript; charset=utf-8',
|
|
92
|
+
'.tsx': 'application/javascript; charset=utf-8',
|
|
93
|
+
'.jsx': 'application/javascript; charset=utf-8',
|
|
94
|
+
'.mts': 'application/javascript; charset=utf-8',
|
|
95
|
+
'.cts': 'application/javascript; charset=utf-8',
|
|
96
|
+
// SFC formats — served as JS so a future framework plugin can transform them.
|
|
97
|
+
// Without a compiler plugin, the raw source would be served and the browser
|
|
98
|
+
// would fail to parse it — that's a framework integration problem, not a
|
|
99
|
+
// resolver correctness problem. At least the MIME is right.
|
|
100
|
+
'.vue': 'application/javascript; charset=utf-8',
|
|
101
|
+
'.svelte': 'application/javascript; charset=utf-8',
|
|
102
|
+
'.css': 'text/css; charset=utf-8',
|
|
103
|
+
'.json': 'application/json; charset=utf-8',
|
|
104
|
+
'.svg': 'image/svg+xml',
|
|
105
|
+
'.png': 'image/png',
|
|
106
|
+
'.jpg': 'image/jpeg',
|
|
107
|
+
'.jpeg': 'image/jpeg',
|
|
108
|
+
'.gif': 'image/gif',
|
|
109
|
+
'.ico': 'image/x-icon',
|
|
110
|
+
'.webp': 'image/webp',
|
|
111
|
+
'.woff': 'font/woff',
|
|
112
|
+
'.woff2': 'font/woff2',
|
|
113
|
+
'.ttf': 'font/ttf',
|
|
114
|
+
'.otf': 'font/otf',
|
|
115
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
116
|
+
'.md': 'text/plain; charset=utf-8',
|
|
117
|
+
'.wasm': 'application/wasm',
|
|
118
|
+
'.map': 'application/json; charset=utf-8',
|
|
119
|
+
'.mp4': 'video/mp4',
|
|
120
|
+
'.webm': 'video/webm',
|
|
121
|
+
'.mp3': 'audio/mpeg',
|
|
122
|
+
'.pdf': 'application/pdf',
|
|
123
|
+
};
|
|
124
|
+
/** File extensions that should be treated as static assets when imported from JS */
|
|
125
|
+
const ASSET_EXTS = new Set([
|
|
126
|
+
'.svg', '.png', '.jpg', '.jpeg', '.gif', '.ico', '.webp',
|
|
127
|
+
'.woff', '.woff2', '.ttf', '.otf',
|
|
128
|
+
'.mp4', '.webm', '.mp3', '.pdf',
|
|
129
|
+
]);
|
|
130
|
+
// ── CJS external-require → ESM import rewriting ─────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* esbuild, when bundling CJS source with `external` specifiers, leaves the
|
|
133
|
+
* `require("pkg")` calls in the output wrapped in a `__require()` helper that
|
|
134
|
+
* falls back to the global `require`. In the browser there is no global
|
|
135
|
+
* `require`, so every such call throws.
|
|
136
|
+
*
|
|
137
|
+
* The fix: detect all distinct `__require("X")` specifiers in the output,
|
|
138
|
+
* emit a top-level ESM `import * as __ns_X from "X"` for each, and replace
|
|
139
|
+
* every `__require("X")` call with a reference to that namespace (with
|
|
140
|
+
* default-export interop — `__ns_X.default ?? __ns_X`).
|
|
141
|
+
*
|
|
142
|
+
* Before:
|
|
143
|
+
* var __require = ((x) => typeof require !== "undefined" ? require : ...);
|
|
144
|
+
* var React = __require("react");
|
|
145
|
+
*
|
|
146
|
+
* After:
|
|
147
|
+
* import * as __nimbus_ext__react from "react";
|
|
148
|
+
* const __nimbus_req = (id) => {
|
|
149
|
+
* if (id === "react") return __nimbus_ext__react.default ?? __nimbus_ext__react;
|
|
150
|
+
* throw new Error("require: " + id);
|
|
151
|
+
* };
|
|
152
|
+
* var __require = ((x) => typeof require !== "undefined" ? require : ...);
|
|
153
|
+
* var React = __nimbus_req("react");
|
|
154
|
+
*
|
|
155
|
+
* We inject `__nimbus_req` but keep esbuild's `__require` definition so we
|
|
156
|
+
* don't have to rewrite its declaration — we just replace the call-sites.
|
|
157
|
+
*/
|
|
158
|
+
function rewriteExternalRequires(code, basePath) {
|
|
159
|
+
// Find all `__require("specifier")` calls where the specifier is bare
|
|
160
|
+
// (not a relative/absolute path). Bare specifiers are the only ones that
|
|
161
|
+
// can be external.
|
|
162
|
+
const specifiers = new Set();
|
|
163
|
+
for (const m of code.matchAll(/__require\s*\(\s*["']([^"'.][^"']*)["']\s*\)/g)) {
|
|
164
|
+
specifiers.add(m[1]);
|
|
165
|
+
}
|
|
166
|
+
if (specifiers.size === 0)
|
|
167
|
+
return code;
|
|
168
|
+
// Generate namespace variable names — sanitize specifier for use in identifier.
|
|
169
|
+
const specMap = new Map(); // specifier → namespace var name
|
|
170
|
+
for (const spec of specifiers) {
|
|
171
|
+
const safe = spec.replace(/[^A-Za-z0-9_]/g, '_');
|
|
172
|
+
specMap.set(spec, '__nimbus_ext__' + safe);
|
|
173
|
+
}
|
|
174
|
+
// Build the import banner + dispatch function.
|
|
175
|
+
const imports = [];
|
|
176
|
+
const cases = [];
|
|
177
|
+
for (const [spec, ns] of specMap) {
|
|
178
|
+
// Build URL with base prefix (makeModuleUrl handles edge cases)
|
|
179
|
+
const url = makeModuleUrl(basePath, spec);
|
|
180
|
+
imports.push(`import * as ${ns} from ${JSON.stringify(url)};`);
|
|
181
|
+
cases.push(` if (id === ${JSON.stringify(spec)}) return ${ns}.default ?? ${ns};`);
|
|
182
|
+
}
|
|
183
|
+
const banner = imports.join('\n') + '\n' +
|
|
184
|
+
'function __nimbus_req(id) {\n' +
|
|
185
|
+
cases.join('\n') + '\n' +
|
|
186
|
+
' throw new Error("Cannot require external: " + id);\n' +
|
|
187
|
+
'}\n';
|
|
188
|
+
// Replace all `__require("specifier")` call-sites with `__nimbus_req("specifier")`.
|
|
189
|
+
// The replacement preserves the specifier string so the dispatch function
|
|
190
|
+
// looks up the correct namespace.
|
|
191
|
+
const rewritten = code.replace(/__require\s*\(\s*(["'])([^"'.][^"']*)\1\s*\)/g, (_m, q, s) => `__nimbus_req(${q}${s}${q})`);
|
|
192
|
+
return banner + rewritten;
|
|
193
|
+
}
|
|
194
|
+
// ── CJS → ESM named-exports synthesis ───────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* esbuild bundles CJS packages by wrapping them in __commonJS helpers.
|
|
197
|
+
* The resulting ESM bundle only has `export default require_X()` — no named
|
|
198
|
+
* exports. This breaks `import { createRoot } from "react-dom/client"`.
|
|
199
|
+
*
|
|
200
|
+
* We fix this by STATICALLY analyzing the bundled source to find CJS export
|
|
201
|
+
* patterns, then emitting named exports for each found name. We cannot use
|
|
202
|
+
* `new Function()` or `eval()` to get runtime export keys because the
|
|
203
|
+
* Cloudflare Workers runtime disallows string-to-code generation outside of
|
|
204
|
+
* module initialization.
|
|
205
|
+
*
|
|
206
|
+
* Patterns we detect (scanning the entire bundled text, not just the top level):
|
|
207
|
+
* - `exports.NAME = ...`
|
|
208
|
+
* - `exports["NAME"] = ...`
|
|
209
|
+
* - `Object.defineProperty(exports, "NAME", ...)`
|
|
210
|
+
* - `module.exports.NAME = ...`
|
|
211
|
+
* - `module.exports = { NAME, NAME2, ... }` (object literal)
|
|
212
|
+
*
|
|
213
|
+
* Input (esbuild output):
|
|
214
|
+
* var require_X = __commonJS({ "...": function(exports) { exports.jsx = ...; exports.jsxs = ...; } });
|
|
215
|
+
* export default require_X();
|
|
216
|
+
*
|
|
217
|
+
* Output (synthesized):
|
|
218
|
+
* var require_X = __commonJS({...});
|
|
219
|
+
* const __nimbus_ns = require_X();
|
|
220
|
+
* export default __nimbus_ns;
|
|
221
|
+
* export const jsx = __nimbus_ns.jsx;
|
|
222
|
+
* export const jsxs = __nimbus_ns.jsxs;
|
|
223
|
+
*
|
|
224
|
+
* Note: we emit `export const NAME = __nimbus_ns.NAME` per key rather than
|
|
225
|
+
* `export const { NAME, ... } = __nimbus_ns` destructuring. The former
|
|
226
|
+
* preserves live binding semantics slightly better and avoids issues when
|
|
227
|
+
* a key name happens to shadow a keyword or identifier.
|
|
228
|
+
*
|
|
229
|
+
* Returns the original code unchanged if:
|
|
230
|
+
* - The bundle already has named exports (not a CJS-only bundle)
|
|
231
|
+
* - No `export default` pattern found
|
|
232
|
+
* - No CJS export patterns found in the bundle source
|
|
233
|
+
*/
|
|
234
|
+
function synthesizeCjsNamedExports(code) {
|
|
235
|
+
// Quick check: does the bundle already have named exports?
|
|
236
|
+
if (/(^|\n)\s*export\s+(?:const|let|var|function|class|\{|\*)\b/.test(code)) {
|
|
237
|
+
return code;
|
|
238
|
+
}
|
|
239
|
+
// Confirm we have `export default ...` pattern.
|
|
240
|
+
const defaultMatch = code.match(/(^|\n)\s*export\s+default\s+([^;]+);?\s*(?:\/\/[^\n]*)?$/m);
|
|
241
|
+
if (!defaultMatch)
|
|
242
|
+
return code;
|
|
243
|
+
// Extract named exports by scanning the bundled source for CJS patterns.
|
|
244
|
+
const names = extractCjsExportNames(code);
|
|
245
|
+
if (names.length === 0)
|
|
246
|
+
return code;
|
|
247
|
+
// Build the synthesized bundle. Replace `export default X` with:
|
|
248
|
+
// const __nimbus_ns = X;
|
|
249
|
+
// export default __nimbus_ns;
|
|
250
|
+
// export const NAME1 = __nimbus_ns.NAME1;
|
|
251
|
+
// export const NAME2 = __nimbus_ns.NAME2;
|
|
252
|
+
// ...
|
|
253
|
+
const namedExportLines = names
|
|
254
|
+
.map(n => `export const ${n} = __nimbus_ns.${n};`)
|
|
255
|
+
.join('\n');
|
|
256
|
+
const rewritten = code.replace(/(^|\n)\s*export\s+default\s+([^;]+);?\s*$/m, `$1const __nimbus_ns = ($2);\nexport default __nimbus_ns;\n${namedExportLines}\n`);
|
|
257
|
+
return rewritten;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Statically extract named export names from a CJS bundle source. Scans the
|
|
261
|
+
* entire text (not scoped — CJS exports appear throughout __commonJS wrappers)
|
|
262
|
+
* for `exports.X =`, `exports["X"] =`, `Object.defineProperty(exports, "X", ...)`,
|
|
263
|
+
* and `module.exports = { X, Y }` patterns.
|
|
264
|
+
*
|
|
265
|
+
* Returns a deduplicated array of valid ES identifier names, filtered to
|
|
266
|
+
* exclude reserved keywords and `default` (which is already the default export).
|
|
267
|
+
*/
|
|
268
|
+
function extractCjsExportNames(code) {
|
|
269
|
+
const names = new Set();
|
|
270
|
+
// Pattern 1: exports.NAME = ...
|
|
271
|
+
// Matches `exports.createRoot = ...`, `exports . hydrateRoot = ...`
|
|
272
|
+
for (const m of code.matchAll(/(?:^|[^.\w$])exports\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g)) {
|
|
273
|
+
names.add(m[1]);
|
|
274
|
+
}
|
|
275
|
+
// Pattern 2: exports["NAME"] = ... or exports['NAME'] = ...
|
|
276
|
+
for (const m of code.matchAll(/(?:^|[^.\w$])exports\s*\[\s*["']([A-Za-z_$][A-Za-z0-9_$]*)["']\s*\]\s*=/g)) {
|
|
277
|
+
names.add(m[1]);
|
|
278
|
+
}
|
|
279
|
+
// Pattern 3: Object.defineProperty(exports, "NAME", ...)
|
|
280
|
+
for (const m of code.matchAll(/Object\.defineProperty\s*\(\s*exports\s*,\s*["']([A-Za-z_$][A-Za-z0-9_$]*)["']/g)) {
|
|
281
|
+
names.add(m[1]);
|
|
282
|
+
}
|
|
283
|
+
// Pattern 4: module.exports.NAME = ...
|
|
284
|
+
for (const m of code.matchAll(/module\s*\.\s*exports\s*\.\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*=/g)) {
|
|
285
|
+
names.add(m[1]);
|
|
286
|
+
}
|
|
287
|
+
// Pattern 5: module.exports = { NAME, NAME2: value, ... }
|
|
288
|
+
// Find `module.exports = {` then scan the balanced braces for keys.
|
|
289
|
+
const moduleExportsMatch = code.match(/module\s*\.\s*exports\s*=\s*\{/);
|
|
290
|
+
if (moduleExportsMatch) {
|
|
291
|
+
const startIdx = moduleExportsMatch.index + moduleExportsMatch[0].length;
|
|
292
|
+
// Find matching closing brace (simple depth tracking, ignoring strings/comments).
|
|
293
|
+
let depth = 1;
|
|
294
|
+
let i = startIdx;
|
|
295
|
+
while (i < code.length && depth > 0) {
|
|
296
|
+
const ch = code[i];
|
|
297
|
+
if (ch === '{')
|
|
298
|
+
depth++;
|
|
299
|
+
else if (ch === '}')
|
|
300
|
+
depth--;
|
|
301
|
+
else if (ch === '"' || ch === "'" || ch === '`') {
|
|
302
|
+
// Skip string literal
|
|
303
|
+
const quote = ch;
|
|
304
|
+
i++;
|
|
305
|
+
while (i < code.length && code[i] !== quote) {
|
|
306
|
+
if (code[i] === '\\')
|
|
307
|
+
i++;
|
|
308
|
+
i++;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
i++;
|
|
312
|
+
}
|
|
313
|
+
if (depth === 0) {
|
|
314
|
+
const objBody = code.substring(startIdx, i - 1);
|
|
315
|
+
// Extract keys from object literal (identifier before `:` or `,`/`}`).
|
|
316
|
+
for (const m of objBody.matchAll(/(?:^|[,{])\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*(?=[,:}])/g)) {
|
|
317
|
+
names.add(m[1]);
|
|
318
|
+
}
|
|
319
|
+
// Also: quoted keys like "name": value
|
|
320
|
+
for (const m of objBody.matchAll(/(?:^|[,{])\s*["']([A-Za-z_$][A-Za-z0-9_$]*)["']\s*:/g)) {
|
|
321
|
+
names.add(m[1]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Filter: exclude `default` (already the default export), reserved words,
|
|
326
|
+
// and anything that's not a valid ES identifier.
|
|
327
|
+
return Array.from(names).filter(n => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(n) &&
|
|
328
|
+
n !== 'default' &&
|
|
329
|
+
!RESERVED_ES_KEYWORDS.has(n));
|
|
330
|
+
}
|
|
331
|
+
/** ES reserved keywords that can't be used as destructured variable names. */
|
|
332
|
+
const RESERVED_ES_KEYWORDS = new Set([
|
|
333
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
334
|
+
'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
|
|
335
|
+
'finally', 'for', 'function', 'if', 'implements', 'import', 'in',
|
|
336
|
+
'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private',
|
|
337
|
+
'protected', 'public', 'return', 'super', 'switch', 'static', 'this',
|
|
338
|
+
'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
|
|
339
|
+
]);
|
|
340
|
+
// ── Import rewriting ────────────────────────────────────────────────────
|
|
341
|
+
/**
|
|
342
|
+
* Resolve a specifier against path aliases.
|
|
343
|
+
* Uses longest-match semantics (e.g. "@/lib" takes priority over "@").
|
|
344
|
+
* Returns the rewritten path (e.g. "/preview/src/components/Foo") or null.
|
|
345
|
+
*
|
|
346
|
+
* basePath must be the dev server's mount point without trailing slash
|
|
347
|
+
* (e.g. "/preview"). If basePath is "/" or empty, output is origin-rooted.
|
|
348
|
+
*/
|
|
349
|
+
/**
|
|
350
|
+
* Collapse `..` and `.` segments in a VFS path.
|
|
351
|
+
*
|
|
352
|
+
* Essential for entries coming from package.json `main`/`module` that point
|
|
353
|
+
* OUTSIDE their own directory — e.g. react-remove-scroll-bar's nested
|
|
354
|
+
* `constants/package.json` has `"module": "../dist/es2015/constants.js"`,
|
|
355
|
+
* which when naively joined produces `constants/../dist/es2015/constants.js`.
|
|
356
|
+
* The VFS doesn't interpret `..` at lookup time — it treats it as a literal
|
|
357
|
+
* path component that doesn't exist, so resolution fails and we fall through
|
|
358
|
+
* to a bogus CDN wrapper.
|
|
359
|
+
*
|
|
360
|
+
* Leading slashes are preserved. Trailing slashes are stripped.
|
|
361
|
+
* Returns the empty string for a pure-dot path like "." or "./.".
|
|
362
|
+
*/
|
|
363
|
+
function normalizePath(p) {
|
|
364
|
+
const leadingSlash = p.startsWith('/');
|
|
365
|
+
const segments = p.split('/').reduce((acc, seg) => {
|
|
366
|
+
if (seg === '..') {
|
|
367
|
+
// Pop the previous segment if there is one (don't pop past root).
|
|
368
|
+
if (acc.length > 0 && acc[acc.length - 1] !== '..')
|
|
369
|
+
acc.pop();
|
|
370
|
+
else if (!leadingSlash)
|
|
371
|
+
acc.push('..'); // preserve leading .. for relative paths
|
|
372
|
+
}
|
|
373
|
+
else if (seg !== '.' && seg !== '') {
|
|
374
|
+
acc.push(seg);
|
|
375
|
+
}
|
|
376
|
+
return acc;
|
|
377
|
+
}, []);
|
|
378
|
+
return (leadingSlash ? '/' : '') + segments.join('/');
|
|
379
|
+
}
|
|
380
|
+
function resolveAliasSpecifier(specifier, aliases, basePath) {
|
|
381
|
+
// Sort by alias length descending for longest-match-first
|
|
382
|
+
const sorted = Object.entries(aliases).sort((a, b) => b[0].length - a[0].length);
|
|
383
|
+
for (const [alias, target] of sorted) {
|
|
384
|
+
// Match exact alias or alias followed by /
|
|
385
|
+
if (specifier === alias || specifier.startsWith(alias + '/')) {
|
|
386
|
+
const rest = specifier.slice(alias.length); // e.g. "/components/Foo" or ""
|
|
387
|
+
const resolvedTarget = target.replace(/^\.\//, '').replace(/^\//, '');
|
|
388
|
+
// Normalize basePath: strip trailing slash so we always emit exactly one
|
|
389
|
+
// slash between base and target. Handle root ("/" or "") specially.
|
|
390
|
+
const base = basePath === '/' || basePath === '' ? '' : basePath.replace(/\/+$/, '');
|
|
391
|
+
return `${base}/${resolvedTarget}${rest}`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Build an absolute URL for a /@modules/ endpoint, properly prefixed with the
|
|
398
|
+
* dev server's base path so the browser's origin-relative fetch hits the route.
|
|
399
|
+
*
|
|
400
|
+
* Examples:
|
|
401
|
+
* makeModuleUrl('/preview', 'react') → '/preview/@modules/react'
|
|
402
|
+
* makeModuleUrl('/preview/', 'react') → '/preview/@modules/react' (no double slash)
|
|
403
|
+
* makeModuleUrl('/', 'react') → '/@modules/react' (root base)
|
|
404
|
+
* makeModuleUrl('', 'react') → '/@modules/react' (empty/undefined)
|
|
405
|
+
* makeModuleUrl(undefined, 'react') → '/@modules/react' (undefined)
|
|
406
|
+
*
|
|
407
|
+
* This matters because the browser resolves `/@modules/...` against the origin
|
|
408
|
+
* (NOT against <base>), so without the prefix the request misses the
|
|
409
|
+
* `/preview/*` route and 404s.
|
|
410
|
+
*/
|
|
411
|
+
function makeModuleUrl(basePath, specifier) {
|
|
412
|
+
if (!basePath || basePath === '/' || basePath === '') {
|
|
413
|
+
return `/@modules/${specifier}`;
|
|
414
|
+
}
|
|
415
|
+
// Strip trailing slash from basePath so we get exactly one slash before @modules
|
|
416
|
+
const base = basePath.replace(/\/+$/, '');
|
|
417
|
+
return `${base}/@modules/${specifier}`;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Node.js core builtins. Bare imports of these names should be rewritten to
|
|
421
|
+
* `node:<name>` rather than treated as bare npm specifiers — otherwise the
|
|
422
|
+
* browser dev server tries to fetch them from `/preview/@modules/crypto` and
|
|
423
|
+
* 404s. See audit/sections/03-resolver-gaps.md §3.6.
|
|
424
|
+
*/
|
|
425
|
+
const NODE_BUILTINS = new Set([
|
|
426
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'constants',
|
|
427
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'fs/promises', 'http',
|
|
428
|
+
'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'path/posix',
|
|
429
|
+
'path/win32', 'perf_hooks', 'process', 'punycode', 'querystring', 'readline',
|
|
430
|
+
'repl', 'stream', 'stream/consumers', 'stream/promises', 'stream/web',
|
|
431
|
+
'string_decoder', 'sys', 'timers', 'timers/promises', 'tls', 'trace_events',
|
|
432
|
+
'tty', 'url', 'util', 'util/types', 'v8', 'vm', 'wasi', 'worker_threads',
|
|
433
|
+
'zlib',
|
|
434
|
+
]);
|
|
435
|
+
/**
|
|
436
|
+
* Resolve a bare specifier: alias → resolved path, or bare → /<base>/@modules/pkg.
|
|
437
|
+
*
|
|
438
|
+
* `importerCtx` carries the importing-file context so we can resolve
|
|
439
|
+
* Node subpath imports (`#X`) per Node spec — `package.json#imports`
|
|
440
|
+
* is package-LOCAL, looked up via the closest enclosing package.json
|
|
441
|
+
* walking up from the importer's directory. Without it, `#X` survives
|
|
442
|
+
* the rewriter and crashes the browser preview (Markflow on prod
|
|
443
|
+
* 0a488bab: `Uncaught TypeError: Failed to resolve module specifier
|
|
444
|
+
* "#minpath"`).
|
|
445
|
+
*
|
|
446
|
+
* Returns null if specifier should not be rewritten.
|
|
447
|
+
*/
|
|
448
|
+
function resolveBareSpecifier(specifier, aliases, basePath, importerCtx) {
|
|
449
|
+
// Skip already-rewritten or absolute URLs
|
|
450
|
+
if (specifier.startsWith('@modules/'))
|
|
451
|
+
return null;
|
|
452
|
+
if (specifier.startsWith('http://') || specifier.startsWith('https://'))
|
|
453
|
+
return null;
|
|
454
|
+
// Skip protocol-like specifiers: data:, blob:, virtual:, node:, etc.
|
|
455
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(specifier))
|
|
456
|
+
return null;
|
|
457
|
+
// Subpath imports (#X) — Node.js `package.json#imports` field. MUST
|
|
458
|
+
// be resolved against the importing file's owning package.json. If
|
|
459
|
+
// we have an importer context, walk up to resolve; otherwise leave
|
|
460
|
+
// the specifier alone (so the dev-server bundle path or the browser
|
|
461
|
+
// surfaces a meaningful error rather than a misleading /@modules/
|
|
462
|
+
// 404). See exports-resolver.ts for the spec impl.
|
|
463
|
+
if (specifier.startsWith('#')) {
|
|
464
|
+
if (!importerCtx)
|
|
465
|
+
return null;
|
|
466
|
+
const target = resolveHashImportFromImporter(specifier, importerCtx);
|
|
467
|
+
if (!target)
|
|
468
|
+
return null;
|
|
469
|
+
// Convert VFS path to a /preview-relative URL so the browser can
|
|
470
|
+
// load it without going through /@modules/. Strip the project root
|
|
471
|
+
// prefix so the URL is `${basePath}/<rel-from-root>`.
|
|
472
|
+
const rel = target.startsWith(importerCtx.root + '/')
|
|
473
|
+
? target.substring(importerCtx.root.length + 1)
|
|
474
|
+
: target;
|
|
475
|
+
const base = (basePath || '').replace(/\/+$/, '');
|
|
476
|
+
return base + '/' + rel.replace(/^\/+/, '');
|
|
477
|
+
}
|
|
478
|
+
// Bare-builtin handling: a bare `crypto` (no protocol prefix) that names
|
|
479
|
+
// a Node core builtin must NOT be served from /@modules/ (which 404s in
|
|
480
|
+
// the browser — verified Mossaic regression). Returning null here leaves
|
|
481
|
+
// the literal `import 'crypto'` in place — matching the behaviour for
|
|
482
|
+
// already-prefixed `node:crypto` (skipped by the protocol regex above).
|
|
483
|
+
// Both surface a clear "Failed to resolve module 'crypto'" rather than a
|
|
484
|
+
// misleading 404. Browser-side shim wiring is the W7 deliverable per
|
|
485
|
+
// audit/sections/03-resolver-gaps.md §3.6.
|
|
486
|
+
if (NODE_BUILTINS.has(specifier)) {
|
|
487
|
+
return null; // leave the bare specifier untouched
|
|
488
|
+
}
|
|
489
|
+
// Check aliases (works for CSS specifiers too — alias takes priority)
|
|
490
|
+
if (aliases) {
|
|
491
|
+
const resolved = resolveAliasSpecifier(specifier, aliases, basePath || '/preview');
|
|
492
|
+
if (resolved)
|
|
493
|
+
return resolved;
|
|
494
|
+
}
|
|
495
|
+
// Don't rewrite CSS bare imports to /@modules/ (they should be file paths)
|
|
496
|
+
if (specifier.endsWith('.css') || specifier.includes('.css?'))
|
|
497
|
+
return null;
|
|
498
|
+
return makeModuleUrl(basePath, specifier);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Walk up from `importerCtx.importerVfsPath` looking for a
|
|
502
|
+
* `package.json` with an `imports` field that exposes `specifier`.
|
|
503
|
+
* Returns a VFS path to the resolved target, or null if no
|
|
504
|
+
* `package.json#imports` covers it. First package.json wins per Node
|
|
505
|
+
* spec — even if it has no `imports` field, we stop walking (the
|
|
506
|
+
* specifier is unresolved against the importing module's package).
|
|
507
|
+
*
|
|
508
|
+
* Conditions: ESM browser default order
|
|
509
|
+
* (`import` > `module` > `browser` > `default`) — matches what
|
|
510
|
+
* `serveTransformed` is doing (we're serving for the browser).
|
|
511
|
+
*/
|
|
512
|
+
function resolveHashImportFromImporter(specifier, ctx) {
|
|
513
|
+
let dir = ctx.importerVfsPath;
|
|
514
|
+
const lastSlash = dir.lastIndexOf('/');
|
|
515
|
+
if (lastSlash <= 0)
|
|
516
|
+
return null;
|
|
517
|
+
dir = dir.substring(0, lastSlash);
|
|
518
|
+
const visited = new Set();
|
|
519
|
+
while (dir && !visited.has(dir)) {
|
|
520
|
+
visited.add(dir);
|
|
521
|
+
const pkgJsonPath = dir + '/package.json';
|
|
522
|
+
if (ctx.vfs.exists(pkgJsonPath)) {
|
|
523
|
+
try {
|
|
524
|
+
const pkg = JSON.parse(ctx.vfs.readFileString(pkgJsonPath));
|
|
525
|
+
if (pkg && pkg.imports) {
|
|
526
|
+
const target = resolveExports(pkg.imports, specifier);
|
|
527
|
+
if (target) {
|
|
528
|
+
const rel = target.replace(/^\.\//, '');
|
|
529
|
+
const candidate = dir + '/' + rel;
|
|
530
|
+
if (ctx.vfs.exists(candidate))
|
|
531
|
+
return candidate;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch { /* malformed package.json — keep walking is wrong per
|
|
536
|
+
spec; first package.json wins. Fall through to break. */
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
const lastSlashDir = dir.lastIndexOf('/');
|
|
541
|
+
if (lastSlashDir <= 0)
|
|
542
|
+
break;
|
|
543
|
+
dir = dir.substring(0, lastSlashDir);
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Rewrite all bare import/export specifiers in JS code.
|
|
549
|
+
*
|
|
550
|
+
* Handles ALL import forms including multi-line:
|
|
551
|
+
* 1. import "specifier" (side-effect)
|
|
552
|
+
* 2. import defaultExport from "specifier" (default)
|
|
553
|
+
* 3. import { named } from "specifier" (named, possibly multi-line)
|
|
554
|
+
* 4. import * as ns from "specifier" (namespace)
|
|
555
|
+
* 5. export { named } from "specifier" (re-export)
|
|
556
|
+
* 6. export * from "specifier" (re-export all)
|
|
557
|
+
* 7. import("specifier") (dynamic)
|
|
558
|
+
*
|
|
559
|
+
* Strategy: Three separate passes to avoid cross-statement regex issues.
|
|
560
|
+
* Pass 1: CSS side-effect imports (append ?import)
|
|
561
|
+
* Pass 2: Side-effect bare imports (import "specifier";)
|
|
562
|
+
* Pass 3: All "from" clause specifiers (works with multi-line imports)
|
|
563
|
+
* Pass 4: Dynamic imports (import("specifier"))
|
|
564
|
+
*/
|
|
565
|
+
// Valid npm specifier character class. Allows package names (letters, digits,
|
|
566
|
+
// hyphens, underscores, dots), scoped packages (@scope/name), subpaths
|
|
567
|
+
// (slashes), and query strings (?v=123 for HMR invalidation). Crucially,
|
|
568
|
+
// it EXCLUDES spaces, operators (+, *, (, )), and other punctuation that
|
|
569
|
+
// would never appear in a real import specifier. Before this restriction,
|
|
570
|
+
// regexes like `/from "([^"]*)"/` matched inside string literals such as:
|
|
571
|
+
//
|
|
572
|
+
// throw new Error("called from " + fn.name + " here");
|
|
573
|
+
//
|
|
574
|
+
// treating "` + fn.name + `" as a specifier and corrupting the source code.
|
|
575
|
+
// After: only character sequences that look like valid specifiers match.
|
|
576
|
+
//
|
|
577
|
+
// Leading `#` is permitted so Node.js subpath-imports specifiers
|
|
578
|
+
// (`package.json#imports`, e.g. `import x from "#minpath"` from
|
|
579
|
+
// inside vfile/unified) are parsed by the rewriter and routed to the
|
|
580
|
+
// hash-resolver in `resolveBareSpecifier`. Without `#` in the leading
|
|
581
|
+
// class, `import x from "#minpath"` survived the rewriter literally
|
|
582
|
+
// and the browser crashed with `Failed to resolve module specifier
|
|
583
|
+
// "#minpath"` — Markflow regression on prod 0a488bab.
|
|
584
|
+
const SPECIFIER_WITH_QUERY = '[A-Za-z0-9_@#][A-Za-z0-9_@./?=&-]*';
|
|
585
|
+
function rewriteAllImports(code, aliases, basePath, importerCtx) {
|
|
586
|
+
// Pass 1: CSS side-effect imports: import "./foo.css" → import "./foo.css?import"
|
|
587
|
+
// import "@/index.css" → import "/preview/src/index.css?import"
|
|
588
|
+
code = code.replace(/import\s+(["'])([^"']+\.css)\1\s*;/g, (_match, quote, path) => {
|
|
589
|
+
if (aliases && !path.startsWith('.') && !path.startsWith('/')) {
|
|
590
|
+
const resolved = resolveAliasSpecifier(path, aliases, basePath || '/preview');
|
|
591
|
+
if (resolved)
|
|
592
|
+
return `import ${quote}${resolved}?import${quote};`;
|
|
593
|
+
}
|
|
594
|
+
return `import ${quote}${path}?import${quote};`;
|
|
595
|
+
});
|
|
596
|
+
// Pass 2: Side-effect bare imports: import "specifier";
|
|
597
|
+
code = code.replace(new RegExp(`import\\s+(["'])(${SPECIFIER_WITH_QUERY})\\1\\s*;`, 'g'), (match, quote, specifier) => {
|
|
598
|
+
const resolved = resolveBareSpecifier(specifier, aliases, basePath, importerCtx);
|
|
599
|
+
return resolved ? `import ${quote}${resolved}${quote};` : match;
|
|
600
|
+
});
|
|
601
|
+
// Pass 3: All "from" clause specifiers — handles single AND multi-line imports.
|
|
602
|
+
// Matches: import X from "pkg", import { X } from "pkg", export { X } from "pkg",
|
|
603
|
+
// import * as X from "pkg", export * from "pkg" — even multi-line.
|
|
604
|
+
code = code.replace(new RegExp(`(\\bfrom\\s+)(["'])(${SPECIFIER_WITH_QUERY})\\2`, 'g'), (match, fromPart, quote, specifier) => {
|
|
605
|
+
const resolved = resolveBareSpecifier(specifier, aliases, basePath, importerCtx);
|
|
606
|
+
return resolved ? `${fromPart}${quote}${resolved}${quote}` : match;
|
|
607
|
+
});
|
|
608
|
+
// Pass 4: Dynamic imports: import("specifier")
|
|
609
|
+
code = code.replace(new RegExp(`import\\(\\s*(["'])(${SPECIFIER_WITH_QUERY})\\1\\s*\\)`, 'g'), (match, quote, specifier) => {
|
|
610
|
+
const resolved = resolveBareSpecifier(specifier, aliases, basePath, importerCtx);
|
|
611
|
+
return resolved ? `import(${quote}${resolved}${quote})` : match;
|
|
612
|
+
});
|
|
613
|
+
return code;
|
|
614
|
+
}
|
|
615
|
+
// ── Tailwind @apply expander ────────────────────────────────────────────
|
|
616
|
+
/** Responsive breakpoints for Tailwind prefixes */
|
|
617
|
+
const TW_BREAKPOINTS = {
|
|
618
|
+
'sm': '640px', 'md': '768px', 'lg': '1024px', 'xl': '1280px', '2xl': '1536px',
|
|
619
|
+
};
|
|
620
|
+
/** Standard Tailwind utility → CSS property map */
|
|
621
|
+
const TW_UTILITIES = {
|
|
622
|
+
// Display
|
|
623
|
+
'block': 'display: block',
|
|
624
|
+
'inline-block': 'display: inline-block',
|
|
625
|
+
'inline': 'display: inline',
|
|
626
|
+
'flex': 'display: flex',
|
|
627
|
+
'inline-flex': 'display: inline-flex',
|
|
628
|
+
'grid': 'display: grid',
|
|
629
|
+
'hidden': 'display: none',
|
|
630
|
+
'contents': 'display: contents',
|
|
631
|
+
// Position
|
|
632
|
+
'static': 'position: static',
|
|
633
|
+
'fixed': 'position: fixed',
|
|
634
|
+
'absolute': 'position: absolute',
|
|
635
|
+
'relative': 'position: relative',
|
|
636
|
+
'sticky': 'position: sticky',
|
|
637
|
+
// Inset
|
|
638
|
+
'inset-0': 'inset: 0px',
|
|
639
|
+
'top-0': 'top: 0px',
|
|
640
|
+
'right-0': 'right: 0px',
|
|
641
|
+
'bottom-0': 'bottom: 0px',
|
|
642
|
+
'left-0': 'left: 0px',
|
|
643
|
+
// Flex
|
|
644
|
+
'flex-row': 'flex-direction: row',
|
|
645
|
+
'flex-col': 'flex-direction: column',
|
|
646
|
+
'flex-wrap': 'flex-wrap: wrap',
|
|
647
|
+
'flex-nowrap': 'flex-wrap: nowrap',
|
|
648
|
+
'flex-1': 'flex: 1 1 0%',
|
|
649
|
+
'flex-auto': 'flex: 1 1 auto',
|
|
650
|
+
'flex-initial': 'flex: 0 1 auto',
|
|
651
|
+
'flex-none': 'flex: none',
|
|
652
|
+
'flex-grow': 'flex-grow: 1',
|
|
653
|
+
'flex-shrink': 'flex-shrink: 1',
|
|
654
|
+
'grow': 'flex-grow: 1',
|
|
655
|
+
'shrink': 'flex-shrink: 1',
|
|
656
|
+
'shrink-0': 'flex-shrink: 0',
|
|
657
|
+
// Align / Justify
|
|
658
|
+
'items-start': 'align-items: flex-start',
|
|
659
|
+
'items-end': 'align-items: flex-end',
|
|
660
|
+
'items-center': 'align-items: center',
|
|
661
|
+
'items-baseline': 'align-items: baseline',
|
|
662
|
+
'items-stretch': 'align-items: stretch',
|
|
663
|
+
'justify-start': 'justify-content: flex-start',
|
|
664
|
+
'justify-end': 'justify-content: flex-end',
|
|
665
|
+
'justify-center': 'justify-content: center',
|
|
666
|
+
'justify-between': 'justify-content: space-between',
|
|
667
|
+
'justify-around': 'justify-content: space-around',
|
|
668
|
+
'justify-evenly': 'justify-content: space-evenly',
|
|
669
|
+
'self-auto': 'align-self: auto',
|
|
670
|
+
'self-start': 'align-self: flex-start',
|
|
671
|
+
'self-end': 'align-self: flex-end',
|
|
672
|
+
'self-center': 'align-self: center',
|
|
673
|
+
'self-stretch': 'align-self: stretch',
|
|
674
|
+
// Sizing
|
|
675
|
+
'w-full': 'width: 100%',
|
|
676
|
+
'w-screen': 'width: 100vw',
|
|
677
|
+
'w-auto': 'width: auto',
|
|
678
|
+
'w-fit': 'width: fit-content',
|
|
679
|
+
'w-min': 'width: min-content',
|
|
680
|
+
'w-max': 'width: max-content',
|
|
681
|
+
'min-w-0': 'min-width: 0px',
|
|
682
|
+
'min-w-full': 'min-width: 100%',
|
|
683
|
+
'max-w-none': 'max-width: none',
|
|
684
|
+
'max-w-full': 'max-width: 100%',
|
|
685
|
+
'h-full': 'height: 100%',
|
|
686
|
+
'h-screen': 'height: 100vh',
|
|
687
|
+
'h-auto': 'height: auto',
|
|
688
|
+
'h-fit': 'height: fit-content',
|
|
689
|
+
'min-h-0': 'min-height: 0px',
|
|
690
|
+
'min-h-full': 'min-height: 100%',
|
|
691
|
+
'min-h-screen': 'min-height: 100vh',
|
|
692
|
+
// Overflow
|
|
693
|
+
'overflow-auto': 'overflow: auto',
|
|
694
|
+
'overflow-hidden': 'overflow: hidden',
|
|
695
|
+
'overflow-visible': 'overflow: visible',
|
|
696
|
+
'overflow-scroll': 'overflow: scroll',
|
|
697
|
+
'overflow-x-auto': 'overflow-x: auto',
|
|
698
|
+
'overflow-x-hidden': 'overflow-x: hidden',
|
|
699
|
+
'overflow-y-auto': 'overflow-y: auto',
|
|
700
|
+
'overflow-y-hidden': 'overflow-y: hidden',
|
|
701
|
+
// Typography
|
|
702
|
+
'text-left': 'text-align: left',
|
|
703
|
+
'text-center': 'text-align: center',
|
|
704
|
+
'text-right': 'text-align: right',
|
|
705
|
+
'text-justify': 'text-align: justify',
|
|
706
|
+
'text-xs': 'font-size: 0.75rem; line-height: 1rem',
|
|
707
|
+
'text-sm': 'font-size: 0.875rem; line-height: 1.25rem',
|
|
708
|
+
'text-base': 'font-size: 1rem; line-height: 1.5rem',
|
|
709
|
+
'text-lg': 'font-size: 1.125rem; line-height: 1.75rem',
|
|
710
|
+
'text-xl': 'font-size: 1.25rem; line-height: 1.75rem',
|
|
711
|
+
'text-2xl': 'font-size: 1.5rem; line-height: 2rem',
|
|
712
|
+
'text-3xl': 'font-size: 1.875rem; line-height: 2.25rem',
|
|
713
|
+
'text-4xl': 'font-size: 2.25rem; line-height: 2.5rem',
|
|
714
|
+
'text-5xl': 'font-size: 3rem; line-height: 1.1',
|
|
715
|
+
'text-6xl': 'font-size: 3.75rem; line-height: 1.1',
|
|
716
|
+
'text-7xl': 'font-size: 4.5rem; line-height: 1.1',
|
|
717
|
+
'text-8xl': 'font-size: 6rem; line-height: 1',
|
|
718
|
+
'text-9xl': 'font-size: 8rem; line-height: 1',
|
|
719
|
+
'font-thin': 'font-weight: 100',
|
|
720
|
+
'font-extralight': 'font-weight: 200',
|
|
721
|
+
'font-light': 'font-weight: 300',
|
|
722
|
+
'font-normal': 'font-weight: 400',
|
|
723
|
+
'font-medium': 'font-weight: 500',
|
|
724
|
+
'font-semibold': 'font-weight: 600',
|
|
725
|
+
'font-bold': 'font-weight: 700',
|
|
726
|
+
'font-extrabold': 'font-weight: 800',
|
|
727
|
+
'font-black': 'font-weight: 900',
|
|
728
|
+
'italic': 'font-style: italic',
|
|
729
|
+
'not-italic': 'font-style: normal',
|
|
730
|
+
'underline': 'text-decoration-line: underline',
|
|
731
|
+
'overline': 'text-decoration-line: overline',
|
|
732
|
+
'line-through': 'text-decoration-line: line-through',
|
|
733
|
+
'no-underline': 'text-decoration-line: none',
|
|
734
|
+
'uppercase': 'text-transform: uppercase',
|
|
735
|
+
'lowercase': 'text-transform: lowercase',
|
|
736
|
+
'capitalize': 'text-transform: capitalize',
|
|
737
|
+
'normal-case': 'text-transform: none',
|
|
738
|
+
'truncate': 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap',
|
|
739
|
+
'whitespace-normal': 'white-space: normal',
|
|
740
|
+
'whitespace-nowrap': 'white-space: nowrap',
|
|
741
|
+
'whitespace-pre': 'white-space: pre',
|
|
742
|
+
'whitespace-pre-line': 'white-space: pre-line',
|
|
743
|
+
'whitespace-pre-wrap': 'white-space: pre-wrap',
|
|
744
|
+
'break-words': 'overflow-wrap: break-word',
|
|
745
|
+
'break-all': 'word-break: break-all',
|
|
746
|
+
'leading-none': 'line-height: 1',
|
|
747
|
+
'leading-tight': 'line-height: 1.25',
|
|
748
|
+
'leading-snug': 'line-height: 1.375',
|
|
749
|
+
'leading-normal': 'line-height: 1.5',
|
|
750
|
+
'leading-relaxed': 'line-height: 1.625',
|
|
751
|
+
'leading-loose': 'line-height: 2',
|
|
752
|
+
'tracking-tighter': 'letter-spacing: -0.05em',
|
|
753
|
+
'tracking-tight': 'letter-spacing: -0.025em',
|
|
754
|
+
'tracking-normal': 'letter-spacing: 0em',
|
|
755
|
+
'tracking-wide': 'letter-spacing: 0.025em',
|
|
756
|
+
'tracking-wider': 'letter-spacing: 0.05em',
|
|
757
|
+
'tracking-widest': 'letter-spacing: 0.1em',
|
|
758
|
+
'antialiased': '-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale',
|
|
759
|
+
'subpixel-antialiased': '-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto',
|
|
760
|
+
// Font family (common)
|
|
761
|
+
'font-sans': "font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
|
|
762
|
+
'font-serif': "font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif",
|
|
763
|
+
'font-mono': "font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace",
|
|
764
|
+
// Border
|
|
765
|
+
'border': 'border-width: 1px',
|
|
766
|
+
'border-0': 'border-width: 0px',
|
|
767
|
+
'border-2': 'border-width: 2px',
|
|
768
|
+
'border-4': 'border-width: 4px',
|
|
769
|
+
'border-8': 'border-width: 8px',
|
|
770
|
+
'border-t': 'border-top-width: 1px',
|
|
771
|
+
'border-r': 'border-right-width: 1px',
|
|
772
|
+
'border-b': 'border-bottom-width: 1px',
|
|
773
|
+
'border-l': 'border-left-width: 1px',
|
|
774
|
+
'border-l-4': 'border-left-width: 4px',
|
|
775
|
+
'border-solid': 'border-style: solid',
|
|
776
|
+
'border-dashed': 'border-style: dashed',
|
|
777
|
+
'border-dotted': 'border-style: dotted',
|
|
778
|
+
'border-none': 'border-style: none',
|
|
779
|
+
'border-transparent': 'border-color: transparent',
|
|
780
|
+
'border-current': 'border-color: currentColor',
|
|
781
|
+
// Border radius
|
|
782
|
+
'rounded-none': 'border-radius: 0px',
|
|
783
|
+
'rounded': 'border-radius: 0.25rem',
|
|
784
|
+
'rounded-xl': 'border-radius: 0.75rem',
|
|
785
|
+
'rounded-2xl': 'border-radius: 1rem',
|
|
786
|
+
'rounded-3xl': 'border-radius: 1.5rem',
|
|
787
|
+
'rounded-full': 'border-radius: 9999px',
|
|
788
|
+
// Note: rounded-sm, rounded-md, rounded-lg use CSS variables for shadcn/ui compatibility
|
|
789
|
+
// They are resolved in resolveTwUtility() with var(--radius)
|
|
790
|
+
// Background
|
|
791
|
+
'bg-transparent': 'background-color: transparent',
|
|
792
|
+
'bg-current': 'background-color: currentColor',
|
|
793
|
+
'bg-white': 'background-color: #ffffff',
|
|
794
|
+
'bg-black': 'background-color: #000000',
|
|
795
|
+
// Opacity
|
|
796
|
+
'opacity-0': 'opacity: 0',
|
|
797
|
+
'opacity-5': 'opacity: 0.05',
|
|
798
|
+
'opacity-10': 'opacity: 0.1',
|
|
799
|
+
'opacity-25': 'opacity: 0.25',
|
|
800
|
+
'opacity-50': 'opacity: 0.5',
|
|
801
|
+
'opacity-75': 'opacity: 0.75',
|
|
802
|
+
'opacity-100': 'opacity: 1',
|
|
803
|
+
// Shadow
|
|
804
|
+
'shadow-sm': 'box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
805
|
+
'shadow': 'box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
|
806
|
+
'shadow-md': 'box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
807
|
+
'shadow-lg': 'box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
808
|
+
'shadow-xl': 'box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
809
|
+
'shadow-none': 'box-shadow: 0 0 #0000',
|
|
810
|
+
// Transition
|
|
811
|
+
'transition': 'transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
812
|
+
'transition-all': 'transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
813
|
+
'transition-colors': 'transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
814
|
+
'transition-opacity': 'transition-property: opacity; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
815
|
+
'transition-shadow': 'transition-property: box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
816
|
+
'transition-transform': 'transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms',
|
|
817
|
+
'transition-none': 'transition-property: none',
|
|
818
|
+
'ease-linear': 'transition-timing-function: linear',
|
|
819
|
+
'ease-in': 'transition-timing-function: cubic-bezier(0.4, 0, 1, 1)',
|
|
820
|
+
'ease-out': 'transition-timing-function: cubic-bezier(0, 0, 0.2, 1)',
|
|
821
|
+
'ease-in-out': 'transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1)',
|
|
822
|
+
// Transform
|
|
823
|
+
'transform': 'transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))',
|
|
824
|
+
'transform-none': 'transform: none',
|
|
825
|
+
// Cursor
|
|
826
|
+
'cursor-auto': 'cursor: auto',
|
|
827
|
+
'cursor-default': 'cursor: default',
|
|
828
|
+
'cursor-pointer': 'cursor: pointer',
|
|
829
|
+
'cursor-wait': 'cursor: wait',
|
|
830
|
+
'cursor-text': 'cursor: text',
|
|
831
|
+
'cursor-move': 'cursor: move',
|
|
832
|
+
'cursor-not-allowed': 'cursor: not-allowed',
|
|
833
|
+
// Pointer events
|
|
834
|
+
'pointer-events-none': 'pointer-events: none',
|
|
835
|
+
'pointer-events-auto': 'pointer-events: auto',
|
|
836
|
+
// User select
|
|
837
|
+
'select-none': 'user-select: none',
|
|
838
|
+
'select-text': 'user-select: text',
|
|
839
|
+
'select-all': 'user-select: all',
|
|
840
|
+
'select-auto': 'user-select: auto',
|
|
841
|
+
// Z-index
|
|
842
|
+
'z-0': 'z-index: 0',
|
|
843
|
+
'z-10': 'z-index: 10',
|
|
844
|
+
'z-20': 'z-index: 20',
|
|
845
|
+
'z-30': 'z-index: 30',
|
|
846
|
+
'z-40': 'z-index: 40',
|
|
847
|
+
'z-50': 'z-index: 50',
|
|
848
|
+
'z-auto': 'z-index: auto',
|
|
849
|
+
// List style
|
|
850
|
+
'list-none': 'list-style-type: none',
|
|
851
|
+
'list-disc': 'list-style-type: disc',
|
|
852
|
+
'list-decimal': 'list-style-type: decimal',
|
|
853
|
+
'list-inside': 'list-style-position: inside',
|
|
854
|
+
'list-outside': 'list-style-position: outside',
|
|
855
|
+
// Visibility
|
|
856
|
+
'visible': 'visibility: visible',
|
|
857
|
+
'invisible': 'visibility: hidden',
|
|
858
|
+
// Scroll
|
|
859
|
+
'scroll-smooth': 'scroll-behavior: smooth',
|
|
860
|
+
'scroll-auto': 'scroll-behavior: auto',
|
|
861
|
+
// Object fit
|
|
862
|
+
'object-contain': 'object-fit: contain',
|
|
863
|
+
'object-cover': 'object-fit: cover',
|
|
864
|
+
'object-fill': 'object-fit: fill',
|
|
865
|
+
'object-none': 'object-fit: none',
|
|
866
|
+
// Aspect ratio
|
|
867
|
+
'aspect-auto': 'aspect-ratio: auto',
|
|
868
|
+
'aspect-square': 'aspect-ratio: 1 / 1',
|
|
869
|
+
'aspect-video': 'aspect-ratio: 16 / 9',
|
|
870
|
+
// Outline
|
|
871
|
+
'outline-none': 'outline: 2px solid transparent; outline-offset: 2px',
|
|
872
|
+
'outline': 'outline-style: solid',
|
|
873
|
+
// Ring (common)
|
|
874
|
+
'ring-0': 'box-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color)',
|
|
875
|
+
// Appearance
|
|
876
|
+
'appearance-none': 'appearance: none',
|
|
877
|
+
// Resize
|
|
878
|
+
'resize-none': 'resize: none',
|
|
879
|
+
'resize': 'resize: both',
|
|
880
|
+
'resize-x': 'resize: horizontal',
|
|
881
|
+
'resize-y': 'resize: vertical',
|
|
882
|
+
// Text decoration
|
|
883
|
+
'hover\\:underline': 'text-decoration-line: underline',
|
|
884
|
+
};
|
|
885
|
+
/**
|
|
886
|
+
* Resolve a single Tailwind utility class to CSS.
|
|
887
|
+
* Handles standard utilities, spacing (p-4, m-2, gap-4), colors (text-*, bg-*),
|
|
888
|
+
* and custom theme colors (bg-dark-navy → rgb(var(--color-dark-navy))).
|
|
889
|
+
*/
|
|
890
|
+
function resolveTwUtility(cls) {
|
|
891
|
+
// Direct lookup
|
|
892
|
+
if (TW_UTILITIES[cls])
|
|
893
|
+
return TW_UTILITIES[cls];
|
|
894
|
+
// Duration: duration-150, duration-300, etc.
|
|
895
|
+
let m = cls.match(/^duration-(\d+)$/);
|
|
896
|
+
if (m)
|
|
897
|
+
return `transition-duration: ${m[1]}ms`;
|
|
898
|
+
// Delay: delay-150, delay-300, etc.
|
|
899
|
+
m = cls.match(/^delay-(\d+)$/);
|
|
900
|
+
if (m)
|
|
901
|
+
return `transition-delay: ${m[1]}ms`;
|
|
902
|
+
// Spacing: p-{n}, px-{n}, py-{n}, pt-{n}, m-{n}, mx-{n}, my-{n}, mt-{n}, gap-{n}, space-x-{n}
|
|
903
|
+
m = cls.match(/^([pm])([xytblr])?-(\d+(?:\.\d+)?)$/);
|
|
904
|
+
if (m) {
|
|
905
|
+
const prop = m[1] === 'p' ? 'padding' : 'margin';
|
|
906
|
+
const value = `${parseFloat(m[3]) * 0.25}rem`;
|
|
907
|
+
const side = m[2];
|
|
908
|
+
if (!side)
|
|
909
|
+
return `${prop}: ${value}`;
|
|
910
|
+
if (side === 'x')
|
|
911
|
+
return `${prop}-left: ${value}; ${prop}-right: ${value}`;
|
|
912
|
+
if (side === 'y')
|
|
913
|
+
return `${prop}-top: ${value}; ${prop}-bottom: ${value}`;
|
|
914
|
+
const sideMap = { t: 'top', b: 'bottom', l: 'left', r: 'right' };
|
|
915
|
+
return `${prop}-${sideMap[side]}: ${value}`;
|
|
916
|
+
}
|
|
917
|
+
// mx-auto, my-auto
|
|
918
|
+
if (cls === 'mx-auto')
|
|
919
|
+
return 'margin-left: auto; margin-right: auto';
|
|
920
|
+
if (cls === 'my-auto')
|
|
921
|
+
return 'margin-top: auto; margin-bottom: auto';
|
|
922
|
+
if (cls === 'ml-auto')
|
|
923
|
+
return 'margin-left: auto';
|
|
924
|
+
if (cls === 'mr-auto')
|
|
925
|
+
return 'margin-right: auto';
|
|
926
|
+
// Negative spacing: -m-{n}, -mt-{n}, etc.
|
|
927
|
+
m = cls.match(/^-([pm])([xytblr])?-(\d+(?:\.\d+)?)$/);
|
|
928
|
+
if (m) {
|
|
929
|
+
const prop = m[1] === 'p' ? 'padding' : 'margin';
|
|
930
|
+
const value = `-${parseFloat(m[3]) * 0.25}rem`;
|
|
931
|
+
const side = m[2];
|
|
932
|
+
if (!side)
|
|
933
|
+
return `${prop}: ${value}`;
|
|
934
|
+
if (side === 'x')
|
|
935
|
+
return `${prop}-left: ${value}; ${prop}-right: ${value}`;
|
|
936
|
+
if (side === 'y')
|
|
937
|
+
return `${prop}-top: ${value}; ${prop}-bottom: ${value}`;
|
|
938
|
+
const sideMap = { t: 'top', b: 'bottom', l: 'left', r: 'right' };
|
|
939
|
+
return `${prop}-${sideMap[side]}: ${value}`;
|
|
940
|
+
}
|
|
941
|
+
// Gap: gap-{n}
|
|
942
|
+
m = cls.match(/^gap-(\d+(?:\.\d+)?)$/);
|
|
943
|
+
if (m)
|
|
944
|
+
return `gap: ${parseFloat(m[1]) * 0.25}rem`;
|
|
945
|
+
m = cls.match(/^gap-x-(\d+(?:\.\d+)?)$/);
|
|
946
|
+
if (m)
|
|
947
|
+
return `column-gap: ${parseFloat(m[1]) * 0.25}rem`;
|
|
948
|
+
m = cls.match(/^gap-y-(\d+(?:\.\d+)?)$/);
|
|
949
|
+
if (m)
|
|
950
|
+
return `row-gap: ${parseFloat(m[1]) * 0.25}rem`;
|
|
951
|
+
// Width/Height with numbers: w-{n}, h-{n}
|
|
952
|
+
m = cls.match(/^w-(\d+(?:\.\d+)?)$/);
|
|
953
|
+
if (m)
|
|
954
|
+
return `width: ${parseFloat(m[1]) * 0.25}rem`;
|
|
955
|
+
m = cls.match(/^h-(\d+(?:\.\d+)?)$/);
|
|
956
|
+
if (m)
|
|
957
|
+
return `height: ${parseFloat(m[1]) * 0.25}rem`;
|
|
958
|
+
m = cls.match(/^max-w-(\d+)xl$/);
|
|
959
|
+
if (m)
|
|
960
|
+
return `max-width: ${parseInt(m[1]) * 36}rem`;
|
|
961
|
+
if (cls === 'max-w-xs')
|
|
962
|
+
return 'max-width: 20rem';
|
|
963
|
+
if (cls === 'max-w-sm')
|
|
964
|
+
return 'max-width: 24rem';
|
|
965
|
+
if (cls === 'max-w-md')
|
|
966
|
+
return 'max-width: 28rem';
|
|
967
|
+
if (cls === 'max-w-lg')
|
|
968
|
+
return 'max-width: 32rem';
|
|
969
|
+
if (cls === 'max-w-xl')
|
|
970
|
+
return 'max-width: 36rem';
|
|
971
|
+
if (cls === 'max-w-2xl')
|
|
972
|
+
return 'max-width: 42rem';
|
|
973
|
+
if (cls === 'max-w-3xl')
|
|
974
|
+
return 'max-width: 48rem';
|
|
975
|
+
if (cls === 'max-w-4xl')
|
|
976
|
+
return 'max-width: 56rem';
|
|
977
|
+
if (cls === 'max-w-5xl')
|
|
978
|
+
return 'max-width: 64rem';
|
|
979
|
+
if (cls === 'max-w-6xl')
|
|
980
|
+
return 'max-width: 72rem';
|
|
981
|
+
if (cls === 'max-w-7xl')
|
|
982
|
+
return 'max-width: 80rem';
|
|
983
|
+
if (cls === 'max-w-screen-sm')
|
|
984
|
+
return 'max-width: 640px';
|
|
985
|
+
if (cls === 'max-w-screen-md')
|
|
986
|
+
return 'max-width: 768px';
|
|
987
|
+
if (cls === 'max-w-screen-lg')
|
|
988
|
+
return 'max-width: 1024px';
|
|
989
|
+
if (cls === 'max-w-screen-xl')
|
|
990
|
+
return 'max-width: 1280px';
|
|
991
|
+
if (cls === 'max-w-screen-2xl')
|
|
992
|
+
return 'max-width: 1536px';
|
|
993
|
+
if (cls === 'max-w-prose')
|
|
994
|
+
return 'max-width: 65ch';
|
|
995
|
+
// Top/right/bottom/left: top-{n}, inset-x-{n}, etc.
|
|
996
|
+
m = cls.match(/^(top|right|bottom|left)-(\d+(?:\.\d+)?)$/);
|
|
997
|
+
if (m)
|
|
998
|
+
return `${m[1]}: ${parseFloat(m[2]) * 0.25}rem`;
|
|
999
|
+
// Fractional width: w-1/2, w-1/3, etc.
|
|
1000
|
+
m = cls.match(/^w-(\d+)\/(\d+)$/);
|
|
1001
|
+
if (m)
|
|
1002
|
+
return `width: ${(parseInt(m[1]) / parseInt(m[2]) * 100).toFixed(6)}%`;
|
|
1003
|
+
// Line height with numbers
|
|
1004
|
+
m = cls.match(/^leading-(\d+)$/);
|
|
1005
|
+
if (m)
|
|
1006
|
+
return `line-height: ${parseFloat(m[1]) * 0.25}rem`;
|
|
1007
|
+
// Z-index dynamic
|
|
1008
|
+
m = cls.match(/^z-(\d+)$/);
|
|
1009
|
+
if (m)
|
|
1010
|
+
return `z-index: ${m[1]}`;
|
|
1011
|
+
// Min-height with numbers
|
|
1012
|
+
m = cls.match(/^min-h-\[([^\]]+)\]$/);
|
|
1013
|
+
if (m)
|
|
1014
|
+
return `min-height: ${m[1]}`;
|
|
1015
|
+
// Arbitrary values: w-[200px], p-[10px], text-[14px], bg-[#ff0000]
|
|
1016
|
+
m = cls.match(/^([a-z-]+)-\[([^\]]+)\]$/);
|
|
1017
|
+
if (m) {
|
|
1018
|
+
const propMap = {
|
|
1019
|
+
'w': 'width', 'h': 'height', 'p': 'padding', 'm': 'margin',
|
|
1020
|
+
'top': 'top', 'right': 'right', 'bottom': 'bottom', 'left': 'left',
|
|
1021
|
+
'text': 'font-size', 'bg': 'background-color', 'border': 'border-color',
|
|
1022
|
+
'rounded': 'border-radius', 'gap': 'gap', 'max-w': 'max-width',
|
|
1023
|
+
'min-w': 'min-width', 'max-h': 'max-height', 'min-h': 'min-height',
|
|
1024
|
+
'opacity': 'opacity', 'z': 'z-index', 'leading': 'line-height',
|
|
1025
|
+
};
|
|
1026
|
+
if (propMap[m[1]])
|
|
1027
|
+
return `${propMap[m[1]]}: ${m[2]}`;
|
|
1028
|
+
}
|
|
1029
|
+
// Grid columns
|
|
1030
|
+
m = cls.match(/^grid-cols-(\d+)$/);
|
|
1031
|
+
if (m)
|
|
1032
|
+
return `grid-template-columns: repeat(${m[1]}, minmax(0, 1fr))`;
|
|
1033
|
+
m = cls.match(/^col-span-(\d+)$/);
|
|
1034
|
+
if (m)
|
|
1035
|
+
return `grid-column: span ${m[1]} / span ${m[1]}`;
|
|
1036
|
+
// Text colors — theme-aware custom colors (rgb-based CSS vars)
|
|
1037
|
+
// bg-dark-navy, text-slate, text-lightest-slate, border-lightest-navy, etc.
|
|
1038
|
+
const themeColors = [
|
|
1039
|
+
'dark-navy', 'navy', 'light-navy', 'lightest-navy',
|
|
1040
|
+
'slate', 'light-slate', 'lightest-slate',
|
|
1041
|
+
'accent', 'accent-tint', 'green', 'green-tint',
|
|
1042
|
+
];
|
|
1043
|
+
for (const tc of themeColors) {
|
|
1044
|
+
if (cls === `text-${tc}`)
|
|
1045
|
+
return `color: rgb(var(--color-${tc}))`;
|
|
1046
|
+
if (cls === `bg-${tc}`)
|
|
1047
|
+
return `background-color: rgb(var(--color-${tc}))`;
|
|
1048
|
+
if (cls === `border-${tc}`)
|
|
1049
|
+
return `border-color: rgb(var(--color-${tc}))`;
|
|
1050
|
+
}
|
|
1051
|
+
// shadcn/ui HSL-based CSS variable colors
|
|
1052
|
+
const hslColors = {
|
|
1053
|
+
'background': '--background', 'foreground': '--foreground',
|
|
1054
|
+
'primary': '--primary', 'primary-foreground': '--primary-foreground',
|
|
1055
|
+
'secondary': '--secondary', 'secondary-foreground': '--secondary-foreground',
|
|
1056
|
+
'muted': '--muted', 'muted-foreground': '--muted-foreground',
|
|
1057
|
+
'accent': '--accent', 'accent-foreground': '--accent-foreground',
|
|
1058
|
+
'destructive': '--destructive', 'destructive-foreground': '--destructive-foreground',
|
|
1059
|
+
'border': '--border', 'input': '--input', 'ring': '--ring',
|
|
1060
|
+
'card': '--card', 'card-foreground': '--card-foreground',
|
|
1061
|
+
'popover': '--popover', 'popover-foreground': '--popover-foreground',
|
|
1062
|
+
};
|
|
1063
|
+
for (const [name, cssVar] of Object.entries(hslColors)) {
|
|
1064
|
+
if (cls === `text-${name}`)
|
|
1065
|
+
return `color: hsl(var(${cssVar}))`;
|
|
1066
|
+
if (cls === `bg-${name}`)
|
|
1067
|
+
return `background-color: hsl(var(${cssVar}))`;
|
|
1068
|
+
if (cls === `border-${name}`)
|
|
1069
|
+
return `border-color: hsl(var(${cssVar}))`;
|
|
1070
|
+
if (cls === `ring-${name}`)
|
|
1071
|
+
return `--tw-ring-color: hsl(var(${cssVar}))`;
|
|
1072
|
+
}
|
|
1073
|
+
// Standard Tailwind color palette (bg-red-500, text-blue-600, etc.)
|
|
1074
|
+
const twColorPalette = {
|
|
1075
|
+
'white': { DEFAULT: '#ffffff' }, 'black': { DEFAULT: '#000000' },
|
|
1076
|
+
'red': { '50': '#fef2f2', '100': '#fee2e2', '200': '#fecaca', '300': '#fca5a5', '400': '#f87171', '500': '#ef4444', '600': '#dc2626', '700': '#b91c1c', '800': '#991b1b', '900': '#7f1d1d' },
|
|
1077
|
+
'orange': { '50': '#fff7ed', '100': '#ffedd5', '200': '#fed7aa', '300': '#fdba74', '400': '#fb923c', '500': '#f97316', '600': '#ea580c', '700': '#c2410c', '800': '#9a3412', '900': '#7c2d12' },
|
|
1078
|
+
'yellow': { '50': '#fefce8', '100': '#fef9c3', '200': '#fef08a', '300': '#fde047', '400': '#facc15', '500': '#eab308', '600': '#ca8a04', '700': '#a16207', '800': '#854d0e', '900': '#713f12' },
|
|
1079
|
+
'green': { '50': '#f0fdf4', '100': '#dcfce7', '200': '#bbf7d0', '300': '#86efac', '400': '#4ade80', '500': '#22c55e', '600': '#16a34a', '700': '#15803d', '800': '#166534', '900': '#14532d' },
|
|
1080
|
+
'blue': { '50': '#eff6ff', '100': '#dbeafe', '200': '#bfdbfe', '300': '#93c5fd', '400': '#60a5fa', '500': '#3b82f6', '600': '#2563eb', '700': '#1d4ed8', '800': '#1e40af', '900': '#1e3a8a' },
|
|
1081
|
+
'gray': { '50': '#f9fafb', '100': '#f3f4f6', '200': '#e5e7eb', '300': '#d1d5db', '400': '#9ca3af', '500': '#6b7280', '600': '#4b5563', '700': '#374151', '800': '#1f2937', '900': '#111827' },
|
|
1082
|
+
'zinc': { '50': '#fafafa', '100': '#f4f4f5', '200': '#e4e4e7', '300': '#d4d4d8', '400': '#a1a1aa', '500': '#71717a', '600': '#52525b', '700': '#3f3f46', '800': '#27272a', '900': '#18181b' },
|
|
1083
|
+
'neutral': { '50': '#fafafa', '100': '#f5f5f5', '200': '#e5e5e5', '300': '#d4d4d4', '400': '#a3a3a3', '500': '#737373', '600': '#525252', '700': '#404040', '800': '#262626', '900': '#171717' },
|
|
1084
|
+
};
|
|
1085
|
+
m = cls.match(/^(text|bg|border)-([a-z]+)-(\d{2,3})$/);
|
|
1086
|
+
if (m && twColorPalette[m[2]] && twColorPalette[m[2]][m[3]]) {
|
|
1087
|
+
const propMap = { text: 'color', bg: 'background-color', border: 'border-color' };
|
|
1088
|
+
return `${propMap[m[1]]}: ${twColorPalette[m[2]][m[3]]}`;
|
|
1089
|
+
}
|
|
1090
|
+
// Rounded with CSS variable (shadcn pattern): rounded-lg → var(--radius)
|
|
1091
|
+
if (cls === 'rounded-lg')
|
|
1092
|
+
return 'border-radius: var(--radius)';
|
|
1093
|
+
if (cls === 'rounded-md')
|
|
1094
|
+
return 'border-radius: calc(var(--radius) - 2px)';
|
|
1095
|
+
if (cls === 'rounded-sm')
|
|
1096
|
+
return 'border-radius: calc(var(--radius) - 4px)';
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Expand @apply directives in CSS.
|
|
1101
|
+
* Handles responsive prefixes (md:, lg:, etc.) and pseudo-class prefixes (hover:, focus:).
|
|
1102
|
+
*/
|
|
1103
|
+
function expandApplyDirectives(css) {
|
|
1104
|
+
return css.replace(/@apply\s+([^;]+);/g, (_match, classList) => {
|
|
1105
|
+
const classes = classList.trim().split(/\s+/);
|
|
1106
|
+
const baseCss = [];
|
|
1107
|
+
const mediaQueries = {};
|
|
1108
|
+
for (const cls of classes) {
|
|
1109
|
+
// Check for responsive prefix: md:text-3xl, lg:flex, etc.
|
|
1110
|
+
let actualCls = cls;
|
|
1111
|
+
let breakpoint = null;
|
|
1112
|
+
const bpMatch = cls.match(/^(sm|md|lg|xl|2xl):(.+)$/);
|
|
1113
|
+
if (bpMatch) {
|
|
1114
|
+
breakpoint = bpMatch[1];
|
|
1115
|
+
actualCls = bpMatch[2];
|
|
1116
|
+
}
|
|
1117
|
+
// Check for pseudo-class prefix: hover:underline, focus:ring, etc. — skip for now
|
|
1118
|
+
if (actualCls.includes(':')) {
|
|
1119
|
+
baseCss.push(`/* @apply: pseudo "${cls}" skipped */`);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
const resolved = resolveTwUtility(actualCls);
|
|
1123
|
+
if (resolved) {
|
|
1124
|
+
if (breakpoint && TW_BREAKPOINTS[breakpoint]) {
|
|
1125
|
+
if (!mediaQueries[breakpoint])
|
|
1126
|
+
mediaQueries[breakpoint] = [];
|
|
1127
|
+
mediaQueries[breakpoint].push(resolved);
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
baseCss.push(resolved);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
baseCss.push(`/* @apply: unknown "${cls}" */`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
let result = baseCss.map(p => p + ';').join(' ');
|
|
1138
|
+
// Note: media queries inside @apply can't properly be emitted inline inside
|
|
1139
|
+
// a rule. We emit them as comments with the CSS so users can see what's intended.
|
|
1140
|
+
// The Tailwind Play CDN handles responsive classes in HTML class attributes.
|
|
1141
|
+
for (const [bp, props] of Object.entries(mediaQueries)) {
|
|
1142
|
+
result += ` /* @media(min-width:${TW_BREAKPOINTS[bp]}) { ${props.join('; ')} } */`;
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Process CSS for Tailwind: strip @tailwind directives, expand @apply, strip @layer wrappers.
|
|
1149
|
+
*/
|
|
1150
|
+
function processTailwindCss(css) {
|
|
1151
|
+
// Strip @tailwind directives (Play CDN handles them)
|
|
1152
|
+
css = css.replace(/@tailwind\s+(base|components|utilities)\s*;/g, '/* $& — handled by Tailwind Play CDN */');
|
|
1153
|
+
// Expand @apply directives
|
|
1154
|
+
css = expandApplyDirectives(css);
|
|
1155
|
+
// Unwrap @layer blocks — keep the inner CSS
|
|
1156
|
+
css = css.replace(/@layer\s+(base|components|utilities)\s*\{/g, '/* @layer $1 { */');
|
|
1157
|
+
// Fix matching closing braces — this is tricky for nested braces.
|
|
1158
|
+
// Simple approach: just replace the @layer opening, the browser will handle extra }
|
|
1159
|
+
// Actually, let's use a balanced brace approach
|
|
1160
|
+
css = unwrapLayerBlocks(css);
|
|
1161
|
+
return css;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Unwrap @layer blocks: @layer base { ...css... } → ...css...
|
|
1165
|
+
*/
|
|
1166
|
+
function unwrapLayerBlocks(css) {
|
|
1167
|
+
const layerRegex = /@layer\s+(?:base|components|utilities)\s*\{/g;
|
|
1168
|
+
let result = '';
|
|
1169
|
+
let lastIdx = 0;
|
|
1170
|
+
let match;
|
|
1171
|
+
while ((match = layerRegex.exec(css)) !== null) {
|
|
1172
|
+
result += css.slice(lastIdx, match.index);
|
|
1173
|
+
// Find matching closing brace
|
|
1174
|
+
let depth = 1;
|
|
1175
|
+
let i = match.index + match[0].length;
|
|
1176
|
+
while (i < css.length && depth > 0) {
|
|
1177
|
+
if (css[i] === '{')
|
|
1178
|
+
depth++;
|
|
1179
|
+
else if (css[i] === '}')
|
|
1180
|
+
depth--;
|
|
1181
|
+
i++;
|
|
1182
|
+
}
|
|
1183
|
+
// Extract inner content (between { and matching })
|
|
1184
|
+
result += css.slice(match.index + match[0].length, i - 1);
|
|
1185
|
+
lastIdx = i;
|
|
1186
|
+
}
|
|
1187
|
+
result += css.slice(lastIdx);
|
|
1188
|
+
return result;
|
|
1189
|
+
}
|
|
1190
|
+
// ── ViteDevServer ───────────────────────────────────────────────────────
|
|
1191
|
+
export class ViteDevServer {
|
|
1192
|
+
vfs;
|
|
1193
|
+
esbuild;
|
|
1194
|
+
root;
|
|
1195
|
+
onHmrMessage;
|
|
1196
|
+
port;
|
|
1197
|
+
basePath;
|
|
1198
|
+
running = false;
|
|
1199
|
+
moduleCache = new Map();
|
|
1200
|
+
unsubVfs = null;
|
|
1201
|
+
/** True if index.html has an importmap (browser handles bare specifiers) */
|
|
1202
|
+
hasImportmap = false;
|
|
1203
|
+
/** Path aliases from vite.config.ts (e.g. { "@": "./src" }) */
|
|
1204
|
+
aliases;
|
|
1205
|
+
/** Define replacements for esbuild (e.g. { "global": "globalThis" }) */
|
|
1206
|
+
define;
|
|
1207
|
+
/** Whether this project uses TailwindCSS */
|
|
1208
|
+
hasTailwind = false;
|
|
1209
|
+
/** Parsed tailwind config JS (for CDN injection) */
|
|
1210
|
+
tailwindConfigJs = null;
|
|
1211
|
+
/** NPM cache for pre-bundled ESM modules (optional). */
|
|
1212
|
+
npmCache = null;
|
|
1213
|
+
/** Inject React Router basename into entry files? Default: true. */
|
|
1214
|
+
injectBasename;
|
|
1215
|
+
/** Worker env (LOADER, ctx-exports) for the on-demand-bundle facet path.
|
|
1216
|
+
* Null = legacy in-supervisor esbuild fallback. */
|
|
1217
|
+
env;
|
|
1218
|
+
ctx = null;
|
|
1219
|
+
/** Lazily-constructed pool for on-demand bundling. Mirrors the
|
|
1220
|
+
* pre-bundle pool's wasm-modules-map shape. Created on first
|
|
1221
|
+
* cold-path /preview/@modules/<spec> request. */
|
|
1222
|
+
onDemandPool = null;
|
|
1223
|
+
onDemandPoolPromise = null;
|
|
1224
|
+
/**
|
|
1225
|
+
* In-flight on-demand-bundle coalescing map. When the browser fires
|
|
1226
|
+
* multiple parallel requests for the same /preview/@modules/<spec>
|
|
1227
|
+
* (which happens on first preview load — every imported module
|
|
1228
|
+
* resolves concurrently), we want exactly ONE bundle attempt per
|
|
1229
|
+
* spec. The map holds the in-flight Promise<Response> keyed by
|
|
1230
|
+
* cacheKey; subsequent fetches return the same promise. Entry is
|
|
1231
|
+
* deleted when the promise settles so repeat-after-cache-expiry
|
|
1232
|
+
* goes through the cold path again.
|
|
1233
|
+
*
|
|
1234
|
+
* Without coalescing, N parallel requests each build a slice
|
|
1235
|
+
* (~28 MiB) and submit N facet RPCs in parallel. With a shared DO
|
|
1236
|
+
* isolate (Mini-PRD: DO shared isolate issues), the supervisor's
|
|
1237
|
+
* peak heap during page-load = N × slice_size + baseline, which
|
|
1238
|
+
* crashes the supervisor for N≥3 on a busy isolate.
|
|
1239
|
+
*/
|
|
1240
|
+
pendingBundles = new Map();
|
|
1241
|
+
/**
|
|
1242
|
+
* Single-slot semaphore for the on-demand bundle slow path. Serializes
|
|
1243
|
+
* slice-walk + facet-dispatch ACROSS DIFFERENT specs so the
|
|
1244
|
+
* supervisor holds at most ONE 28 MiB slice in memory at any time
|
|
1245
|
+
* during a flurry of /preview/@modules/* requests. Coupled with
|
|
1246
|
+
* pendingBundles (same-spec coalescing) this caps peak supervisor
|
|
1247
|
+
* slice memory at 28 MiB regardless of browser parallelism.
|
|
1248
|
+
*
|
|
1249
|
+
* Implementation: a chain of Promise<void> — each waiter awaits the
|
|
1250
|
+
* previous, runs its critical section, then releases. Latency
|
|
1251
|
+
* impact is bounded by per-spec bundle wall time (typically <1 s
|
|
1252
|
+
* for non-barrel packages); the browser's module-fetch parallelism
|
|
1253
|
+
* just becomes serialized at the bundler boundary, not at the wire.
|
|
1254
|
+
*/
|
|
1255
|
+
onDemandQueue = Promise.resolve();
|
|
1256
|
+
/**
|
|
1257
|
+
* Primitives wave (P8): the supervisor's per-PID log store. When set
|
|
1258
|
+
* (alongside `pid`), every diagnostic emitted by the dev server is
|
|
1259
|
+
* appended here on the 'stderr' stream so the Process tab UI shows
|
|
1260
|
+
* a real, scrolling, trace of bundler activity. When unset, falls
|
|
1261
|
+
* back to console.warn / console.error only (legacy behaviour).
|
|
1262
|
+
*/
|
|
1263
|
+
logPid = null;
|
|
1264
|
+
logStore = null;
|
|
1265
|
+
constructor(opts) {
|
|
1266
|
+
this.vfs = opts.vfs;
|
|
1267
|
+
this.esbuild = opts.esbuild;
|
|
1268
|
+
this.injectBasename = opts.injectBasename !== false;
|
|
1269
|
+
this.logPid = (opts.pid != null) ? opts.pid : null;
|
|
1270
|
+
this.logStore = opts.processLogs || null;
|
|
1271
|
+
// Normalize root: resolve ./, collapse //, strip leading/trailing slashes
|
|
1272
|
+
this.root = opts.root
|
|
1273
|
+
.replace(/\/\.\//g, '/') // /./ → /
|
|
1274
|
+
.replace(/\/\.$/, '') // trailing /.
|
|
1275
|
+
.replace(/\/+/g, '/') // collapse //
|
|
1276
|
+
.replace(/^\/+/, '') // leading /
|
|
1277
|
+
.replace(/\/+$/, ''); // trailing /
|
|
1278
|
+
this.onHmrMessage = opts.onHmrMessage;
|
|
1279
|
+
this.port = opts.port || 5173;
|
|
1280
|
+
this.basePath = (opts.basePath || '/preview').replace(/\/+$/, '');
|
|
1281
|
+
this.aliases = opts.aliases || {};
|
|
1282
|
+
this.env = opts.env;
|
|
1283
|
+
this.ctx = opts.ctx ?? null;
|
|
1284
|
+
if (opts.sql) {
|
|
1285
|
+
this.npmCache = new NpmCache(opts.sql);
|
|
1286
|
+
}
|
|
1287
|
+
// Merge user defines with standard Vite defines.
|
|
1288
|
+
// process.env.NODE_ENV is set explicitly so bundled CJS packages (like React's
|
|
1289
|
+
// `if (process.env.NODE_ENV !== "production")` guards) get the correct branch.
|
|
1290
|
+
// Without this, many packages emit warnings or fail when evaluated in the browser.
|
|
1291
|
+
// BASE_URL reflects the actual URL prefix we're mounted at. With
|
|
1292
|
+
// per-session routing this becomes e.g. `/s/nimble-otter-4271/preview/`,
|
|
1293
|
+
// which user code that does `new URL(path, import.meta.env.BASE_URL)`
|
|
1294
|
+
// or `<img src={import.meta.env.BASE_URL + 'logo.png'}>` needs to work.
|
|
1295
|
+
const baseUrlValue = JSON.stringify((this.basePath === '' || this.basePath === '/') ? '/' : this.basePath + '/');
|
|
1296
|
+
this.define = {
|
|
1297
|
+
'import.meta.env.DEV': 'true',
|
|
1298
|
+
'import.meta.env.PROD': 'false',
|
|
1299
|
+
'import.meta.env.MODE': '"development"',
|
|
1300
|
+
'import.meta.env.SSR': 'false',
|
|
1301
|
+
'import.meta.env.BASE_URL': baseUrlValue,
|
|
1302
|
+
'process.env.NODE_ENV': '"development"',
|
|
1303
|
+
'global': 'globalThis',
|
|
1304
|
+
...(opts.define || {}),
|
|
1305
|
+
};
|
|
1306
|
+
this.detectTailwind();
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Lazily construct the NimbusLoaderPool used for on-demand bundling
|
|
1310
|
+
* of /preview/@modules/<spec> requests that miss both the in-memory
|
|
1311
|
+
* and pkg_esm_bundles caches. Mirrors the pre-bundle pool's
|
|
1312
|
+
* configuration: 1 worker, internal pLimit not needed (one bundle
|
|
1313
|
+
* per request), wasm shipped via wasmModules.
|
|
1314
|
+
*
|
|
1315
|
+
* Returns null when env/ctx aren't available (legacy fallback used).
|
|
1316
|
+
*/
|
|
1317
|
+
async ensureOnDemandPool() {
|
|
1318
|
+
if (this.onDemandPool)
|
|
1319
|
+
return this.onDemandPool;
|
|
1320
|
+
if (this.onDemandPoolPromise)
|
|
1321
|
+
return this.onDemandPoolPromise;
|
|
1322
|
+
if (!this.env || !this.ctx)
|
|
1323
|
+
return null;
|
|
1324
|
+
this.onDemandPoolPromise = (async () => {
|
|
1325
|
+
const { NimbusLoaderPool } = await import('../loaders/loader-pool.js');
|
|
1326
|
+
const { PRE_BUNDLE_PREAMBLE } = await import('../loaders/pre-bundle-preamble.js');
|
|
1327
|
+
const { fetchEsbuildWasmBytes } = await import('../runtime/esbuild-wasm-bytes.js');
|
|
1328
|
+
const wasmBytes = await fetchEsbuildWasmBytes(this.env);
|
|
1329
|
+
const pool = new NimbusLoaderPool(this.env, this.ctx, {
|
|
1330
|
+
concurrency: 1,
|
|
1331
|
+
timeoutMs: 60_000,
|
|
1332
|
+
retries: 0,
|
|
1333
|
+
// Use a distinct tag so the on-demand pool's cached worker
|
|
1334
|
+
// doesn't collide with the install-time pre-bundle pool.
|
|
1335
|
+
// Sharing fnHash + preamble + wasm fingerprint between pools
|
|
1336
|
+
// would otherwise alias them in workerd's loader cache.
|
|
1337
|
+
tag: 'on-demand-bundle',
|
|
1338
|
+
preamble: PRE_BUNDLE_PREAMBLE,
|
|
1339
|
+
wasmModules: { 'esbuild.wasm': wasmBytes },
|
|
1340
|
+
});
|
|
1341
|
+
this.onDemandPool = pool;
|
|
1342
|
+
return pool;
|
|
1343
|
+
})();
|
|
1344
|
+
try {
|
|
1345
|
+
return await this.onDemandPoolPromise;
|
|
1346
|
+
}
|
|
1347
|
+
catch (e) {
|
|
1348
|
+
this.onDemandPoolPromise = null;
|
|
1349
|
+
throw e;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/** Detect TailwindCSS usage in the project */
|
|
1353
|
+
detectTailwind() {
|
|
1354
|
+
// Check for tailwind config files
|
|
1355
|
+
const configFiles = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs', 'tailwind.config.mjs'];
|
|
1356
|
+
for (const cfg of configFiles) {
|
|
1357
|
+
const cfgPath = this.root + '/' + cfg;
|
|
1358
|
+
if (this.vfs.exists(cfgPath)) {
|
|
1359
|
+
this.hasTailwind = true;
|
|
1360
|
+
try {
|
|
1361
|
+
let code = this.vfs.readFileString(cfgPath);
|
|
1362
|
+
// Strip require() calls and module.exports, convert to plain object
|
|
1363
|
+
code = code.replace(/require\s*\(\s*["'][^"']*["']\s*\)/g, '[]');
|
|
1364
|
+
code = code.replace(/import\s+.*?from\s+["'][^"']*["']\s*;?/g, '');
|
|
1365
|
+
// Extract the config object — look for export default or module.exports
|
|
1366
|
+
const exportMatch = code.match(/(?:export\s+default|module\.exports\s*=)\s*(\{[\s\S]*\})\s*;?\s*$/);
|
|
1367
|
+
if (exportMatch) {
|
|
1368
|
+
this.tailwindConfigJs = exportMatch[1];
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
catch { }
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// Also check package.json for tailwindcss dependency
|
|
1376
|
+
if (!this.hasTailwind) {
|
|
1377
|
+
try {
|
|
1378
|
+
const pkgPath = this.root + '/package.json';
|
|
1379
|
+
if (this.vfs.exists(pkgPath)) {
|
|
1380
|
+
const pkg = JSON.parse(this.vfs.readFileString(pkgPath));
|
|
1381
|
+
if (pkg.dependencies?.tailwindcss || pkg.devDependencies?.tailwindcss) {
|
|
1382
|
+
this.hasTailwind = true;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
catch { }
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Rewrite absolute paths in HTML so they resolve under the basePath.
|
|
1391
|
+
*/
|
|
1392
|
+
rewriteHtmlPaths(html) {
|
|
1393
|
+
if (!this.basePath || this.basePath === '/')
|
|
1394
|
+
return html;
|
|
1395
|
+
const base = this.basePath;
|
|
1396
|
+
html = html.replace(/(\s(?:src|href|action)=)(["'])(\/(?!\/)[^"']*)\2/gi, (match, attr, quote, path) => {
|
|
1397
|
+
if (path.startsWith(base + '/') || path === base)
|
|
1398
|
+
return match;
|
|
1399
|
+
return `${attr}${quote}${base}${path}${quote}`;
|
|
1400
|
+
});
|
|
1401
|
+
return html;
|
|
1402
|
+
}
|
|
1403
|
+
/** Start the dev server (subscribe to VFS events for HMR). */
|
|
1404
|
+
start() {
|
|
1405
|
+
if (this.running)
|
|
1406
|
+
return;
|
|
1407
|
+
this.running = true;
|
|
1408
|
+
this.moduleCache.clear();
|
|
1409
|
+
// Subscribe to VFS events for HMR
|
|
1410
|
+
this.unsubVfs = this.vfs.events.on((events) => {
|
|
1411
|
+
this.handleVfsEvents(events);
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
/** Stop the dev server. */
|
|
1415
|
+
stop() {
|
|
1416
|
+
this.running = false;
|
|
1417
|
+
if (this.unsubVfs) {
|
|
1418
|
+
this.unsubVfs();
|
|
1419
|
+
this.unsubVfs = null;
|
|
1420
|
+
}
|
|
1421
|
+
this.moduleCache.clear();
|
|
1422
|
+
}
|
|
1423
|
+
get isRunning() { return this.running; }
|
|
1424
|
+
/**
|
|
1425
|
+
* Primitives wave (P8): single chokepoint for dev-server diagnostics.
|
|
1426
|
+
* Always writes to the workerd console (so wrangler tail / Worker
|
|
1427
|
+
* logs see it for ops triage) AND appends to the supervisor's
|
|
1428
|
+
* per-PID ProcessLogStore when one is wired (so the Process tab in
|
|
1429
|
+
* the browser shows the same line, on the same stderr stream, with
|
|
1430
|
+
* the same timestamp ordering). Callers no longer have to remember
|
|
1431
|
+
* to do both.
|
|
1432
|
+
*
|
|
1433
|
+
* Levels:
|
|
1434
|
+
* - 'info': stdout stream of the Process tab; NOT echoed to the
|
|
1435
|
+
* workerd console (would spam wrangler tail). Used for normal
|
|
1436
|
+
* activity — request served, module bundled, HMR fired.
|
|
1437
|
+
* Without this level the Process tab was silent past the
|
|
1438
|
+
* synchronous `vite` builtin banner: the only call sites for
|
|
1439
|
+
* log() were error / warn from cold-bundle failures, so on a
|
|
1440
|
+
* clean Markflow run NOTHING reached subscribers and the tab
|
|
1441
|
+
* froze on the banner content. Markflow regression on prod
|
|
1442
|
+
* 0a488bab.
|
|
1443
|
+
* - 'warn' / 'error': stderr stream + console.warn/error. Used
|
|
1444
|
+
* for cold-path bundle failures, synthetic-entry errors, and
|
|
1445
|
+
* other diagnostics worth surfacing on the workerd console
|
|
1446
|
+
* for ops triage.
|
|
1447
|
+
*
|
|
1448
|
+
* Trailing newline is added if missing so the log buffer is line-
|
|
1449
|
+
* oriented (the Process-tab UI splits on `\n`).
|
|
1450
|
+
*/
|
|
1451
|
+
log(level, msg) {
|
|
1452
|
+
if (level !== 'info') {
|
|
1453
|
+
const fn = level === 'error' ? console.error : console.warn;
|
|
1454
|
+
try {
|
|
1455
|
+
fn(msg);
|
|
1456
|
+
}
|
|
1457
|
+
catch { }
|
|
1458
|
+
}
|
|
1459
|
+
if (this.logPid != null && this.logStore) {
|
|
1460
|
+
const text = msg.endsWith('\n') ? msg : msg + '\n';
|
|
1461
|
+
const stream = level === 'info' ? 'stdout' : 'stderr';
|
|
1462
|
+
try {
|
|
1463
|
+
this.logStore.append(this.logPid, stream, text);
|
|
1464
|
+
}
|
|
1465
|
+
catch { }
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/** Handle VFS change events → trigger HMR. */
|
|
1469
|
+
handleVfsEvents(events) {
|
|
1470
|
+
if (!this.running)
|
|
1471
|
+
return;
|
|
1472
|
+
let needsReload = false;
|
|
1473
|
+
let cssOnly = true;
|
|
1474
|
+
let nodeModulesChanged = false;
|
|
1475
|
+
for (const event of events) {
|
|
1476
|
+
if (event.type === 'addDir' || event.type === 'unlinkDir')
|
|
1477
|
+
continue;
|
|
1478
|
+
const path = event.path;
|
|
1479
|
+
this.moduleCache.delete(path);
|
|
1480
|
+
this.moduleCache.delete(this.root + '/' + path);
|
|
1481
|
+
if (path.startsWith(this.root + '/')) {
|
|
1482
|
+
this.moduleCache.delete(path.substring(this.root.length + 1));
|
|
1483
|
+
}
|
|
1484
|
+
if (path.includes('node_modules/')) {
|
|
1485
|
+
nodeModulesChanged = true;
|
|
1486
|
+
}
|
|
1487
|
+
if (path.startsWith(this.root + '/') || path.startsWith(this.root)) {
|
|
1488
|
+
needsReload = true;
|
|
1489
|
+
if (!path.endsWith('.css'))
|
|
1490
|
+
cssOnly = false;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (nodeModulesChanged) {
|
|
1494
|
+
const toDelete = [];
|
|
1495
|
+
for (const key of this.moduleCache.keys()) {
|
|
1496
|
+
if (key.startsWith('@modules/'))
|
|
1497
|
+
toDelete.push(key);
|
|
1498
|
+
}
|
|
1499
|
+
for (const k of toDelete)
|
|
1500
|
+
this.moduleCache.delete(k);
|
|
1501
|
+
}
|
|
1502
|
+
if (needsReload) {
|
|
1503
|
+
if (cssOnly) {
|
|
1504
|
+
this.onHmrMessage({ type: 'nimbus-hmr', event: 'css-update' });
|
|
1505
|
+
this.log('info', `[hmr] css-update (${events.length} event${events.length === 1 ? '' : 's'})`);
|
|
1506
|
+
}
|
|
1507
|
+
else {
|
|
1508
|
+
this.onHmrMessage({ type: 'nimbus-hmr', event: 'full-reload' });
|
|
1509
|
+
this.log('info', `[hmr] full-reload (${events.length} event${events.length === 1 ? '' : 's'})`);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
/** Normalize and sanitize a preview pathname to prevent traversal. */
|
|
1514
|
+
sanitizePath(pathname) {
|
|
1515
|
+
const segments = pathname.split('/').reduce((acc, seg) => {
|
|
1516
|
+
if (seg === '..')
|
|
1517
|
+
acc.pop();
|
|
1518
|
+
else if (seg !== '.' && seg !== '')
|
|
1519
|
+
acc.push(seg);
|
|
1520
|
+
return acc;
|
|
1521
|
+
}, []);
|
|
1522
|
+
return '/' + segments.join('/');
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Handle an HTTP request to the dev server.
|
|
1526
|
+
* Called from the DO's fetch() handler for /preview/* paths.
|
|
1527
|
+
*
|
|
1528
|
+
* Wraps `_handleRequestInner` so EVERY served request appears in
|
|
1529
|
+
* the Process tab's stdout stream (status + path + elapsed). Without
|
|
1530
|
+
* this, the tab was silent past the synchronous banner — the dev
|
|
1531
|
+
* server happily processed requests, but no per-request signal
|
|
1532
|
+
* reached subscribers and the user saw a frozen tab. Markflow
|
|
1533
|
+
* regression on prod 0a488bab.
|
|
1534
|
+
*/
|
|
1535
|
+
async handleRequest(request, pathname) {
|
|
1536
|
+
const t0 = Date.now();
|
|
1537
|
+
const resp = await this._handleRequestInner(request, pathname);
|
|
1538
|
+
const elapsed = Date.now() - t0;
|
|
1539
|
+
// Strip query for the log line — keeps it scannable. The original
|
|
1540
|
+
// pathname (with query) is what was served; we trim purely for
|
|
1541
|
+
// display.
|
|
1542
|
+
const qIdx = pathname.indexOf('?');
|
|
1543
|
+
const displayPath = qIdx >= 0 ? pathname.substring(0, qIdx) : pathname;
|
|
1544
|
+
this.log('info', `${resp.status} ${displayPath} (${elapsed}ms)`);
|
|
1545
|
+
return resp;
|
|
1546
|
+
}
|
|
1547
|
+
async _handleRequestInner(request, pathname) {
|
|
1548
|
+
const safePath = this.sanitizePath(pathname);
|
|
1549
|
+
if (!safePath) {
|
|
1550
|
+
return new Response('400 Bad Request', { status: 400 });
|
|
1551
|
+
}
|
|
1552
|
+
pathname = safePath;
|
|
1553
|
+
// Strip query parameters for path resolution, keep for logic
|
|
1554
|
+
const queryIdx = pathname.indexOf('?');
|
|
1555
|
+
const query = queryIdx >= 0 ? pathname.substring(queryIdx) : '';
|
|
1556
|
+
if (queryIdx >= 0)
|
|
1557
|
+
pathname = pathname.substring(0, queryIdx);
|
|
1558
|
+
const headers = {
|
|
1559
|
+
'Access-Control-Allow-Origin': '*',
|
|
1560
|
+
'Cache-Control': 'no-store',
|
|
1561
|
+
};
|
|
1562
|
+
try {
|
|
1563
|
+
// /@vite/client → HMR client script
|
|
1564
|
+
if (pathname === '/@vite/client' || pathname === '/@vite/client.js') {
|
|
1565
|
+
return new Response('// Nimbus HMR — connected via postMessage\nconsole.log("[nimbus-hmr] client loaded");\n', {
|
|
1566
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
// /__nimbus_assets/tailwind-play.js → vendored Tailwind Play CDN
|
|
1570
|
+
// Served INSTEAD of cdn.tailwindcss.com so preview pages stay
|
|
1571
|
+
// 100% on edge (no third-party CDN fetch from the browser).
|
|
1572
|
+
// Bundle is embedded at build time via scripts/bundle-tailwind-play.mjs;
|
|
1573
|
+
// see src/tailwind-play.generated.ts for the source URL + integrity.
|
|
1574
|
+
if (pathname === '/__nimbus_assets/tailwind-play.js') {
|
|
1575
|
+
return new Response(TAILWIND_PLAY_BUNDLE, {
|
|
1576
|
+
headers: {
|
|
1577
|
+
...headers,
|
|
1578
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
1579
|
+
// Immutable: the bundle is version-pinned at build time.
|
|
1580
|
+
// Override the default no-store so browsers cache it
|
|
1581
|
+
// across preview reloads.
|
|
1582
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
1583
|
+
'X-Tailwind-Play-Version': TAILWIND_PLAY_VERSION,
|
|
1584
|
+
},
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
// /@modules/<pkg> → resolve from node_modules, transform
|
|
1588
|
+
if (pathname.startsWith('/@modules/')) {
|
|
1589
|
+
const specifier = pathname.substring('/@modules/'.length);
|
|
1590
|
+
// Check if this is actually an alias that got misrouted
|
|
1591
|
+
if (this.aliases) {
|
|
1592
|
+
const resolved = resolveAliasSpecifier(specifier, this.aliases, this.basePath);
|
|
1593
|
+
if (resolved) {
|
|
1594
|
+
// Redirect to the correct path
|
|
1595
|
+
return new Response(null, { status: 302, headers: { ...headers, 'Location': resolved } });
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return this.serveModule(specifier, headers);
|
|
1599
|
+
}
|
|
1600
|
+
// / → serve index.html
|
|
1601
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
1602
|
+
return this.serveIndexHtml(headers);
|
|
1603
|
+
}
|
|
1604
|
+
// public/ directory — serve static assets as-is
|
|
1605
|
+
const publicPath = this.root + '/public' + pathname;
|
|
1606
|
+
if (this.vfs.exists(publicPath) && !this.vfs.isDirectory(publicPath)) {
|
|
1607
|
+
const ext = pathname.includes('.') ? '.' + pathname.split('.').pop().toLowerCase() : '';
|
|
1608
|
+
const data = this.vfs.readFile(publicPath);
|
|
1609
|
+
return new Response(data, {
|
|
1610
|
+
headers: { ...headers, 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' },
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
// Serve from VFS (with transforms for TS/TSX/JSX)
|
|
1614
|
+
return this.serveFile(request, pathname, query, headers);
|
|
1615
|
+
}
|
|
1616
|
+
catch (e) {
|
|
1617
|
+
return new Response(`500 Internal Server Error: ${e?.message}`, {
|
|
1618
|
+
status: 500, headers: { ...headers, 'Content-Type': 'text/plain' },
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// ── index.html ────────────────────────────────────────────────────────
|
|
1623
|
+
serveIndexHtml(headers) {
|
|
1624
|
+
const htmlPath = this.root + '/index.html';
|
|
1625
|
+
if (!this.vfs.exists(htmlPath)) {
|
|
1626
|
+
return new Response('<!DOCTYPE html><html><body><h1>No index.html found</h1><p>Create index.html in your project root.</p></body></html>', {
|
|
1627
|
+
headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8' },
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
let html = this.vfs.readFileString(htmlPath);
|
|
1631
|
+
// Detect importmap
|
|
1632
|
+
this.hasImportmap = html.includes('"importmap"') || html.includes("'importmap'");
|
|
1633
|
+
// Build head injections
|
|
1634
|
+
let headInjections = '';
|
|
1635
|
+
// 1. <base> tag for SPA router support
|
|
1636
|
+
if (this.basePath && this.basePath !== '/') {
|
|
1637
|
+
headInjections += `<base href="${this.basePath}/">\n`;
|
|
1638
|
+
}
|
|
1639
|
+
// 2. Tailwind: serve the vendored Play CDN bundle from our edge.
|
|
1640
|
+
// Was previously `<script src="https://cdn.tailwindcss.com/...">`
|
|
1641
|
+
// — that violated the 100% edge contract by making the browser
|
|
1642
|
+
// fetch a third-party CDN on every preview load. The bundle is
|
|
1643
|
+
// now embedded in the supervisor at build time
|
|
1644
|
+
// (scripts/bundle-tailwind-play.mjs) and served from
|
|
1645
|
+
// /<basePath>/__nimbus_assets/tailwind-play.js with immutable
|
|
1646
|
+
// cache headers. The tailwind.config inline-script is still
|
|
1647
|
+
// placed AFTER the bundle script so the IIFE picks it up.
|
|
1648
|
+
if (this.hasTailwind) {
|
|
1649
|
+
const twUrl = (this.basePath && this.basePath !== '/' ? this.basePath : '') +
|
|
1650
|
+
'/__nimbus_assets/tailwind-play.js';
|
|
1651
|
+
headInjections += `<script src="${twUrl}"></script>\n`;
|
|
1652
|
+
if (this.tailwindConfigJs) {
|
|
1653
|
+
// Inject tailwind config
|
|
1654
|
+
headInjections += `<script>\ntailwind.config = ${this.tailwindConfigJs}\n</script>\n`;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// 3. HMR client + runtime error overlay.
|
|
1658
|
+
// Overlay is injected alongside HMR because both need to attach global
|
|
1659
|
+
// window listeners before any user modules start loading. The overlay
|
|
1660
|
+
// catches uncaught runtime errors (including "does not provide an export"
|
|
1661
|
+
// module-link failures that leave the root div empty with no console
|
|
1662
|
+
// output) and surfaces them as a red banner so users never see a
|
|
1663
|
+
// mysteriously blank preview.
|
|
1664
|
+
headInjections += ERROR_OVERLAY_CLIENT + '\n';
|
|
1665
|
+
headInjections += HMR_CLIENT + '\n';
|
|
1666
|
+
// Inject before </head>
|
|
1667
|
+
if (html.includes('</head>')) {
|
|
1668
|
+
html = html.replace('</head>', headInjections + '</head>');
|
|
1669
|
+
}
|
|
1670
|
+
else if (html.includes('<body')) {
|
|
1671
|
+
html = html.replace('<body', headInjections + '<body');
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
html = headInjections + html;
|
|
1675
|
+
}
|
|
1676
|
+
// Rewrite absolute paths to include basePath prefix
|
|
1677
|
+
html = this.rewriteHtmlPaths(html);
|
|
1678
|
+
return new Response(html, {
|
|
1679
|
+
headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8' },
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
// ── Module serving (/@modules/<pkg>) ──────────────────────────────────
|
|
1683
|
+
async serveModule(specifier, headers) {
|
|
1684
|
+
const JS_CT = 'application/javascript; charset=utf-8';
|
|
1685
|
+
const cacheKey = `@modules/${specifier}`;
|
|
1686
|
+
// 1. In-memory cache (hot path — already bundled)
|
|
1687
|
+
const memCached = this.moduleCache.get(cacheKey);
|
|
1688
|
+
if (memCached) {
|
|
1689
|
+
return new Response(memCached.code, {
|
|
1690
|
+
headers: { ...headers, 'Content-Type': JS_CT },
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
// 2. SQLite pre-bundle cache (warm path — from npm install pre-bundling)
|
|
1694
|
+
// Pre-bundled modules may contain leaked bare imports if they were built
|
|
1695
|
+
// with `external` peer deps (see npm-installer.prebundleUsedModules).
|
|
1696
|
+
// Run them through rewriteAllImports so any remaining bare specifiers
|
|
1697
|
+
// get the correct /preview/@modules/ URL prefix, then synthesize named
|
|
1698
|
+
// exports if the bundle only exports default (CJS-only packages).
|
|
1699
|
+
if (this.npmCache) {
|
|
1700
|
+
const esmBundle = this.npmCache.getEsmBundle(specifier);
|
|
1701
|
+
// Only use cached bundles built with the current bundler version.
|
|
1702
|
+
// Stale bundles (from older bundler versions) are treated as missing
|
|
1703
|
+
// and re-bundled on the cold path.
|
|
1704
|
+
if (esmBundle && esmBundle.bundleHash === BUNDLER_VERSION) {
|
|
1705
|
+
let code = esmBundle.esmCode;
|
|
1706
|
+
// Rewrite __require("external") calls to ESM imports so externalized
|
|
1707
|
+
// packages (react, scheduler) actually work in the browser.
|
|
1708
|
+
code = rewriteExternalRequires(code, this.basePath);
|
|
1709
|
+
code = rewriteAllImports(code, this.aliases, this.basePath);
|
|
1710
|
+
code = synthesizeCjsNamedExports(code);
|
|
1711
|
+
this.moduleCache.set(cacheKey, { code, timestamp: Date.now() });
|
|
1712
|
+
return new Response(code, {
|
|
1713
|
+
headers: { ...headers, 'Content-Type': JS_CT },
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
// ── Cold path coalescing + serialization ──────────────────────
|
|
1718
|
+
// Multiple parallel browser requests for the same module are
|
|
1719
|
+
// common on first preview load; coalesce so exactly ONE bundle
|
|
1720
|
+
// attempt runs per spec. Across DIFFERENT specs, serialize via a
|
|
1721
|
+
// single-slot semaphore so peak supervisor slice memory stays at
|
|
1722
|
+
// ~28 MiB regardless of browser parallelism. See
|
|
1723
|
+
// `pendingBundles` and `onDemandQueue` field docs for context.
|
|
1724
|
+
const inflight = this.pendingBundles.get(cacheKey);
|
|
1725
|
+
if (inflight)
|
|
1726
|
+
return inflight;
|
|
1727
|
+
const coldPromise = (async () => {
|
|
1728
|
+
const prev = this.onDemandQueue;
|
|
1729
|
+
let releaseSlot;
|
|
1730
|
+
this.onDemandQueue = new Promise((res) => { releaseSlot = res; });
|
|
1731
|
+
try {
|
|
1732
|
+
await prev;
|
|
1733
|
+
return await this.serveModuleCold(specifier, headers);
|
|
1734
|
+
}
|
|
1735
|
+
finally {
|
|
1736
|
+
releaseSlot();
|
|
1737
|
+
}
|
|
1738
|
+
})();
|
|
1739
|
+
this.pendingBundles.set(cacheKey, coldPromise);
|
|
1740
|
+
coldPromise.finally(() => {
|
|
1741
|
+
// Drop the coalescing entry once settled. Subsequent requests
|
|
1742
|
+
// for the same spec will hit the moduleCache (set inside the
|
|
1743
|
+
// cold path) on the hot path, not re-enter the slow path.
|
|
1744
|
+
this.pendingBundles.delete(cacheKey);
|
|
1745
|
+
});
|
|
1746
|
+
return coldPromise;
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Cold path of serveModule: package resolution → on-demand facet
|
|
1750
|
+
* bundle (synthetic-entry for barrels) → hard-error if bundle fails.
|
|
1751
|
+
* NO CDN fallback (100% edge contract). Extracted so the coalescing
|
|
1752
|
+
* + semaphore wrapper in serveModule() reads cleanly. Always runs
|
|
1753
|
+
* inside the on-demand semaphore — see serveModule's wrapper.
|
|
1754
|
+
*/
|
|
1755
|
+
async serveModuleCold(specifier, headers) {
|
|
1756
|
+
const JS_CT = 'application/javascript; charset=utf-8';
|
|
1757
|
+
const cacheKey = `@modules/${specifier}`;
|
|
1758
|
+
// 3. On-demand bundle (cold path — resolve from node_modules, bundle via esbuild)
|
|
1759
|
+
//
|
|
1760
|
+
// Architecture: this used to call this.esbuild.build(...) in the
|
|
1761
|
+
// supervisor isolate. For large modules (lucide-react, ~18 MiB
|
|
1762
|
+
// unpacked) that OOM'd the supervisor and surfaced as CF error
|
|
1763
|
+
// 1101 on /preview/@modules/lucide-react, taking down the entire
|
|
1764
|
+
// preview. We now dispatch the bundle work to a NimbusLoaderPool
|
|
1765
|
+
// isolate via its own 128 MiB heap — same pattern as install-time
|
|
1766
|
+
// pre-bundling. Supervisor never bundles esbuild for any path.
|
|
1767
|
+
//
|
|
1768
|
+
// Falls back to in-supervisor esbuild ONLY if env/ctx aren't
|
|
1769
|
+
// available (e.g. legacy callers / tests).
|
|
1770
|
+
const resolved = this.resolvePackage(specifier);
|
|
1771
|
+
// Barrel packages (lucide-react, @phosphor-icons/react, react-icons,
|
|
1772
|
+
// @mui/icons-material, …) ship hundreds/thousands of tiny re-export
|
|
1773
|
+
// files. Bundling the whole barrel OOMs esbuild's 128 MiB facet.
|
|
1774
|
+
// Instead we synthesize a tiny entry from the user's actual named
|
|
1775
|
+
// imports (`export { Home, FileText } from 'pkg'`) and bundle THAT
|
|
1776
|
+
// through the standard facet path — esbuild tree-shakes the rest.
|
|
1777
|
+
// 100% edge: NO esm.sh / CDN fallback.
|
|
1778
|
+
if (resolved) {
|
|
1779
|
+
const pkgName = packageNameFromSpecifier(specifier);
|
|
1780
|
+
const pkgDir = this.root + '/node_modules/' + pkgName;
|
|
1781
|
+
const fileCount = countPackageFiles(this.vfs, pkgDir);
|
|
1782
|
+
const isBarrel = fileCount > BARREL_PKG_FILE_THRESHOLD && specifier === pkgName;
|
|
1783
|
+
let bundled = null;
|
|
1784
|
+
const externals = getSharedRuntimeExternals(specifier);
|
|
1785
|
+
const onDemandPool = await this.ensureOnDemandPool().catch(() => null);
|
|
1786
|
+
// Determine the entry path the facet should bundle:
|
|
1787
|
+
// - non-barrel: the package's resolved main entry
|
|
1788
|
+
// - barrel: a synthesized entry written to VFS that
|
|
1789
|
+
// re-exports just the named imports the user's
|
|
1790
|
+
// source uses. esbuild tree-shakes the rest.
|
|
1791
|
+
let bundleEntryPath = resolved;
|
|
1792
|
+
let synthetic = false;
|
|
1793
|
+
let syntheticReferencedFiles = null;
|
|
1794
|
+
if (isBarrel) {
|
|
1795
|
+
const namedImports = scanNamedImports(this.vfs, this.root);
|
|
1796
|
+
const names = namedImports.get(pkgName);
|
|
1797
|
+
if (!names || names.size === 0) {
|
|
1798
|
+
// No statically-resolvable imports. We refuse to CDN-fallback
|
|
1799
|
+
// (100% edge contract) AND we refuse to bundle the whole
|
|
1800
|
+
// barrel (would OOM the facet). Primitives wave (P6): return
|
|
1801
|
+
// a 200 module that throws on first use so the runtime
|
|
1802
|
+
// error surfaces in the BROWSER console (where the user is
|
|
1803
|
+
// looking) AND on the dev-server log (Process tab) — instead
|
|
1804
|
+
// of an opaque 500 in the network panel.
|
|
1805
|
+
const diag = `[vite-dev] cannot bundle ${specifier}: barrel package (${fileCount} files) ` +
|
|
1806
|
+
`with no static named imports detected in user source. ` +
|
|
1807
|
+
`Dynamic imports / computed-name access can't be tree-shaken. ` +
|
|
1808
|
+
`Add explicit imports like ` +
|
|
1809
|
+
`\`import { IconName } from '${pkgName}'\` so Nimbus can synthesize ` +
|
|
1810
|
+
`a tree-shakable entry.`;
|
|
1811
|
+
this.log('error', diag);
|
|
1812
|
+
const escaped = JSON.stringify(diag);
|
|
1813
|
+
return new Response(`// nimbus: ${diag}\n` +
|
|
1814
|
+
`const __err = new Error(${escaped});\n` +
|
|
1815
|
+
`console.error(${escaped});\n` +
|
|
1816
|
+
`export default new Proxy({}, { get: () => { throw __err; } });\n`, {
|
|
1817
|
+
status: 200,
|
|
1818
|
+
headers: { ...headers, 'Content-Type': JS_CT, 'X-Nimbus-Bundle-Status': 'no-static-imports' },
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
const nmDirOnDemand = this.root + '/node_modules';
|
|
1822
|
+
const synth = buildSyntheticEntry(this.vfs, nmDirOnDemand, pkgName, names);
|
|
1823
|
+
if (!synth) {
|
|
1824
|
+
// Primitives wave (P6): emit an honest 200 stub instead of a
|
|
1825
|
+
// 500. The stub has the same effect at runtime (any access
|
|
1826
|
+
// throws), but the network response is a parseable JS module
|
|
1827
|
+
// so the browser's module loader doesn't crash with an
|
|
1828
|
+
// unrelated "failed to fetch" error that obscures the real
|
|
1829
|
+
// diagnostic.
|
|
1830
|
+
const diag = `Nimbus: synthetic entry generation failed for ${specifier} (barrel: ${fileCount} files; ${names.size} static imports). The package's index does not match any pattern Nimbus's barrel synthesizer understands (export {…} from, export * from, top-level decls). File a bug.`;
|
|
1831
|
+
this.log('error', diag);
|
|
1832
|
+
const escaped = JSON.stringify(diag);
|
|
1833
|
+
return new Response(`// nimbus: synthetic entry generation returned null for ${specifier}\n` +
|
|
1834
|
+
`console.error(${escaped});\n` +
|
|
1835
|
+
`const __err = new Error(${escaped});\n` +
|
|
1836
|
+
`export default new Proxy({}, { get: () => { throw __err; } });\n`, { status: 200, headers: { ...headers, 'Content-Type': JS_CT, 'X-Nimbus-Bundle-Status': 'synth-null' } });
|
|
1837
|
+
}
|
|
1838
|
+
const synthPath = syntheticEntryPath(this.root, pkgName);
|
|
1839
|
+
try {
|
|
1840
|
+
this.vfs.mkdir(synthPath.substring(0, synthPath.lastIndexOf('/')), { recursive: true });
|
|
1841
|
+
this.vfs.writeFile(synthPath, synth.code);
|
|
1842
|
+
}
|
|
1843
|
+
catch (e) {
|
|
1844
|
+
// Primitives wave (P6): same 200-stub pattern as the synth-null
|
|
1845
|
+
// branch above. VFS write failure is rare but distinct from a
|
|
1846
|
+
// synthesizer gap, so we keep the diagnostic header value
|
|
1847
|
+
// separate for log triage.
|
|
1848
|
+
const msg = `[vite-dev] failed to write synthetic entry for ${specifier}: ${e?.message || e}`;
|
|
1849
|
+
this.log('error', msg);
|
|
1850
|
+
const escaped = JSON.stringify(msg);
|
|
1851
|
+
return new Response(`// nimbus: ${msg}\nconsole.error(${escaped});\nconst __err = new Error(${escaped});\nexport default new Proxy({}, { get: () => { throw __err; } });\n`, { status: 200, headers: { ...headers, 'Content-Type': JS_CT, 'X-Nimbus-Bundle-Status': 'vfs-write-failed' } });
|
|
1852
|
+
}
|
|
1853
|
+
bundleEntryPath = synthPath;
|
|
1854
|
+
synthetic = true;
|
|
1855
|
+
// Stash the referenced files so the slice-build below uses
|
|
1856
|
+
// a SCOPED slice instead of the standard whole-package walk.
|
|
1857
|
+
syntheticReferencedFiles = synth.referencedFiles;
|
|
1858
|
+
this.log('warn', `[vite-dev] synthesized entry for ${specifier} ` +
|
|
1859
|
+
`(barrel: ${fileCount} files; ${names.size} static imports → tree-shaken bundle)`);
|
|
1860
|
+
}
|
|
1861
|
+
if (onDemandPool) {
|
|
1862
|
+
// Facet path — supervisor stays at 0 esbuild bytes.
|
|
1863
|
+
try {
|
|
1864
|
+
const { buildSliceForSpecifierWithCap, prebundleOne, BUNDLER_VERSION, } = await import('../npm/pre-bundle-facet.js');
|
|
1865
|
+
const SLICE_CAP_BYTES = 28 * 1024 * 1024;
|
|
1866
|
+
const projDir = this.root;
|
|
1867
|
+
const nmDir = projDir + '/node_modules';
|
|
1868
|
+
let slice = null;
|
|
1869
|
+
if (synthetic && syntheticReferencedFiles) {
|
|
1870
|
+
// SCOPED slice: only the files the synthetic entry directly
|
|
1871
|
+
// references (+ transitive relative imports + package.json).
|
|
1872
|
+
// Skips the full package walk so icon-libraries with
|
|
1873
|
+
// thousands of files don't blow the 28 MiB cap.
|
|
1874
|
+
const scoped = buildScopedSliceForSynthetic(this.vfs, nmDir, pkgName, syntheticReferencedFiles);
|
|
1875
|
+
const built = {
|
|
1876
|
+
slice: scoped.entries,
|
|
1877
|
+
totalBytes: scoped.totalBytes,
|
|
1878
|
+
};
|
|
1879
|
+
try {
|
|
1880
|
+
const bytes = this.vfs.readFile(bundleEntryPath);
|
|
1881
|
+
const parentDir = bundleEntryPath.substring(0, bundleEntryPath.lastIndexOf('/'));
|
|
1882
|
+
built.slice.push({ path: '/' + parentDir.replace(/^\/+/, ''), isDir: true });
|
|
1883
|
+
built.slice.push({
|
|
1884
|
+
path: '/' + bundleEntryPath.replace(/^\/+/, ''),
|
|
1885
|
+
bytes,
|
|
1886
|
+
isDir: false,
|
|
1887
|
+
});
|
|
1888
|
+
built.totalBytes += bytes.length + bundleEntryPath.length;
|
|
1889
|
+
}
|
|
1890
|
+
catch (e) {
|
|
1891
|
+
this.log('error', '[vite-dev] synthetic entry unreadable for ' + specifier + ': ' + (e?.message || e));
|
|
1892
|
+
}
|
|
1893
|
+
slice = built;
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
slice = buildSliceForSpecifierWithCap(this.vfs, specifier, nmDir, SLICE_CAP_BYTES);
|
|
1897
|
+
}
|
|
1898
|
+
if (slice) {
|
|
1899
|
+
// Build the spec, then drop our supervisor-side handle to
|
|
1900
|
+
// the slice array immediately — `spec` is the only thing
|
|
1901
|
+
// that needs to keep it alive until the RPC structured-clone
|
|
1902
|
+
// completes. Mirrors the install-time runSlot pattern (see
|
|
1903
|
+
// commit 40cfc01) so peak supervisor heap during a flurry of
|
|
1904
|
+
// /preview/@modules/* requests stays at <(1 × 28 MiB).
|
|
1905
|
+
let spec = {
|
|
1906
|
+
specifier,
|
|
1907
|
+
entryPath: bundleEntryPath,
|
|
1908
|
+
externals,
|
|
1909
|
+
slice: slice.slice,
|
|
1910
|
+
bundlerVersion: BUNDLER_VERSION,
|
|
1911
|
+
define: this.define,
|
|
1912
|
+
};
|
|
1913
|
+
slice = null;
|
|
1914
|
+
let result = null;
|
|
1915
|
+
try {
|
|
1916
|
+
result = await onDemandPool.submit(prebundleOne, spec);
|
|
1917
|
+
}
|
|
1918
|
+
finally {
|
|
1919
|
+
spec = null;
|
|
1920
|
+
}
|
|
1921
|
+
if (result && result.ok && result.esmCode) {
|
|
1922
|
+
bundled = result.esmCode;
|
|
1923
|
+
}
|
|
1924
|
+
else if (result && result.errorText) {
|
|
1925
|
+
this.log('error', '[vite-dev] facet bundle failed for ' + specifier + ': ' + result.errorText);
|
|
1926
|
+
}
|
|
1927
|
+
result = null;
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
this.log('error', '[vite-dev] slice walker exceeded cap for ' + specifier);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
catch (e) {
|
|
1934
|
+
this.log('error', '[vite-dev] on-demand facet dispatch failed for ' + specifier + ': ' + (e?.message || e));
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
else {
|
|
1938
|
+
// Legacy fallback — in-supervisor esbuild. Used only when
|
|
1939
|
+
// env/ctx weren't passed in (no facet pool can be built).
|
|
1940
|
+
try {
|
|
1941
|
+
const result = await this.esbuild.build([bundleEntryPath], {
|
|
1942
|
+
bundle: true,
|
|
1943
|
+
format: 'esm',
|
|
1944
|
+
platform: 'browser',
|
|
1945
|
+
target: 'esnext',
|
|
1946
|
+
define: this.define,
|
|
1947
|
+
external: externals.length > 0 ? externals : undefined,
|
|
1948
|
+
});
|
|
1949
|
+
if (result.outputFiles?.length) {
|
|
1950
|
+
bundled = result.outputFiles[0].contents;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
catch (e) {
|
|
1954
|
+
this.log('error', '[vite-dev] esbuild bundle failed for ' + specifier + ': ' + (e?.message || e));
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
if (bundled !== null) {
|
|
1958
|
+
let code = bundled;
|
|
1959
|
+
// Convert `__require("external")` calls (from CJS source with esbuild
|
|
1960
|
+
// externals) into ESM `import * as` + dispatch. Without this, the
|
|
1961
|
+
// browser throws when it hits __require("react") at runtime.
|
|
1962
|
+
code = rewriteExternalRequires(code, this.basePath);
|
|
1963
|
+
// Rewrite any bare imports that esbuild marked external (e.g., from
|
|
1964
|
+
// the VFS plugin's exports-field-unaware fallback). Without this, a
|
|
1965
|
+
// bundled output like `import X from "scheduler"` would 404 in the
|
|
1966
|
+
// browser because the specifier doesn't include the base prefix.
|
|
1967
|
+
code = rewriteAllImports(code, this.aliases, this.basePath);
|
|
1968
|
+
// For CJS-only packages (react, react-dom), esbuild's __commonJS
|
|
1969
|
+
// wrapper only emits `export default` — named imports like
|
|
1970
|
+
// `import { createRoot } from "react-dom/client"` would fail. Statically
|
|
1971
|
+
// scan the bundled source for CJS export patterns and synthesize
|
|
1972
|
+
// named exports.
|
|
1973
|
+
code = synthesizeCjsNamedExports(code);
|
|
1974
|
+
// Cache ONLY successful bundles. Caching a failed build (where
|
|
1975
|
+
// `code` was the raw unbundled source) would poison the cache
|
|
1976
|
+
// and break the module forever.
|
|
1977
|
+
if (this.npmCache) {
|
|
1978
|
+
try {
|
|
1979
|
+
this.npmCache.putEsmBundle({
|
|
1980
|
+
specifier,
|
|
1981
|
+
bundleHash: BUNDLER_VERSION,
|
|
1982
|
+
esmCode: code,
|
|
1983
|
+
builtAt: Date.now(),
|
|
1984
|
+
inputHash: '',
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
catch { /* non-fatal */ }
|
|
1988
|
+
}
|
|
1989
|
+
this.moduleCache.set(cacheKey, { code, timestamp: Date.now() });
|
|
1990
|
+
return new Response(code, {
|
|
1991
|
+
headers: { ...headers, 'Content-Type': JS_CT },
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
// Bundle failed — fall through to the hard-error response below.
|
|
1995
|
+
// Previously fell through to a CDN fallback (esm.sh) but that
|
|
1996
|
+
// violated the 100% edge contract.
|
|
1997
|
+
}
|
|
1998
|
+
// 4. NOT-INSTALLED → STUB; BUNDLE-FAILED → HARD ERROR.
|
|
1999
|
+
// NO CDN fallback (100% edge contract).
|
|
2000
|
+
//
|
|
2001
|
+
// Two distinct failure modes need different responses:
|
|
2002
|
+
//
|
|
2003
|
+
// a. Package not installed locally. This is common for OPTIONAL
|
|
2004
|
+
// peer-deps surfaced by transitive imports — e.g. framer-motion
|
|
2005
|
+
// does `try { import('@emotion/is-prop-valid') } catch {}` for a
|
|
2006
|
+
// runtime feature flag. Pre-bundle's slice walker marks the
|
|
2007
|
+
// unresolved import as external; the browser then fetches
|
|
2008
|
+
// /preview/@modules/@emotion/is-prop-valid. Returning a 503
|
|
2009
|
+
// with `throw new Error(...)` would fail the entire module graph
|
|
2010
|
+
// BEFORE framer-motion's try/catch runs (the throw happens at
|
|
2011
|
+
// module-load time, not at function-call time). Instead we serve
|
|
2012
|
+
// a tiny stub:
|
|
2013
|
+
//
|
|
2014
|
+
// export default undefined;
|
|
2015
|
+
// export const __nimbus_optional_dep_stub = true;
|
|
2016
|
+
//
|
|
2017
|
+
// The browser-side library's runtime feature-detect picks up the
|
|
2018
|
+
// undefined and gracefully degrades. NO crash, NO CDN hit, NO
|
|
2019
|
+
// accidental "you forgot to install X" silent failure (we log
|
|
2020
|
+
// a clear warning to the supervisor for visibility).
|
|
2021
|
+
//
|
|
2022
|
+
// b. Package IS installed but bundling failed (slice cap, OOM,
|
|
2023
|
+
// unresolvable internal import). Return 503 with throwing
|
|
2024
|
+
// module body — the user needs to see the failure to fix it.
|
|
2025
|
+
const pkgName = packageNameFromSpecifier(specifier);
|
|
2026
|
+
const installed = this.vfs.exists(this.root + '/node_modules/' + pkgName);
|
|
2027
|
+
if (!installed) {
|
|
2028
|
+
const stubMsg = `[vite-dev] serving stub for /preview/@modules/${specifier}: ` +
|
|
2029
|
+
`package not installed locally (likely an optional transitive peer-dep). ` +
|
|
2030
|
+
`If your code calls into ${specifier} directly, add it to package.json ` +
|
|
2031
|
+
`and run \`npm install\`.`;
|
|
2032
|
+
this.log('warn', stubMsg);
|
|
2033
|
+
const stubCode = `// nimbus: ${stubMsg}\n` +
|
|
2034
|
+
`export default undefined;\n` +
|
|
2035
|
+
`export const __nimbus_optional_dep_stub = true;\n`;
|
|
2036
|
+
// Cache so repeated transitive references don't re-walk + re-warn.
|
|
2037
|
+
this.moduleCache.set(cacheKey, { code: stubCode, timestamp: Date.now() });
|
|
2038
|
+
return new Response(stubCode, {
|
|
2039
|
+
headers: { ...headers, 'Content-Type': JS_CT },
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
const diag = `[vite-dev] cannot serve /preview/@modules/${specifier}: ` +
|
|
2043
|
+
`on-demand bundle failed (esbuild OOM, slice cap, or unresolved internal import). ` +
|
|
2044
|
+
`Check supervisor logs for the underlying error. ` +
|
|
2045
|
+
`If the package is a "barrel" (icon library, etc.), Nimbus ` +
|
|
2046
|
+
`auto-tree-shakes from your static named imports — make sure ` +
|
|
2047
|
+
`you're using \`import { Foo } from '${pkgName}'\` syntax ` +
|
|
2048
|
+
`(not \`import * as X\` or dynamic \`import()\`).`;
|
|
2049
|
+
this.log('error', diag);
|
|
2050
|
+
// Primitives wave (P6): same 200-stub pattern. The supervisor's
|
|
2051
|
+
// own bundle pipeline failed — surface the diagnostic in the
|
|
2052
|
+
// browser console where the user is looking, not in a 503 the
|
|
2053
|
+
// browser's module loader treats as "module-not-found".
|
|
2054
|
+
const escaped = JSON.stringify(diag);
|
|
2055
|
+
const errCode = `// nimbus: ${diag}\nconsole.error(${escaped});\n` +
|
|
2056
|
+
`const __err = new Error(${escaped});\n` +
|
|
2057
|
+
`export default new Proxy({}, { get: () => { throw __err; } });\n`;
|
|
2058
|
+
return new Response(errCode, {
|
|
2059
|
+
status: 200,
|
|
2060
|
+
headers: {
|
|
2061
|
+
...headers,
|
|
2062
|
+
'Content-Type': JS_CT,
|
|
2063
|
+
'Cache-Control': 'no-store',
|
|
2064
|
+
'X-Nimbus-Bundle-Status': 'bundle-failed',
|
|
2065
|
+
},
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Resolve a bare package specifier (possibly with subpath) to a VFS file path.
|
|
2070
|
+
*
|
|
2071
|
+
* Algorithm:
|
|
2072
|
+
* 1. Parse into pkgName + subpath (e.g. "pkg/sub/deep" → pkg="pkg", subpath="sub/deep")
|
|
2073
|
+
* 2. Walk search dirs looking for node_modules/<pkg>/
|
|
2074
|
+
* 3. PREFER exports-field resolution (modern packages) with conditions
|
|
2075
|
+
* [import, module, browser, default]
|
|
2076
|
+
* 4. FALL BACK to legacy resolution (packages without exports field):
|
|
2077
|
+
* a. For subpath: try <nmDir>/<subpath>.{js,mjs,cjs,jsx,ts,tsx} — direct file
|
|
2078
|
+
* b. For subpath: try <nmDir>/<subpath>/index.{js,mjs,cjs,jsx,ts,tsx}
|
|
2079
|
+
* c. For subpath: try <nmDir>/<subpath>/package.json → read module/main
|
|
2080
|
+
* d. For root: try pkg.module / pkg.main, then tryResolveFile
|
|
2081
|
+
* e. For root: try <nmDir>/index.{ext}
|
|
2082
|
+
*
|
|
2083
|
+
* Step 4c is what makes `react-remove-scroll-bar/constants` work for legacy
|
|
2084
|
+
* packages without an exports field — the subpath directory has its own
|
|
2085
|
+
* package.json (or just an index.js that we pick up in 4b).
|
|
2086
|
+
*/
|
|
2087
|
+
resolvePackage(specifier) {
|
|
2088
|
+
let pkgName;
|
|
2089
|
+
let subpath;
|
|
2090
|
+
if (specifier.startsWith('@')) {
|
|
2091
|
+
const parts = specifier.split('/');
|
|
2092
|
+
pkgName = parts.slice(0, 2).join('/');
|
|
2093
|
+
subpath = parts.slice(2).join('/');
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
const parts = specifier.split('/');
|
|
2097
|
+
pkgName = parts[0];
|
|
2098
|
+
subpath = parts.slice(1).join('/');
|
|
2099
|
+
}
|
|
2100
|
+
const searchDirs = [this.root, 'home/user', 'usr/share/pkg'];
|
|
2101
|
+
for (const dir of searchDirs) {
|
|
2102
|
+
const nmDir = dir + '/node_modules/' + pkgName;
|
|
2103
|
+
if (!this.vfs.exists(nmDir))
|
|
2104
|
+
continue;
|
|
2105
|
+
// Read the owning package.json once.
|
|
2106
|
+
const pkgJsonPath = nmDir + '/package.json';
|
|
2107
|
+
let pkg = null;
|
|
2108
|
+
if (this.vfs.exists(pkgJsonPath)) {
|
|
2109
|
+
try {
|
|
2110
|
+
pkg = JSON.parse(this.vfs.readFileString(pkgJsonPath));
|
|
2111
|
+
}
|
|
2112
|
+
catch { /* malformed */ }
|
|
2113
|
+
}
|
|
2114
|
+
// 1. EXPORTS-FIELD RESOLUTION (modern packages like zustand, vfile).
|
|
2115
|
+
// Only attempt if the package actually declares exports. For a subpath
|
|
2116
|
+
// that isn't in exports, resolveExports returns null — we fall through
|
|
2117
|
+
// to legacy resolution rather than treating the miss as fatal.
|
|
2118
|
+
if (pkg?.exports) {
|
|
2119
|
+
const entry = resolvePackageEntry(pkg, subpath ? './' + subpath : '.');
|
|
2120
|
+
if (entry) {
|
|
2121
|
+
// Normalize to collapse any ../ segments from relative entry paths.
|
|
2122
|
+
const resolved = this.tryResolveFile(normalizePath(nmDir + '/' + entry.replace(/^\.\//, '')));
|
|
2123
|
+
if (resolved)
|
|
2124
|
+
return resolved;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
// 2. LEGACY SUBPATH (no exports field, or exports didn't cover this subpath).
|
|
2128
|
+
// Packages like react-remove-scroll-bar expose their structure directly
|
|
2129
|
+
// through `files` and flat directory layout. This block walks the
|
|
2130
|
+
// usual Node.js legacy resolution:
|
|
2131
|
+
// a) <subpath>.{ext} — direct file
|
|
2132
|
+
// b) <subpath>/index.{ext} — directory with index
|
|
2133
|
+
// c) <subpath>/package.json — nested package.json (pkg/module/main)
|
|
2134
|
+
//
|
|
2135
|
+
// Branch (c) is critical for packages like react-remove-scroll-bar
|
|
2136
|
+
// whose `constants/package.json` points OUT of the directory with
|
|
2137
|
+
// `"module": "../dist/es2015/constants.js"`. The resulting path
|
|
2138
|
+
// `constants/../dist/es2015/constants.js` MUST be normalized to
|
|
2139
|
+
// `dist/es2015/constants.js` before VFS lookup.
|
|
2140
|
+
if (subpath) {
|
|
2141
|
+
// (a) + (b) combined — tryResolveFile handles both
|
|
2142
|
+
const direct = this.tryResolveFile(normalizePath(nmDir + '/' + subpath));
|
|
2143
|
+
if (direct)
|
|
2144
|
+
return direct;
|
|
2145
|
+
// (c) nested package.json
|
|
2146
|
+
const nestedPkgPath = nmDir + '/' + subpath + '/package.json';
|
|
2147
|
+
if (this.vfs.exists(nestedPkgPath)) {
|
|
2148
|
+
try {
|
|
2149
|
+
const nested = JSON.parse(this.vfs.readFileString(nestedPkgPath));
|
|
2150
|
+
const entry = nested.module || nested.main || 'index.js';
|
|
2151
|
+
// Normalize to collapse ../ segments — critical when a nested
|
|
2152
|
+
// package.json redirects to a sibling directory (common pattern
|
|
2153
|
+
// for legacy packages shipping both es5 and es2015 builds).
|
|
2154
|
+
const resolved = this.tryResolveFile(normalizePath(nmDir + '/' + subpath + '/' + entry.replace(/^\.\//, '')));
|
|
2155
|
+
if (resolved)
|
|
2156
|
+
return resolved;
|
|
2157
|
+
}
|
|
2158
|
+
catch { /* malformed */ }
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
// 3. ROOT ENTRY (no subpath) via module/main fields.
|
|
2162
|
+
if (!subpath && pkg) {
|
|
2163
|
+
const entry = pkg.module || pkg.main;
|
|
2164
|
+
if (entry) {
|
|
2165
|
+
const resolved = this.tryResolveFile(normalizePath(nmDir + '/' + entry.replace(/^\.\//, '')));
|
|
2166
|
+
if (resolved)
|
|
2167
|
+
return resolved;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
// 4. Last resort — <pkgRoot>/index.{ext}
|
|
2171
|
+
const fallback = this.tryResolveFile(nmDir + '/index');
|
|
2172
|
+
if (fallback)
|
|
2173
|
+
return fallback;
|
|
2174
|
+
}
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
tryResolveFile(base) {
|
|
2178
|
+
// Defense in depth: normalize input even if callers already did. The VFS
|
|
2179
|
+
// treats `..` as a literal path component (no traversal resolution at
|
|
2180
|
+
// lookup time), so any un-normalized `../` in the path will miss.
|
|
2181
|
+
const norm = normalizePath(base);
|
|
2182
|
+
// Covers .cjs and .mts/.cts too — legacy packages use .cjs,
|
|
2183
|
+
// some modern packages use .mts for their ESM build.
|
|
2184
|
+
const exts = ['', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.mts', '.cts', '.json'];
|
|
2185
|
+
for (const ext of exts) {
|
|
2186
|
+
const p = norm + ext;
|
|
2187
|
+
if (this.vfs.exists(p) && !this.vfs.isDirectory(p))
|
|
2188
|
+
return p;
|
|
2189
|
+
}
|
|
2190
|
+
// Directory index fallback.
|
|
2191
|
+
if (this.vfs.exists(norm) && this.vfs.isDirectory(norm)) {
|
|
2192
|
+
const idxExts = ['index.js', 'index.mjs', 'index.cjs', 'index.ts', 'index.tsx', 'index.jsx'];
|
|
2193
|
+
for (const idx of idxExts) {
|
|
2194
|
+
const p = norm + '/' + idx;
|
|
2195
|
+
if (this.vfs.exists(p) && !this.vfs.isDirectory(p))
|
|
2196
|
+
return p;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return null;
|
|
2200
|
+
}
|
|
2201
|
+
// ── File serving ──────────────────────────────────────────────────────
|
|
2202
|
+
/**
|
|
2203
|
+
* Try to resolve a URL pathname to an actual file in the VFS by applying
|
|
2204
|
+
* Vite/webpack-style extension resolution. This is critical for ES module
|
|
2205
|
+
* imports like `import App from "./App"` where the browser requests
|
|
2206
|
+
* /preview/src/App with no extension.
|
|
2207
|
+
*
|
|
2208
|
+
* Resolution order (matches Vite's default `resolve.extensions` plus .vue/.svelte):
|
|
2209
|
+
* 1. Exact file path
|
|
2210
|
+
* 2. path + .tsx, .ts, .jsx, .js, .mjs, .cjs, .vue, .svelte, .json
|
|
2211
|
+
* 3. path as directory → path/index.{ext} (covering .html for static sites)
|
|
2212
|
+
* 4. For .js/.mjs/.cjs/.jsx specifiers that don't resolve, try .ts/.tsx/.mts/.cts
|
|
2213
|
+
* fallback — common in TypeScript projects with NodeNext module resolution
|
|
2214
|
+
* (imports written as "./bar.js" while source is "./bar.ts")
|
|
2215
|
+
*
|
|
2216
|
+
* Note: `sanitizePath` strips trailing slashes before this runs, so requests
|
|
2217
|
+
* like `/utils/` arrive here as `/utils` — they hit the directory-index branch
|
|
2218
|
+
* via the `isDirectory` check, which is correct.
|
|
2219
|
+
*/
|
|
2220
|
+
resolveFileCandidate(pathname) {
|
|
2221
|
+
const basePath = this.root + pathname;
|
|
2222
|
+
// Extension candidates, in preference order (TSX first — most common in React projects).
|
|
2223
|
+
// .vue and .svelte are included for resolver completeness — serving/transforming
|
|
2224
|
+
// them still requires a framework plugin, but at least the resolver finds them.
|
|
2225
|
+
const extensions = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.vue', '.svelte', '.json'];
|
|
2226
|
+
const indexFiles = [
|
|
2227
|
+
'/index.tsx', '/index.ts', '/index.jsx', '/index.js', '/index.mjs', '/index.cjs',
|
|
2228
|
+
'/index.vue', '/index.svelte', '/index.json', '/index.html',
|
|
2229
|
+
];
|
|
2230
|
+
// 1. Exact path match
|
|
2231
|
+
if (this.vfs.exists(basePath) && !this.vfs.isDirectory(basePath)) {
|
|
2232
|
+
return { vfsPath: basePath, pathname };
|
|
2233
|
+
}
|
|
2234
|
+
// Determine whether the final path segment already has an extension.
|
|
2235
|
+
// We compute lastDot and lastSlash relative to the path to handle cases like
|
|
2236
|
+
// `/src/v1.0/App` where a dot appears in a DIRECTORY name (lastDot < lastSlash).
|
|
2237
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
2238
|
+
const lastDot = pathname.lastIndexOf('.');
|
|
2239
|
+
const hasExtension = lastDot > lastSlash && lastDot < pathname.length - 1;
|
|
2240
|
+
// 2. Append extensions if the final segment has no extension.
|
|
2241
|
+
if (!hasExtension) {
|
|
2242
|
+
for (const ext of extensions) {
|
|
2243
|
+
const candidate = basePath + ext;
|
|
2244
|
+
if (this.vfs.exists(candidate) && !this.vfs.isDirectory(candidate)) {
|
|
2245
|
+
return { vfsPath: candidate, pathname: pathname + ext };
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
else {
|
|
2250
|
+
// 4. TypeScript .js → .ts / .tsx fallback for NodeNext projects.
|
|
2251
|
+
const currentExt = pathname.substring(lastDot);
|
|
2252
|
+
const jsToTs = {
|
|
2253
|
+
'.js': ['.ts', '.tsx'],
|
|
2254
|
+
'.mjs': ['.mts', '.ts'],
|
|
2255
|
+
'.cjs': ['.cts', '.ts'],
|
|
2256
|
+
'.jsx': ['.tsx'],
|
|
2257
|
+
};
|
|
2258
|
+
const fallbacks = jsToTs[currentExt];
|
|
2259
|
+
if (fallbacks) {
|
|
2260
|
+
const withoutExt = pathname.substring(0, lastDot);
|
|
2261
|
+
for (const tsExt of fallbacks) {
|
|
2262
|
+
const candidate = this.root + withoutExt + tsExt;
|
|
2263
|
+
if (this.vfs.exists(candidate) && !this.vfs.isDirectory(candidate)) {
|
|
2264
|
+
return { vfsPath: candidate, pathname: withoutExt + tsExt };
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
// 3. Directory → /index.{ext}
|
|
2270
|
+
if (this.vfs.exists(basePath) && this.vfs.isDirectory(basePath)) {
|
|
2271
|
+
for (const idx of indexFiles) {
|
|
2272
|
+
const candidate = basePath + idx;
|
|
2273
|
+
if (this.vfs.exists(candidate) && !this.vfs.isDirectory(candidate)) {
|
|
2274
|
+
return { vfsPath: candidate, pathname: pathname + idx };
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Decide whether a 404 on this request should fall back to index.html (SPA
|
|
2282
|
+
* routing) or stay as 404. We MUST NOT return HTML for JS module requests —
|
|
2283
|
+
* the browser rejects them with a MIME-type error and the whole app breaks.
|
|
2284
|
+
*
|
|
2285
|
+
* Strategy: trust `Sec-Fetch-Dest` (set by all modern browsers) as the
|
|
2286
|
+
* primary signal. Fall back to `Accept` header analysis + tight source-path
|
|
2287
|
+
* heuristics for edge cases like old clients or non-browser fetchers.
|
|
2288
|
+
*
|
|
2289
|
+
* We intentionally DO NOT treat every path under `/api/`, `/hooks/`,
|
|
2290
|
+
* `/components/`, etc. as a module — those are common client-side route
|
|
2291
|
+
* names in real React/Vue apps, and marking them as module would 404 legit
|
|
2292
|
+
* navigation. Only paths rooted under unambiguous build-system directories
|
|
2293
|
+
* (`/src/`, `/node_modules/`, `/@vite/`, `/@modules/`, `/@fs/`, `/public/`,
|
|
2294
|
+
* `/assets/`) are treated as definitely-not-SPA.
|
|
2295
|
+
*/
|
|
2296
|
+
isModuleRequest(request, pathname, query) {
|
|
2297
|
+
// 1. Explicit module query marker (?import)
|
|
2298
|
+
if (query.includes('import'))
|
|
2299
|
+
return true;
|
|
2300
|
+
// 2. Sec-Fetch-Dest — most reliable signal. All modern browsers set this:
|
|
2301
|
+
// 'document' / 'iframe' → top-level navigation (SPA fallback OK)
|
|
2302
|
+
// 'script' / 'style' / 'font' / 'image' / 'audio' / 'video' / 'worker'
|
|
2303
|
+
// / 'object' / 'embed' / 'manifest' / 'track' → subresource (no HTML)
|
|
2304
|
+
// 'empty' → fetch() — treat as module to avoid MIME errors
|
|
2305
|
+
const secFetchDest = request.headers.get('Sec-Fetch-Dest');
|
|
2306
|
+
if (secFetchDest) {
|
|
2307
|
+
if (secFetchDest === 'document' || secFetchDest === 'iframe') {
|
|
2308
|
+
// Explicit navigation — not a module request
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
// Anything else (script, style, empty, etc.) is a subresource.
|
|
2312
|
+
return true;
|
|
2313
|
+
}
|
|
2314
|
+
// 3. Has-extension check — any recognizable file extension means
|
|
2315
|
+
// "this is a specific file, not a SPA route". Catches .js, .css,
|
|
2316
|
+
// .svg, .png, .woff2, etc. Uses slash-aware dot detection to avoid
|
|
2317
|
+
// misidentifying paths with dotted directory segments.
|
|
2318
|
+
const lastSlash = pathname.lastIndexOf('/');
|
|
2319
|
+
const lastDot = pathname.lastIndexOf('.');
|
|
2320
|
+
if (lastDot > lastSlash && lastDot < pathname.length - 1) {
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
// 4. Unambiguous build-system source directories. Trimmed from the prior
|
|
2324
|
+
// aggressive list — `/components/`, `/pages/`, `/hooks/`, `/utils/`,
|
|
2325
|
+
// `/api/` etc. are common SPA route names in real apps and should NOT
|
|
2326
|
+
// force module treatment based on path alone.
|
|
2327
|
+
const sourcePatterns = [
|
|
2328
|
+
'/src/', '/node_modules/', '/@vite/', '/@modules/', '/@fs/',
|
|
2329
|
+
'/public/', '/assets/',
|
|
2330
|
+
];
|
|
2331
|
+
for (const pat of sourcePatterns) {
|
|
2332
|
+
if (pathname.includes(pat))
|
|
2333
|
+
return true;
|
|
2334
|
+
}
|
|
2335
|
+
// 5. Accept header. A genuine browser navigation always includes `text/html`.
|
|
2336
|
+
// fetch()'s default '*/*' indicates a programmatic request — treat as module.
|
|
2337
|
+
const accept = request.headers.get('Accept') || '';
|
|
2338
|
+
if (accept.includes('text/html'))
|
|
2339
|
+
return false;
|
|
2340
|
+
if (accept === '*/*' || accept === '')
|
|
2341
|
+
return true;
|
|
2342
|
+
if (accept.includes('application/javascript') || accept.includes('application/json'))
|
|
2343
|
+
return true;
|
|
2344
|
+
if (accept.includes('text/css'))
|
|
2345
|
+
return true;
|
|
2346
|
+
return false;
|
|
2347
|
+
}
|
|
2348
|
+
async serveFile(request, pathname, query, headers) {
|
|
2349
|
+
let vfsPath = this.root + pathname;
|
|
2350
|
+
// If the exact path doesn't exist, try Vite-style extension resolution.
|
|
2351
|
+
// This is essential for ES module imports like `import X from "./foo"` —
|
|
2352
|
+
// the browser fetches /preview/src/foo with no extension, and we need to
|
|
2353
|
+
// try .tsx/.ts/.jsx/.js/.mjs/.cjs/.json and directory index files.
|
|
2354
|
+
if (!this.vfs.exists(vfsPath) || (this.vfs.isDirectory(vfsPath) && !pathname.endsWith('/'))) {
|
|
2355
|
+
const resolved = this.resolveFileCandidate(pathname);
|
|
2356
|
+
if (resolved) {
|
|
2357
|
+
vfsPath = resolved.vfsPath;
|
|
2358
|
+
pathname = resolved.pathname;
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
// Nothing matched. Only fall back to index.html for true navigation
|
|
2362
|
+
// requests — never for module/asset/style fetches (which would get a
|
|
2363
|
+
// MIME-type error in the browser and break everything).
|
|
2364
|
+
if (this.isModuleRequest(request, pathname, query)) {
|
|
2365
|
+
return new Response('404 Not Found: ' + pathname, {
|
|
2366
|
+
status: 404, headers: { ...headers, 'Content-Type': 'text/plain' },
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
return this.serveIndexHtml(headers);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
if (this.vfs.isDirectory(vfsPath)) {
|
|
2373
|
+
const indexPath = vfsPath + '/index.html';
|
|
2374
|
+
if (this.vfs.exists(indexPath)) {
|
|
2375
|
+
let html = this.vfs.readFileString(indexPath);
|
|
2376
|
+
if (html.includes('</head>')) {
|
|
2377
|
+
html = html.replace('</head>', ERROR_OVERLAY_CLIENT + '\n' + HMR_CLIENT + '\n</head>');
|
|
2378
|
+
}
|
|
2379
|
+
html = this.rewriteHtmlPaths(html);
|
|
2380
|
+
return new Response(html, {
|
|
2381
|
+
headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8' },
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
return new Response('403 Directory listing not supported', {
|
|
2385
|
+
status: 403, headers,
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
// Extract extension using slash-aware dot detection. Naive `split('.').pop()`
|
|
2389
|
+
// returns garbage for paths with dotted directories like `/src/v1.0/App`
|
|
2390
|
+
// (would produce `ext = '.0/app'`). We only accept a dot that occurs AFTER
|
|
2391
|
+
// the final slash and has at least one character after it.
|
|
2392
|
+
const pExtLastSlash = pathname.lastIndexOf('/');
|
|
2393
|
+
const pExtLastDot = pathname.lastIndexOf('.');
|
|
2394
|
+
const ext = (pExtLastDot > pExtLastSlash && pExtLastDot < pathname.length - 1)
|
|
2395
|
+
? pathname.substring(pExtLastDot).toLowerCase()
|
|
2396
|
+
: '';
|
|
2397
|
+
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
2398
|
+
// Transform TS/TSX/JSX/MTS/CTS files
|
|
2399
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.jsx' || ext === '.mts' || ext === '.cts') {
|
|
2400
|
+
return this.serveTransformed(vfsPath, ext, headers);
|
|
2401
|
+
}
|
|
2402
|
+
// CSS handling
|
|
2403
|
+
if (ext === '.css') {
|
|
2404
|
+
let css = this.vfs.readFileString(vfsPath);
|
|
2405
|
+
// Resolve @import "./other.css" (but pass through @import url(...) for Google Fonts etc.)
|
|
2406
|
+
const cssDir = vfsPath.includes('/') ? vfsPath.substring(0, vfsPath.lastIndexOf('/')) : this.root;
|
|
2407
|
+
css = css.replace(/@import\s+["']([^"']+)["']\s*;/g, (_match, importPath) => {
|
|
2408
|
+
if (importPath.startsWith('http'))
|
|
2409
|
+
return _match;
|
|
2410
|
+
const resolvedPath = importPath.startsWith('/')
|
|
2411
|
+
? this.root + importPath
|
|
2412
|
+
: cssDir + '/' + importPath;
|
|
2413
|
+
try {
|
|
2414
|
+
return this.vfs.readFileString(resolvedPath);
|
|
2415
|
+
}
|
|
2416
|
+
catch {
|
|
2417
|
+
return `/* @import "${importPath}" not found */`;
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
// Process Tailwind CSS directives if project uses Tailwind
|
|
2421
|
+
if (this.hasTailwind) {
|
|
2422
|
+
css = processTailwindCss(css);
|
|
2423
|
+
}
|
|
2424
|
+
// CSS-as-JS module: if ?import query, wrap in JS that injects <style>
|
|
2425
|
+
if (query.includes('import')) {
|
|
2426
|
+
const escapedCss = css.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
2427
|
+
const jsCode = `(function() {
|
|
2428
|
+
var id = ${JSON.stringify(this.basePath + pathname)};
|
|
2429
|
+
var existing = document.querySelector('style[data-vite-dev-id="' + id + '"]');
|
|
2430
|
+
if (existing) { existing.textContent = \`${escapedCss}\`; return; }
|
|
2431
|
+
var s = document.createElement('style');
|
|
2432
|
+
s.setAttribute('data-vite-dev-id', id);
|
|
2433
|
+
s.textContent = \`${escapedCss}\`;
|
|
2434
|
+
document.head.appendChild(s);
|
|
2435
|
+
})();`;
|
|
2436
|
+
return new Response(jsCode, {
|
|
2437
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return new Response(css, {
|
|
2441
|
+
headers: { ...headers, 'Content-Type': 'text/css; charset=utf-8' },
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
// JSON files: serve as ES module when imported from JS
|
|
2445
|
+
if (ext === '.json') {
|
|
2446
|
+
const accept = request.headers.get('Accept') || '';
|
|
2447
|
+
// If requested as a module (via import), wrap as ES module
|
|
2448
|
+
if (accept.includes('application/javascript') || query.includes('import')) {
|
|
2449
|
+
const json = this.vfs.readFileString(vfsPath);
|
|
2450
|
+
const code = `export default ${json};`;
|
|
2451
|
+
return new Response(code, {
|
|
2452
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
// Otherwise serve as plain JSON
|
|
2456
|
+
const data = this.vfs.readFile(vfsPath);
|
|
2457
|
+
return new Response(data, {
|
|
2458
|
+
headers: { ...headers, 'Content-Type': contentType },
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
// JS files: rewrite imports (cached)
|
|
2462
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
2463
|
+
const cached = this.moduleCache.get(vfsPath);
|
|
2464
|
+
if (cached) {
|
|
2465
|
+
return new Response(cached.code, {
|
|
2466
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
let code = this.vfs.readFileString(vfsPath);
|
|
2470
|
+
if (!this.hasImportmap) {
|
|
2471
|
+
// Pass importer context so `#X` subpath imports in this user JS
|
|
2472
|
+
// file resolve against its owning package.json.
|
|
2473
|
+
const importerCtx = {
|
|
2474
|
+
importerVfsPath: vfsPath,
|
|
2475
|
+
root: this.root,
|
|
2476
|
+
vfs: this.vfs,
|
|
2477
|
+
};
|
|
2478
|
+
code = rewriteAllImports(code, this.aliases, this.basePath, importerCtx);
|
|
2479
|
+
}
|
|
2480
|
+
this.moduleCache.set(vfsPath, { code, timestamp: Date.now() });
|
|
2481
|
+
return new Response(code, {
|
|
2482
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
// Asset imports from JS: serve as URL-exporting module
|
|
2486
|
+
if (ASSET_EXTS.has(ext) && query.includes('import')) {
|
|
2487
|
+
const url = this.basePath + pathname;
|
|
2488
|
+
const code = `export default ${JSON.stringify(url)};`;
|
|
2489
|
+
return new Response(code, {
|
|
2490
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
// Other files: serve as-is (binary)
|
|
2494
|
+
const data = this.vfs.readFile(vfsPath);
|
|
2495
|
+
return new Response(data, { headers: { ...headers, 'Content-Type': contentType } });
|
|
2496
|
+
}
|
|
2497
|
+
// ── TS/TSX/JSX transform ──────────────────────────────────────────────
|
|
2498
|
+
async serveTransformed(vfsPath, ext, headers) {
|
|
2499
|
+
const cached = this.moduleCache.get(vfsPath);
|
|
2500
|
+
if (cached) {
|
|
2501
|
+
return new Response(cached.code, {
|
|
2502
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
let code = this.vfs.readFileString(vfsPath);
|
|
2506
|
+
// Auto-inject React Router `basename` into entry files so user links
|
|
2507
|
+
// like <NavLink to="/x"> correctly resolve to `${basePath}/x`. Safe
|
|
2508
|
+
// no-op if the file doesn't reference createBrowserRouter/<BrowserRouter>
|
|
2509
|
+
// or the user has opted out (explicit basename / line-leading comment /
|
|
2510
|
+
// vite.config.ts nimbusInjectBasename: false).
|
|
2511
|
+
if (this.injectBasename && shouldProcessForRouter(vfsPath)) {
|
|
2512
|
+
try {
|
|
2513
|
+
code = injectRouterBasename(code, this.basePath);
|
|
2514
|
+
}
|
|
2515
|
+
catch (e) {
|
|
2516
|
+
// Never let the transform break serving — log and continue with original.
|
|
2517
|
+
console.warn('[vite-dev] basename injection skipped for', vfsPath, ':', e?.message);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
const loader = ext === '.tsx' ? 'tsx' : ext === '.jsx' ? 'jsx' : 'ts';
|
|
2521
|
+
try {
|
|
2522
|
+
const hasPreact = code.includes('from "preact"') || code.includes("from 'preact'") || code.includes('from "preact/');
|
|
2523
|
+
const jsxOpts = hasPreact
|
|
2524
|
+
? { jsx: 'transform', jsxFactory: 'h', jsxFragment: 'Fragment' }
|
|
2525
|
+
: { jsx: 'automatic' };
|
|
2526
|
+
const result = await this.esbuild.transform(code, {
|
|
2527
|
+
loader,
|
|
2528
|
+
format: 'esm',
|
|
2529
|
+
target: 'esnext',
|
|
2530
|
+
...jsxOpts,
|
|
2531
|
+
define: this.define,
|
|
2532
|
+
sourcemap: 'inline',
|
|
2533
|
+
});
|
|
2534
|
+
code = result.code;
|
|
2535
|
+
}
|
|
2536
|
+
catch (e) {
|
|
2537
|
+
const errMsg = (e?.message || String(e)).replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
2538
|
+
code = `
|
|
2539
|
+
console.error(\`[nimbus-vite] Transform error:\\n${errMsg}\`);
|
|
2540
|
+
if (!document.getElementById('nimbus-error-overlay')) {
|
|
2541
|
+
const d = document.createElement('div');
|
|
2542
|
+
d.id = 'nimbus-error-overlay';
|
|
2543
|
+
d.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#ff6b6b;font-family:monospace;padding:32px;overflow:auto;white-space:pre-wrap;font-size:14px;line-height:1.6';
|
|
2544
|
+
d.innerHTML = '<div style="max-width:800px;margin:0 auto"><h2 style="color:#ff6b6b;margin-bottom:16px">Transform Error</h2><pre style="color:#e4e4e7;background:#1a1a2e;padding:16px;border-radius:8px;overflow-x:auto">' + \`${errMsg}\`.replace(/</g,'<') + '</pre><p style="color:#666;margin-top:16px">Fix the error and save. The page will reload.</p></div>';
|
|
2545
|
+
d.onclick = () => d.remove();
|
|
2546
|
+
document.body.appendChild(d);
|
|
2547
|
+
}\n`;
|
|
2548
|
+
return new Response(code, {
|
|
2549
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
// Rewrite all imports: CSS ?import, aliases, bare → /@modules/,
|
|
2553
|
+
// dynamic, AND `package.json#imports` (subpath imports `#X`).
|
|
2554
|
+
// Pass the importer context so `#X` resolves against the user's
|
|
2555
|
+
// owning package.json — the rare-but-real case where a user .ts/
|
|
2556
|
+
// .tsx file directly uses a Node subpath import.
|
|
2557
|
+
if (!this.hasImportmap) {
|
|
2558
|
+
const importerCtx = {
|
|
2559
|
+
importerVfsPath: vfsPath,
|
|
2560
|
+
root: this.root,
|
|
2561
|
+
vfs: this.vfs,
|
|
2562
|
+
};
|
|
2563
|
+
code = rewriteAllImports(code, this.aliases, this.basePath, importerCtx);
|
|
2564
|
+
}
|
|
2565
|
+
this.moduleCache.set(vfsPath, { code, timestamp: Date.now() });
|
|
2566
|
+
return new Response(code, {
|
|
2567
|
+
headers: { ...headers, 'Content-Type': 'application/javascript; charset=utf-8' },
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
get stats() {
|
|
2571
|
+
return {
|
|
2572
|
+
running: this.running,
|
|
2573
|
+
port: this.port,
|
|
2574
|
+
root: this.root,
|
|
2575
|
+
cachedModules: this.moduleCache.size,
|
|
2576
|
+
hasTailwind: this.hasTailwind,
|
|
2577
|
+
aliases: Object.keys(this.aliases),
|
|
2578
|
+
};
|
|
2579
|
+
}
|
|
2580
|
+
}
|