devflare 1.0.0-next.5 → 1.0.0-next.50
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/LICENSE +21 -0
- package/LLM.md +17312 -2139
- package/README.md +394 -605
- package/bin/devflare.js +30 -14
- package/dist/_chunks/account-BI3WZMDy.js +462 -0
- package/dist/_chunks/account-resources-CsmVowJp.js +159 -0
- package/dist/_chunks/ai-CBEuLdoP.js +322 -0
- package/dist/_chunks/api-TzdliH-6.js +568 -0
- package/dist/_chunks/build-artifacts-DU5V0J0Z.js +483 -0
- package/dist/_chunks/build-e0KR38-4.js +24 -0
- package/dist/_chunks/cli-DuUz10Mn.js +1163 -0
- package/dist/_chunks/cloudflare-BaloqI3H.js +157 -0
- package/dist/_chunks/colors-DiK6wPJ5.js +13 -0
- package/dist/_chunks/command-utils-DA0Qx9_K.js +45 -0
- package/dist/_chunks/compiler-Zw16C9Pi.js +489 -0
- package/dist/_chunks/config-Brju62xy.js +78 -0
- package/dist/_chunks/config-path-DaL1ylZ_.js +42 -0
- package/dist/_chunks/context-CX50Y2Kb.js +2250 -0
- package/dist/_chunks/decorators-QmV57ixr.js +19 -0
- package/dist/_chunks/dependencies-C2oxFjU_.js +57 -0
- package/dist/_chunks/deploy-B-Kku1Vk.js +1032 -0
- package/dist/_chunks/dev-D4e_RRZn.js +3471 -0
- package/dist/_chunks/doctor-jm_K4dxL.js +219 -0
- package/dist/_chunks/durable-object-DMtH0XYX.js +234 -0
- package/dist/_chunks/env-S0_nMVz1.js +125 -0
- package/dist/_chunks/family-_9-mGSfO.js +143 -0
- package/dist/_chunks/generated-artifacts-dMJY4KAt.js +32 -0
- package/dist/_chunks/glob-CmQOvunB.js +56 -0
- package/dist/_chunks/init-DnkJOmpW.js +165 -0
- package/dist/_chunks/loader-Cr0_OWPy.js +1288 -0
- package/dist/_chunks/local-dev-vars-CTSa-wvF.js +81 -0
- package/dist/_chunks/local-hyperdrive-CJ90j46O.js +119 -0
- package/dist/_chunks/local-media-bindings-DsAW4AMF.js +335 -0
- package/dist/_chunks/local-secrets-DMPkmOn3.js +123 -0
- package/dist/_chunks/local-workflow-entrypoints-Cq_WJajd.js +90 -0
- package/dist/_chunks/login-FjTAAQbi.js +51 -0
- package/dist/_chunks/package-metadata-DjHBLB-z.js +39 -0
- package/dist/_chunks/preferences-BKp_7XJx.js +241 -0
- package/dist/_chunks/preview-C3Cdr8an.js +462 -0
- package/dist/_chunks/preview-registry-ClAbLTHH.js +1186 -0
- package/dist/_chunks/preview-resources-CGMBjsYB.js +359 -0
- package/dist/_chunks/previews-C7hs9-BT.js +1134 -0
- package/dist/_chunks/productions-T9Als4fE.js +411 -0
- package/dist/_chunks/ref-CYJNAAe_.js +178 -0
- package/dist/_chunks/remote-CZnP-HLJ.js +79 -0
- package/dist/_chunks/remote-config-D39BY9ji.js +132 -0
- package/dist/_chunks/resolve-package-qizBfCIm.js +152 -0
- package/dist/_chunks/rolldown-runtime-CvQ6eiug.js +14 -0
- package/dist/_chunks/routes-BFYCqHb-.js +833 -0
- package/dist/_chunks/runtime-DL-Nz3k8.js +550 -0
- package/dist/_chunks/schema-normalization-BCwTLTGp.js +660 -0
- package/dist/_chunks/secrets-DIpJGe6C.js +95 -0
- package/dist/_chunks/tail-IY-ijYmv.js +239 -0
- package/dist/_chunks/token-Dmi0KXyX.js +333 -0
- package/dist/_chunks/tokens-iVcgNXi-.js +221 -0
- package/dist/_chunks/types-DoKsMYfm.js +484 -0
- package/dist/_chunks/ui-BUoZApvE.js +103 -0
- package/dist/_chunks/usage-BBTGzjon.js +272 -0
- package/dist/_chunks/vite-CGtVNf5E.js +1267 -0
- package/dist/_chunks/vite-utils-OVLMV605.js +183 -0
- package/dist/_chunks/worker-BEZI4od1.js +410 -0
- package/dist/_chunks/worker-bundler-dTTnYU3n.js +458 -0
- package/dist/_chunks/worker-entrypoint-CQW77lG8.js +247 -0
- package/dist/_chunks/workerName-CFJsLZA-.js +23 -0
- package/dist/bridge/client.d.ts +49 -7
- package/dist/bridge/client.d.ts.map +1 -1
- package/dist/bridge/gateway-runtime.d.ts +8 -0
- package/dist/bridge/gateway-runtime.d.ts.map +1 -0
- package/dist/bridge/index.d.ts +6 -6
- package/dist/bridge/index.d.ts.map +1 -1
- package/dist/bridge/log.d.ts +5 -0
- package/dist/bridge/log.d.ts.map +1 -0
- package/dist/bridge/miniflare.d.ts +82 -2
- package/dist/bridge/miniflare.d.ts.map +1 -1
- package/dist/bridge/proxy.d.ts +12 -11
- package/dist/bridge/proxy.d.ts.map +1 -1
- package/dist/bridge/server.d.ts +2 -1
- package/dist/bridge/server.d.ts.map +1 -1
- package/dist/bridge/v2/body-streams.d.ts +53 -0
- package/dist/bridge/v2/body-streams.d.ts.map +1 -0
- package/dist/bridge/v2/codec.d.ts +106 -0
- package/dist/bridge/v2/codec.d.ts.map +1 -0
- package/dist/bridge/v2/control-messages.d.ts +46 -0
- package/dist/bridge/v2/control-messages.d.ts.map +1 -0
- package/dist/bridge/v2/frames.d.ts +109 -0
- package/dist/bridge/v2/frames.d.ts.map +1 -0
- package/dist/bridge/v2/index.d.ts +11 -0
- package/dist/bridge/v2/index.d.ts.map +1 -0
- package/dist/bridge/v2/serialization.d.ts +48 -0
- package/dist/bridge/v2/serialization.d.ts.map +1 -0
- package/dist/bridge/v2/transport.d.ts +31 -0
- package/dist/bridge/v2/transport.d.ts.map +1 -0
- package/dist/bridge/v2/value-codec.d.ts +61 -0
- package/dist/bridge/v2/value-codec.d.ts.map +1 -0
- package/dist/bridge/{serialization.d.ts → v2/value-serialization.d.ts} +40 -18
- package/dist/bridge/v2/value-serialization.d.ts.map +1 -0
- package/dist/bridge/{protocol.d.ts → v2/wire.d.ts} +32 -8
- package/dist/bridge/v2/wire.d.ts.map +1 -0
- package/dist/bridge/v2/ws-relay.d.ts +53 -0
- package/dist/bridge/v2/ws-relay.d.ts.map +1 -0
- package/dist/browser-shim/handler.d.ts +2 -2
- package/dist/browser-shim/handler.d.ts.map +1 -1
- package/dist/browser-shim/index.d.ts +2 -2
- package/dist/browser-shim/server.d.ts +63 -0
- package/dist/browser-shim/server.d.ts.map +1 -1
- package/dist/browser.d.ts +5411 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +80 -0
- package/dist/bundler/defaults.d.ts +23 -0
- package/dist/bundler/defaults.d.ts.map +1 -0
- package/dist/bundler/do-bundler.d.ts +7 -0
- package/dist/bundler/do-bundler.d.ts.map +1 -1
- package/dist/bundler/index.d.ts +3 -1
- package/dist/bundler/index.d.ts.map +1 -1
- package/dist/bundler/rolldown-shared.d.ts +53 -0
- package/dist/bundler/rolldown-shared.d.ts.map +1 -0
- package/dist/bundler/worker-bundler.d.ts +13 -0
- package/dist/bundler/worker-bundler.d.ts.map +1 -0
- package/dist/bundler/worker-compat.d.ts +4 -0
- package/dist/bundler/worker-compat.d.ts.map +1 -0
- package/dist/cli/build-manifest.d.ts +66 -0
- package/dist/cli/build-manifest.d.ts.map +1 -0
- package/dist/cli/command-utils.d.ts +18 -0
- package/dist/cli/command-utils.d.ts.map +1 -0
- package/dist/cli/commands/account.d.ts +2 -2
- package/dist/cli/commands/account.d.ts.map +1 -1
- package/dist/cli/commands/ai.d.ts +1 -1
- package/dist/cli/commands/ai.d.ts.map +1 -1
- package/dist/cli/commands/build-artifacts.d.ts +40 -0
- package/dist/cli/commands/build-artifacts.d.ts.map +1 -0
- package/dist/cli/commands/build.d.ts +2 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/config.d.ts +4 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/deploy/metadata.d.ts +18 -0
- package/dist/cli/commands/deploy/metadata.d.ts.map +1 -0
- package/dist/cli/commands/deploy/prepare.d.ts +23 -0
- package/dist/cli/commands/deploy/prepare.d.ts.map +1 -0
- package/dist/cli/commands/deploy/runtime.d.ts +4 -0
- package/dist/cli/commands/deploy/runtime.d.ts.map +1 -0
- package/dist/cli/commands/deploy/verification.d.ts +36 -0
- package/dist/cli/commands/deploy/verification.d.ts.map +1 -0
- package/dist/cli/commands/deploy.d.ts +2 -2
- package/dist/cli/commands/deploy.d.ts.map +1 -1
- package/dist/cli/commands/dev.d.ts +9 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -2
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/login.d.ts +4 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/previews-support/cleanup.d.ts +9 -0
- package/dist/cli/commands/previews-support/cleanup.d.ts.map +1 -0
- package/dist/cli/commands/previews-support/family.d.ts +10 -0
- package/dist/cli/commands/previews-support/family.d.ts.map +1 -0
- package/dist/cli/commands/previews-support/render.d.ts +8 -0
- package/dist/cli/commands/previews-support/render.d.ts.map +1 -0
- package/dist/cli/commands/previews-support/theme.d.ts +10 -0
- package/dist/cli/commands/previews-support/theme.d.ts.map +1 -0
- package/dist/cli/commands/previews-support/types.d.ts +70 -0
- package/dist/cli/commands/previews-support/types.d.ts.map +1 -0
- package/dist/cli/commands/previews.d.ts +4 -0
- package/dist/cli/commands/previews.d.ts.map +1 -0
- package/dist/cli/commands/productions.d.ts +4 -0
- package/dist/cli/commands/productions.d.ts.map +1 -0
- package/dist/cli/commands/remote.d.ts +1 -1
- package/dist/cli/commands/remote.d.ts.map +1 -1
- package/dist/cli/commands/secrets.d.ts +4 -0
- package/dist/cli/commands/secrets.d.ts.map +1 -0
- package/dist/cli/commands/tail.d.ts +44 -0
- package/dist/cli/commands/tail.d.ts.map +1 -0
- package/dist/cli/commands/token.d.ts +4 -0
- package/dist/cli/commands/token.d.ts.map +1 -0
- package/dist/cli/commands/type-generation/discovery.d.ts +7 -0
- package/dist/cli/commands/type-generation/discovery.d.ts.map +1 -0
- package/dist/cli/commands/type-generation/generator.d.ts +121 -0
- package/dist/cli/commands/type-generation/generator.d.ts.map +1 -0
- package/dist/cli/commands/type-generation/models.d.ts +27 -0
- package/dist/cli/commands/type-generation/models.d.ts.map +1 -0
- package/dist/cli/commands/types.d.ts +2 -2
- package/dist/cli/commands/types.d.ts.map +1 -1
- package/dist/cli/commands/worker.d.ts +4 -0
- package/dist/cli/commands/worker.d.ts.map +1 -0
- package/dist/cli/config-path.d.ts +6 -0
- package/dist/cli/config-path.d.ts.map +1 -0
- package/dist/cli/dependencies.d.ts +2 -1
- package/dist/cli/dependencies.d.ts.map +1 -1
- package/dist/cli/deploy-strategy.d.ts +17 -0
- package/dist/cli/deploy-strategy.d.ts.map +1 -0
- package/dist/cli/deploy-target.d.ts +17 -0
- package/dist/cli/deploy-target.d.ts.map +1 -0
- package/dist/cli/generated-artifacts.d.ts +12 -0
- package/dist/cli/generated-artifacts.d.ts.map +1 -0
- package/dist/cli/gradual-deploy.d.ts +49 -0
- package/dist/cli/gradual-deploy.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/account.d.ts +3 -0
- package/dist/cli/help-pages/pages/account.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/core.d.ts +4 -0
- package/dist/cli/help-pages/pages/core.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/index.d.ts +3 -0
- package/dist/cli/help-pages/pages/index.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/misc.d.ts +3 -0
- package/dist/cli/help-pages/pages/misc.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/previews.d.ts +3 -0
- package/dist/cli/help-pages/pages/previews.d.ts.map +1 -0
- package/dist/cli/help-pages/pages/productions.d.ts +3 -0
- package/dist/cli/help-pages/pages/productions.d.ts.map +1 -0
- package/dist/cli/help-pages/render.d.ts +12 -0
- package/dist/cli/help-pages/render.d.ts.map +1 -0
- package/dist/cli/help-pages/shared.d.ts +15 -0
- package/dist/cli/help-pages/shared.d.ts.map +1 -0
- package/dist/cli/help-pages/types.d.ts +23 -0
- package/dist/cli/help-pages/types.d.ts.map +1 -0
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/package-metadata.d.ts +16 -0
- package/dist/cli/package-metadata.d.ts.map +1 -0
- package/dist/cli/preview-bindings.d.ts +49 -0
- package/dist/cli/preview-bindings.d.ts.map +1 -0
- package/dist/cli/preview.d.ts +10 -0
- package/dist/cli/preview.d.ts.map +1 -0
- package/dist/cli/ui.d.ts +37 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/workspace-build-guard.d.ts +14 -0
- package/dist/cli/workspace-build-guard.d.ts.map +1 -0
- package/dist/cli/wrangler-auth.d.ts +1 -1
- package/dist/cloudflare/account-core.d.ts +6 -0
- package/dist/cloudflare/account-core.d.ts.map +1 -0
- package/dist/cloudflare/account-resources.d.ts +40 -0
- package/dist/cloudflare/account-resources.d.ts.map +1 -0
- package/dist/cloudflare/account-status.d.ts +11 -0
- package/dist/cloudflare/account-status.d.ts.map +1 -0
- package/dist/cloudflare/account-workers.d.ts +14 -0
- package/dist/cloudflare/account-workers.d.ts.map +1 -0
- package/dist/cloudflare/account.d.ts +7 -64
- package/dist/cloudflare/account.d.ts.map +1 -1
- package/dist/cloudflare/api.d.ts +71 -3
- package/dist/cloudflare/api.d.ts.map +1 -1
- package/dist/cloudflare/auth.d.ts +1 -1
- package/dist/cloudflare/auth.d.ts.map +1 -1
- package/dist/cloudflare/index.d.ts +61 -6
- package/dist/cloudflare/index.d.ts.map +1 -1
- package/dist/cloudflare/index.js +4 -13
- package/dist/cloudflare/known-permission-group-ids.generated.d.ts +10 -0
- package/dist/cloudflare/known-permission-group-ids.generated.d.ts.map +1 -0
- package/dist/cloudflare/kv-namespace.d.ts +4 -0
- package/dist/cloudflare/kv-namespace.d.ts.map +1 -0
- package/dist/cloudflare/preferences.d.ts +7 -0
- package/dist/cloudflare/preferences.d.ts.map +1 -1
- package/dist/cloudflare/preview-registry-cache.d.ts +6 -0
- package/dist/cloudflare/preview-registry-cache.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-inference.d.ts +14 -0
- package/dist/cloudflare/preview-registry-inference.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-records.d.ts +4 -0
- package/dist/cloudflare/preview-registry-records.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-shape.d.ts +47 -0
- package/dist/cloudflare/preview-registry-shape.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-store.d.ts +14 -0
- package/dist/cloudflare/preview-registry-store.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-transport.d.ts +4 -0
- package/dist/cloudflare/preview-registry-transport.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry-types.d.ts +103 -0
- package/dist/cloudflare/preview-registry-types.d.ts.map +1 -0
- package/dist/cloudflare/preview-registry.d.ts +42 -0
- package/dist/cloudflare/preview-registry.d.ts.map +1 -0
- package/dist/cloudflare/preview-urls.d.ts +3 -0
- package/dist/cloudflare/preview-urls.d.ts.map +1 -0
- package/dist/cloudflare/registry-schema.d.ts +253 -0
- package/dist/cloudflare/registry-schema.d.ts.map +1 -0
- package/dist/cloudflare/remote-config.d.ts.map +1 -1
- package/dist/cloudflare/tokens.d.ts +44 -0
- package/dist/cloudflare/tokens.d.ts.map +1 -0
- package/dist/cloudflare/types.d.ts +124 -7
- package/dist/cloudflare/types.d.ts.map +1 -1
- package/dist/cloudflare/usage.d.ts +31 -3
- package/dist/cloudflare/usage.d.ts.map +1 -1
- package/dist/config/binding-resolution-helpers.d.ts +41 -0
- package/dist/config/binding-resolution-helpers.d.ts.map +1 -0
- package/dist/config/compatibility.d.ts +3 -0
- package/dist/config/compatibility.d.ts.map +1 -0
- package/dist/config/compiler/bindings.d.ts +14 -0
- package/dist/config/compiler/bindings.d.ts.map +1 -0
- package/dist/config/compiler/core-helpers.d.ts +6 -0
- package/dist/config/compiler/core-helpers.d.ts.map +1 -0
- package/dist/config/compiler/do-workers.d.ts +34 -0
- package/dist/config/compiler/do-workers.d.ts.map +1 -0
- package/dist/config/compiler/paths.d.ts +18 -0
- package/dist/config/compiler/paths.d.ts.map +1 -0
- package/dist/config/compiler/types.d.ts +298 -0
- package/dist/config/compiler/types.d.ts.map +1 -0
- package/dist/config/compiler.d.ts +22 -134
- package/dist/config/compiler.d.ts.map +1 -1
- package/dist/config/cron.d.ts +19 -0
- package/dist/config/cron.d.ts.map +1 -0
- package/dist/config/define.d.ts +8 -5
- package/dist/config/define.d.ts.map +1 -1
- package/dist/config/deploy-resources.d.ts +58 -0
- package/dist/config/deploy-resources.d.ts.map +1 -0
- package/dist/config/env-vars.d.ts +309 -0
- package/dist/config/env-vars.d.ts.map +1 -0
- package/dist/config/framework-providers.d.ts +9 -0
- package/dist/config/framework-providers.d.ts.map +1 -0
- package/dist/config/index.d.ts +12 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/loader.d.ts +2 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/local-dev-vars.d.ts +15 -0
- package/dist/config/local-dev-vars.d.ts.map +1 -0
- package/dist/config/preview-resources.d.ts +83 -0
- package/dist/config/preview-resources.d.ts.map +1 -0
- package/dist/config/preview.d.ts +29 -0
- package/dist/config/preview.d.ts.map +1 -0
- package/dist/config/ref.d.ts +16 -25
- package/dist/config/ref.d.ts.map +1 -1
- package/dist/config/resolve-phased.d.ts +72 -0
- package/dist/config/resolve-phased.d.ts.map +1 -0
- package/dist/config/resolve.d.ts +11 -0
- package/dist/config/resolve.d.ts.map +1 -0
- package/dist/config/resource-resolution.d.ts +80 -0
- package/dist/config/resource-resolution.d.ts.map +1 -0
- package/dist/config/schema-bindings.d.ts +2025 -0
- package/dist/config/schema-bindings.d.ts.map +1 -0
- package/dist/config/schema-build.d.ts +67 -0
- package/dist/config/schema-build.d.ts.map +1 -0
- package/dist/config/schema-env.d.ts +3579 -0
- package/dist/config/schema-env.d.ts.map +1 -0
- package/dist/config/schema-normalization.d.ts +265 -0
- package/dist/config/schema-normalization.d.ts.map +1 -0
- package/dist/config/schema-runtime.d.ts +508 -0
- package/dist/config/schema-runtime.d.ts.map +1 -0
- package/dist/config/schema-types-bindings-platform.d.ts +508 -0
- package/dist/config/schema-types-bindings-platform.d.ts.map +1 -0
- package/dist/config/schema-types-bindings-resources.d.ts +767 -0
- package/dist/config/schema-types-bindings-resources.d.ts.map +1 -0
- package/dist/config/schema-types-bindings.d.ts +293 -0
- package/dist/config/schema-types-bindings.d.ts.map +1 -0
- package/dist/config/schema-types-build.d.ts +86 -0
- package/dist/config/schema-types-build.d.ts.map +1 -0
- package/dist/config/schema-types-runtime.d.ts +917 -0
- package/dist/config/schema-types-runtime.d.ts.map +1 -0
- package/dist/config/schema-types.d.ts +424 -0
- package/dist/config/schema-types.d.ts.map +1 -0
- package/dist/config/schema.d.ts +5324 -2011
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/service-bindings-validation.d.ts +36 -0
- package/dist/config/service-bindings-validation.d.ts.map +1 -0
- package/dist/config-entry.d.ts +7 -0
- package/dist/config-entry.d.ts.map +1 -0
- package/dist/config-entry.js +3 -0
- package/dist/decorators/durable-object.d.ts.map +1 -1
- package/dist/decorators/index.d.ts +2 -2
- package/dist/decorators/index.js +2 -9
- package/dist/dev-server/d1-migrations.d.ts +36 -0
- package/dist/dev-server/d1-migrations.d.ts.map +1 -0
- package/dist/dev-server/dev-server-state.d.ts +59 -0
- package/dist/dev-server/dev-server-state.d.ts.map +1 -0
- package/dist/dev-server/gateway-script.d.ts +21 -0
- package/dist/dev-server/gateway-script.d.ts.map +1 -0
- package/dist/dev-server/index.d.ts +1 -1
- package/dist/dev-server/miniflare-bindings.d.ts +81 -0
- package/dist/dev-server/miniflare-bindings.d.ts.map +1 -0
- package/dist/dev-server/miniflare-dev-config.d.ts +36 -0
- package/dist/dev-server/miniflare-dev-config.d.ts.map +1 -0
- package/dist/dev-server/miniflare-log.d.ts +20 -0
- package/dist/dev-server/miniflare-log.d.ts.map +1 -0
- package/dist/dev-server/miniflare-worker-config.d.ts +82 -0
- package/dist/dev-server/miniflare-worker-config.d.ts.map +1 -0
- package/dist/dev-server/reload-queue.d.ts +13 -0
- package/dist/dev-server/reload-queue.d.ts.map +1 -0
- package/dist/dev-server/runtime-stdio.d.ts +8 -0
- package/dist/dev-server/runtime-stdio.d.ts.map +1 -0
- package/dist/dev-server/server-startup-helpers.d.ts +84 -0
- package/dist/dev-server/server-startup-helpers.d.ts.map +1 -0
- package/dist/dev-server/server.d.ts +2 -0
- package/dist/dev-server/server.d.ts.map +1 -1
- package/dist/dev-server/vite-process.d.ts +15 -0
- package/dist/dev-server/vite-process.d.ts.map +1 -0
- package/dist/dev-server/vite-utils.d.ts +19 -1
- package/dist/dev-server/vite-utils.d.ts.map +1 -1
- package/dist/dev-server/worker-source-watcher.d.ts +17 -0
- package/dist/dev-server/worker-source-watcher.d.ts.map +1 -0
- package/dist/dev-server/worker-surface-paths.d.ts +6 -0
- package/dist/dev-server/worker-surface-paths.d.ts.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/index.d.ts +8 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -298
- package/dist/runtime/context-events.d.ts +13 -0
- package/dist/runtime/context-events.d.ts.map +1 -0
- package/dist/runtime/context-types.d.ts +82 -0
- package/dist/runtime/context-types.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +20 -44
- package/dist/runtime/context.d.ts.map +1 -1
- package/dist/runtime/exports.d.ts +70 -55
- package/dist/runtime/exports.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +10 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +5 -111
- package/dist/runtime/middleware.d.ts +119 -67
- package/dist/runtime/middleware.d.ts.map +1 -1
- package/dist/runtime/router/index.d.ts +7 -0
- package/dist/runtime/router/index.d.ts.map +1 -0
- package/dist/runtime/router/types.d.ts +24 -0
- package/dist/runtime/router/types.d.ts.map +1 -0
- package/dist/runtime/validation.d.ts +18 -4
- package/dist/runtime/validation.d.ts.map +1 -1
- package/dist/secrets/local-secrets.d.ts +46 -0
- package/dist/secrets/local-secrets.d.ts.map +1 -0
- package/dist/shims/local-hyperdrive.d.ts +12 -0
- package/dist/shims/local-hyperdrive.d.ts.map +1 -0
- package/dist/shims/local-media-bindings.d.ts +19 -0
- package/dist/shims/local-media-bindings.d.ts.map +1 -0
- package/dist/shims/local-worker-loader.d.ts +3 -0
- package/dist/shims/local-worker-loader.d.ts.map +1 -0
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +313 -167
- package/dist/sveltekit/local-bindings.d.ts +4 -0
- package/dist/sveltekit/local-bindings.d.ts.map +1 -0
- package/dist/sveltekit/platform.d.ts +20 -2
- package/dist/sveltekit/platform.d.ts.map +1 -1
- package/dist/test/ai-search.d.ts +39 -0
- package/dist/test/ai-search.d.ts.map +1 -0
- package/dist/test/alarm.d.ts +55 -0
- package/dist/test/alarm.d.ts.map +1 -0
- package/dist/test/binding-hints.d.ts +10 -0
- package/dist/test/binding-hints.d.ts.map +1 -0
- package/dist/test/cf.d.ts +71 -35
- package/dist/test/cf.d.ts.map +1 -1
- package/dist/test/containers.d.ts +87 -0
- package/dist/test/containers.d.ts.map +1 -0
- package/dist/test/email.d.ts +16 -7
- package/dist/test/email.d.ts.map +1 -1
- package/dist/test/index.d.ts +20 -21
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +4971 -70
- package/dist/test/offline-bindings.d.ts +115 -0
- package/dist/test/offline-bindings.d.ts.map +1 -0
- package/dist/test/queue.d.ts.map +1 -1
- package/dist/test/remote-ai.d.ts.map +1 -1
- package/dist/test/remote-cloudflare.d.ts +13 -0
- package/dist/test/remote-cloudflare.d.ts.map +1 -0
- package/dist/test/remote-vectorize.d.ts.map +1 -1
- package/dist/test/resolve-service-bindings.d.ts +64 -4
- package/dist/test/resolve-service-bindings.d.ts.map +1 -1
- package/dist/test/scheduled.d.ts.map +1 -1
- package/dist/test/should-skip.d.ts +14 -18
- package/dist/test/should-skip.d.ts.map +1 -1
- package/dist/test/simple-context-bindings.d.ts +13 -0
- package/dist/test/simple-context-bindings.d.ts.map +1 -0
- package/dist/test/simple-context-durable-objects.d.ts +6 -0
- package/dist/test/simple-context-durable-objects.d.ts.map +1 -0
- package/dist/test/simple-context-env.d.ts +35 -0
- package/dist/test/simple-context-env.d.ts.map +1 -0
- package/dist/test/simple-context-gateway-script.d.ts +2 -0
- package/dist/test/simple-context-gateway-script.d.ts.map +1 -0
- package/dist/test/simple-context-handlers.d.ts +12 -0
- package/dist/test/simple-context-handlers.d.ts.map +1 -0
- package/dist/test/simple-context-lifecycle.d.ts +32 -0
- package/dist/test/simple-context-lifecycle.d.ts.map +1 -0
- package/dist/test/simple-context-mfconfig.d.ts +10 -0
- package/dist/test/simple-context-mfconfig.d.ts.map +1 -0
- package/dist/test/simple-context-multi-worker.d.ts +16 -0
- package/dist/test/simple-context-multi-worker.d.ts.map +1 -0
- package/dist/test/simple-context-paths.d.ts +40 -0
- package/dist/test/simple-context-paths.d.ts.map +1 -0
- package/dist/test/simple-context-runtime.d.ts +19 -0
- package/dist/test/simple-context-runtime.d.ts.map +1 -0
- package/dist/test/simple-context-startup.d.ts +11 -0
- package/dist/test/simple-context-startup.d.ts.map +1 -0
- package/dist/test/simple-context-transport.d.ts +14 -0
- package/dist/test/simple-context-transport.d.ts.map +1 -0
- package/dist/test/simple-context.d.ts +8 -27
- package/dist/test/simple-context.d.ts.map +1 -1
- package/dist/test/tail.d.ts +3 -2
- package/dist/test/tail.d.ts.map +1 -1
- package/dist/test/utilities/analytics-engine.d.ts +22 -0
- package/dist/test/utilities/analytics-engine.d.ts.map +1 -0
- package/dist/test/utilities/artifacts.d.ts +11 -0
- package/dist/test/utilities/artifacts.d.ts.map +1 -0
- package/dist/test/utilities/context.d.ts +39 -0
- package/dist/test/utilities/context.d.ts.map +1 -0
- package/dist/test/utilities/d1.d.ts +21 -0
- package/dist/test/utilities/d1.d.ts.map +1 -0
- package/dist/test/utilities/env.d.ts +43 -0
- package/dist/test/utilities/env.d.ts.map +1 -0
- package/dist/test/utilities/kv.d.ts +11 -0
- package/dist/test/utilities/kv.d.ts.map +1 -0
- package/dist/test/utilities/media.d.ts +16 -0
- package/dist/test/utilities/media.d.ts.map +1 -0
- package/dist/test/utilities/platform.d.ts +41 -0
- package/dist/test/utilities/platform.d.ts.map +1 -0
- package/dist/test/utilities/queue.d.ts +5 -0
- package/dist/test/utilities/queue.d.ts.map +1 -0
- package/dist/test/utilities/r2.d.ts +2 -0
- package/dist/test/utilities/r2.d.ts.map +1 -0
- package/dist/test/utilities/stream-flagship.d.ts +27 -0
- package/dist/test/utilities/stream-flagship.d.ts.map +1 -0
- package/dist/test/utilities/vectorize.d.ts +38 -0
- package/dist/test/utilities/vectorize.d.ts.map +1 -0
- package/dist/test/utilities/workflows.d.ts +26 -0
- package/dist/test/utilities/workflows.d.ts.map +1 -0
- package/dist/test/utilities.d.ts +13 -98
- package/dist/test/utilities.d.ts.map +1 -1
- package/dist/test/worker.d.ts +6 -0
- package/dist/test/worker.d.ts.map +1 -1
- package/dist/transform/durable-object.d.ts.map +1 -1
- package/dist/transform/index.d.ts +2 -2
- package/dist/transform/worker-entrypoint.d.ts +5 -0
- package/dist/transform/worker-entrypoint.d.ts.map +1 -1
- package/dist/utils/entrypoint-discovery.d.ts +6 -3
- package/dist/utils/entrypoint-discovery.d.ts.map +1 -1
- package/dist/utils/glob.d.ts.map +1 -1
- package/dist/utils/resolve-package.d.ts.map +1 -1
- package/dist/utils/send-email.d.ts +15 -0
- package/dist/utils/send-email.d.ts.map +1 -0
- package/dist/utils/send-email.js +163 -0
- package/dist/vite/config-file.d.ts +25 -0
- package/dist/vite/config-file.d.ts.map +1 -0
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +2 -339
- package/dist/vite/plugin-config-hook.d.ts +28 -0
- package/dist/vite/plugin-config-hook.d.ts.map +1 -0
- package/dist/vite/plugin-context.d.ts +27 -0
- package/dist/vite/plugin-context.d.ts.map +1 -0
- package/dist/vite/plugin-durable-objects.d.ts +18 -0
- package/dist/vite/plugin-durable-objects.d.ts.map +1 -0
- package/dist/vite/plugin-programmatic.d.ts +44 -0
- package/dist/vite/plugin-programmatic.d.ts.map +1 -0
- package/dist/vite/plugin-service-bindings.d.ts +13 -0
- package/dist/vite/plugin-service-bindings.d.ts.map +1 -0
- package/dist/vite/plugin-transform.d.ts +41 -0
- package/dist/vite/plugin-transform.d.ts.map +1 -0
- package/dist/vite/plugin.d.ts +10 -41
- package/dist/vite/plugin.d.ts.map +1 -1
- package/dist/worker-entry/composed-worker.d.ts +7 -0
- package/dist/worker-entry/composed-worker.d.ts.map +1 -0
- package/dist/worker-entry/durable-object-discovery.d.ts +20 -0
- package/dist/worker-entry/durable-object-discovery.d.ts.map +1 -0
- package/dist/worker-entry/extensions.d.ts +5 -0
- package/dist/worker-entry/extensions.d.ts.map +1 -0
- package/dist/worker-entry/routes.d.ts +22 -0
- package/dist/worker-entry/routes.d.ts.map +1 -0
- package/dist/worker-entry/surface-paths.d.ts +26 -0
- package/dist/worker-entry/surface-paths.d.ts.map +1 -0
- package/dist/worker-entry/validate-fetch-style.d.ts +12 -0
- package/dist/worker-entry/validate-fetch-style.d.ts.map +1 -0
- package/dist/workflows/local-workflow-entrypoints.d.ts +7 -0
- package/dist/workflows/local-workflow-entrypoints.d.ts.map +1 -0
- package/package.json +159 -116
- package/R2.md +0 -170
- package/dist/account-rvrj687w.js +0 -397
- package/dist/ai-dx4fr9jh.js +0 -107
- package/dist/bridge/protocol.d.ts.map +0 -1
- package/dist/bridge/serialization.d.ts.map +0 -1
- package/dist/browser-shim/worker.d.ts +0 -14
- package/dist/browser-shim/worker.d.ts.map +0 -1
- package/dist/build-mnf6v8gd.js +0 -53
- package/dist/deploy-nhceck39.js +0 -70
- package/dist/dev-b9dmrj7b.js +0 -2417
- package/dist/doctor-fmgb3d28.js +0 -190
- package/dist/durable-object-t4kbb0yt.js +0 -13
- package/dist/index-07q6yxyc.js +0 -168
- package/dist/index-18hvb6gb.js +0 -194
- package/dist/index-1xpj0m4r.js +0 -57
- package/dist/index-37x76zdn.js +0 -4
- package/dist/index-3t6rypgc.js +0 -13
- package/dist/index-67qcae0f.js +0 -183
- package/dist/index-a855bdsx.js +0 -18
- package/dist/index-d8bdkx2h.js +0 -109
- package/dist/index-ep3445yc.js +0 -2225
- package/dist/index-gz1gndna.js +0 -307
- package/dist/index-hcex3rgh.js +0 -266
- package/dist/index-m2q41jwa.js +0 -462
- package/dist/index-n7rs26ft.js +0 -77
- package/dist/index-pf5s73n9.js +0 -1413
- package/dist/index-rbht7m9r.js +0 -36
- package/dist/index-tfyxa77h.js +0 -850
- package/dist/index-tk6ej9dj.js +0 -94
- package/dist/index-z14anrqp.js +0 -226
- package/dist/init-f9mgmew3.js +0 -186
- package/dist/remote-q59qk463.js +0 -97
- package/dist/test/bridge-context.d.ts +0 -76
- package/dist/test/bridge-context.d.ts.map +0 -1
- package/dist/test/multi-worker-context.d.ts +0 -114
- package/dist/test/multi-worker-context.d.ts.map +0 -1
- package/dist/types-5nyrz1sz.js +0 -454
- package/dist/worker-entrypoint-m9th0rg0.js +0 -13
|
@@ -0,0 +1,3471 @@
|
|
|
1
|
+
import { c as getDevflareDotenvPaths, d as resolveConfigEnvVars, o as EnvVarResolutionError } from "./preview-C3Cdr8an.js";
|
|
2
|
+
import { i as resolveConfigPath, r as loadConfig } from "./loader-Cr0_OWPy.js";
|
|
3
|
+
import { g as normalizeR2Binding, n as getLocalKVNamespaceIdentifier, r as getSingleBrowserBindingName, t as getLocalD1DatabaseIdentifier } from "./schema-normalization-BCwTLTGp.js";
|
|
4
|
+
import { t as applyLocalDevVarsToConfig } from "./local-dev-vars-CTSa-wvF.js";
|
|
5
|
+
import { a as cyanBold, f as logLine, g as yellow, o as dim, r as createCliTheme } from "./ui-BUoZApvE.js";
|
|
6
|
+
import { clearLocalSendEmailBindings, setLocalSendEmailBindings } from "../utils/send-email.js";
|
|
7
|
+
import { C as buildSendEmailConfig, D as buildWorkerLoadersConfig, E as buildVersionMetadataConfig, O as buildWorkflowsConfig, S as buildSecretsStoreConfig, T as buildTailConsumersConfig, _ as buildMtlsCertificatesConfig, b as buildQueueProducers, c as buildAiSearchInstancesConfig, d as buildArtifactsConfig, f as buildDispatchNamespacesConfig, g as buildMediaConfig, h as buildImagesConfig, l as buildAiSearchNamespacesConfig, m as buildHyperdrivesConfig, n as getRouteDirectoryCandidate, p as buildFlagshipConfig, s as resolveServiceBindings, t as discoverRoutes, u as buildAnalyticsEngineConfig, v as buildPipelinesConfig, w as buildStreamConfig, x as buildRateLimitsConfig, y as buildQueueConsumers } from "./routes-BFYCqHb-.js";
|
|
8
|
+
import { r as transformDurableObject } from "./durable-object-DMtH0XYX.js";
|
|
9
|
+
import { i as findFiles } from "./glob-CmQOvunB.js";
|
|
10
|
+
import { d as DEFAULT_FETCH_ENTRY_FILES, f as DEFAULT_QUEUE_ENTRY_FILES, g as discoverDurableObjectFiles, h as resolveWorkerSurfacePaths, i as writeGeneratedViteConfig, l as prepareComposedWorkerEntrypoint, m as hasWorkerSurfacePaths, n as resolveEffectiveViteProject, p as DEFAULT_SCHEDULED_ENTRY_FILES, u as DEFAULT_EMAIL_ENTRY_FILES } from "./vite-CGtVNf5E.js";
|
|
11
|
+
import { a as createWorkerdBundlerDefaults, i as writeWorkerCompatibleBundle, n as ensureDebugShim, r as resolveWorkerCompatibleRolldownConfig, t as bundleWorkerEntry } from "./worker-bundler-dTTnYU3n.js";
|
|
12
|
+
import { t as bundleWorkflowEntrypointScript } from "./local-workflow-entrypoints-Cq_WJajd.js";
|
|
13
|
+
import { n as buildLocalSecretWrappedBindingConfig } from "./local-secrets-DMPkmOn3.js";
|
|
14
|
+
import { t as buildLocalBindingShimServiceConfig } from "./local-media-bindings-DsAW4AMF.js";
|
|
15
|
+
import { i as waitForViteReady, n as resolveViteMode, r as stopSpawnedProcessTree, t as detectViteProject } from "./vite-utils-OVLMV605.js";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { dirname, relative, resolve } from "pathe";
|
|
18
|
+
import { exec, spawn } from "node:child_process";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join as join$1 } from "node:path";
|
|
21
|
+
import { createConsola } from "consola";
|
|
22
|
+
import picomatch from "picomatch";
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { promisify } from "node:util";
|
|
25
|
+
import { createInterface } from "node:readline";
|
|
26
|
+
import { createServer } from "node:http";
|
|
27
|
+
import { Browser, detectBrowserPlatform, install, resolveBuildId } from "@puppeteer/browsers";
|
|
28
|
+
import puppeteerCore from "puppeteer-core";
|
|
29
|
+
//#region src/bundler/do-bundler.ts
|
|
30
|
+
/**
|
|
31
|
+
* Convert PascalCase class name to SCREAMING_SNAKE_CASE binding name
|
|
32
|
+
*/
|
|
33
|
+
function classToBindingName(className) {
|
|
34
|
+
return className.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Discover DO classes from a glob pattern.
|
|
38
|
+
* Respects .gitignore automatically.
|
|
39
|
+
*/
|
|
40
|
+
async function discoverDOs(cwd, pattern) {
|
|
41
|
+
const discovered = [];
|
|
42
|
+
const files = await discoverDurableObjectFiles(cwd, pattern);
|
|
43
|
+
for (const [filePath, classNames] of files) for (const className of classNames) discovered.push({
|
|
44
|
+
filePath,
|
|
45
|
+
className,
|
|
46
|
+
bindingName: classToBindingName(className)
|
|
47
|
+
});
|
|
48
|
+
return discovered;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Strip @durableObject decorator and its import from source code
|
|
52
|
+
* For dev mode, we just need to remove the decorator syntax - the DO class works as-is
|
|
53
|
+
*/
|
|
54
|
+
function stripDecoratorSyntax(code) {
|
|
55
|
+
let result = code;
|
|
56
|
+
result = result.replace(/@durableObject\s*\([^)]*\)\s*\n?\s*(?=export\s+class)/g, "");
|
|
57
|
+
result = result.replace(/import\s*\{([^}]*)\bdurableObject\b[^}]*\}\s*from\s*['"]devflare\/runtime['"]\s*;?/g, (match, imports) => {
|
|
58
|
+
const cleanedImports = imports.split(",").map((s) => s.trim()).filter((s) => !s.startsWith("durableObject")).join(", ");
|
|
59
|
+
if (cleanedImports.trim() === "") return "";
|
|
60
|
+
return `import { ${cleanedImports} } from 'devflare/runtime'`;
|
|
61
|
+
});
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bundle a single DO file using Rolldown
|
|
66
|
+
*
|
|
67
|
+
* Strategy:
|
|
68
|
+
* 1. Read the source file
|
|
69
|
+
* 2. Strip @durableObject decorator (not needed at runtime - just a marker)
|
|
70
|
+
* 3. Feed the cleaned code to Rolldown through a virtual-entry plugin whose id
|
|
71
|
+
* sits in the source directory, so relative imports resolve identically to
|
|
72
|
+
* the original Durable Object module — without ever writing a temp file
|
|
73
|
+
* next to user source.
|
|
74
|
+
* 4. Bundle with Rolldown.
|
|
75
|
+
*/
|
|
76
|
+
async function bundleDOFile(sourcePath, className, outDir, cwd, bundleOptions) {
|
|
77
|
+
const fs = await import("node:fs/promises");
|
|
78
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
79
|
+
const sourceCode = await fs.readFile(sourcePath, "utf-8");
|
|
80
|
+
const entryCode = `${(await transformDurableObject(sourceCode, sourcePath))?.code ?? stripDecoratorSyntax(sourceCode)}
|
|
81
|
+
|
|
82
|
+
// Default export for worker (required by Miniflare)
|
|
83
|
+
export default {
|
|
84
|
+
async fetch(request) {
|
|
85
|
+
return new Response('DO Worker for ${className}', { status: 200 });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
`;
|
|
89
|
+
const virtualEntryId = resolve(dirname(sourcePath), `.devflare-do-${className}.virtual.ts`);
|
|
90
|
+
const classOutDir = resolve(outDir, className);
|
|
91
|
+
try {
|
|
92
|
+
await fs.rm(classOutDir, {
|
|
93
|
+
recursive: true,
|
|
94
|
+
force: true
|
|
95
|
+
});
|
|
96
|
+
} catch {}
|
|
97
|
+
await fs.mkdir(classOutDir, { recursive: true });
|
|
98
|
+
const debugShimPath = await ensureDebugShim(outDir);
|
|
99
|
+
const virtualEntryPlugin = {
|
|
100
|
+
name: "devflare-do-virtual-entry",
|
|
101
|
+
resolveId(id) {
|
|
102
|
+
if (id === virtualEntryId) return virtualEntryId;
|
|
103
|
+
return null;
|
|
104
|
+
},
|
|
105
|
+
load(id) {
|
|
106
|
+
if (id === virtualEntryId) return entryCode;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const userRolldownOptions = bundleOptions?.rolldownOptions;
|
|
111
|
+
const userPlugins = userRolldownOptions?.plugins;
|
|
112
|
+
const mergedPlugins = userPlugins === void 0 ? [virtualEntryPlugin] : Array.isArray(userPlugins) ? [virtualEntryPlugin, ...userPlugins] : [virtualEntryPlugin, userPlugins];
|
|
113
|
+
const outFile = resolve(classOutDir, "index.js");
|
|
114
|
+
const defaults = createWorkerdBundlerDefaults();
|
|
115
|
+
const { inputOptions, outputOptions } = resolveWorkerCompatibleRolldownConfig({
|
|
116
|
+
cwd,
|
|
117
|
+
inputFile: virtualEntryId,
|
|
118
|
+
outFile,
|
|
119
|
+
...defaults,
|
|
120
|
+
platform: "neutral",
|
|
121
|
+
alias: { debug: debugShimPath },
|
|
122
|
+
rolldownOptions: {
|
|
123
|
+
...userRolldownOptions,
|
|
124
|
+
plugins: mergedPlugins
|
|
125
|
+
},
|
|
126
|
+
sourcemap: bundleOptions?.sourcemap ?? defaults.sourcemap,
|
|
127
|
+
minify: bundleOptions?.minify ?? defaults.minify,
|
|
128
|
+
inlineDynamicImports: true,
|
|
129
|
+
defaultTsconfigMode: "always"
|
|
130
|
+
});
|
|
131
|
+
await writeWorkerCompatibleBundle({
|
|
132
|
+
inputOptions,
|
|
133
|
+
outputOptions,
|
|
134
|
+
outFile
|
|
135
|
+
});
|
|
136
|
+
return resolve(classOutDir, "index.js");
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Bundle all discovered DOs
|
|
140
|
+
*/
|
|
141
|
+
async function bundleAllDOs(discovered, outDir, cwd, logger, bundleOptions) {
|
|
142
|
+
await import("node:fs/promises");
|
|
143
|
+
const bundles = /* @__PURE__ */ new Map();
|
|
144
|
+
const classes = /* @__PURE__ */ new Map();
|
|
145
|
+
const sourceFiles = /* @__PURE__ */ new Map();
|
|
146
|
+
const errors = [];
|
|
147
|
+
for (const do_ of discovered) {
|
|
148
|
+
const existing = sourceFiles.get(do_.filePath) || [];
|
|
149
|
+
existing.push(do_.className);
|
|
150
|
+
sourceFiles.set(do_.filePath, existing);
|
|
151
|
+
}
|
|
152
|
+
for (const do_ of discovered) try {
|
|
153
|
+
logger?.debug(`Bundling ${do_.className} from ${do_.filePath}`);
|
|
154
|
+
const outFile = await bundleDOFile(do_.filePath, do_.className, outDir, cwd, bundleOptions);
|
|
155
|
+
bundles.set(do_.bindingName, outFile);
|
|
156
|
+
classes.set(do_.bindingName, do_.className);
|
|
157
|
+
logger?.debug(` → ${outFile}`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
160
|
+
errors.push(err);
|
|
161
|
+
logger?.error(`Failed to bundle ${do_.className}:`, err.message);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
bundles,
|
|
165
|
+
classes,
|
|
166
|
+
sourceFiles,
|
|
167
|
+
errors
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Create a DO bundler with watch support
|
|
172
|
+
*/
|
|
173
|
+
function createDOBundler(options) {
|
|
174
|
+
const { cwd, pattern, outDir, logger, onRebuild, rolldownOptions, sourcemap, minify } = options;
|
|
175
|
+
let result = {
|
|
176
|
+
bundles: /* @__PURE__ */ new Map(),
|
|
177
|
+
classes: /* @__PURE__ */ new Map(),
|
|
178
|
+
sourceFiles: /* @__PURE__ */ new Map(),
|
|
179
|
+
errors: []
|
|
180
|
+
};
|
|
181
|
+
let watcher = null;
|
|
182
|
+
let chokidarWatcher = null;
|
|
183
|
+
/**
|
|
184
|
+
* Perform initial build
|
|
185
|
+
*/
|
|
186
|
+
async function build() {
|
|
187
|
+
const discovered = await discoverDOs(cwd, pattern);
|
|
188
|
+
if (discovered.length === 0) {
|
|
189
|
+
logger?.debug("No DOs found matching pattern:", pattern);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
logger?.info(`Found ${discovered.length} Durable Object(s)`);
|
|
193
|
+
for (const do_ of discovered) logger?.info(` • ${do_.className} → ${do_.bindingName}`);
|
|
194
|
+
result = await bundleAllDOs(discovered, outDir, cwd, logger, {
|
|
195
|
+
rolldownOptions,
|
|
196
|
+
sourcemap,
|
|
197
|
+
minify
|
|
198
|
+
});
|
|
199
|
+
if (result.errors.length === 0) logger?.success(`Bundled ${result.bundles.size} DO(s) to ${outDir}`);
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Watch for changes and rebuild
|
|
204
|
+
*
|
|
205
|
+
* Strategy: Watch parent directories of DO files with a filter for matching files.
|
|
206
|
+
* This allows detection of new DO files created during dev.
|
|
207
|
+
* Uses compiled picomatch for fast pattern matching instead of re-globbing on every event.
|
|
208
|
+
*/
|
|
209
|
+
async function watch() {
|
|
210
|
+
const chokidar = await import("chokidar");
|
|
211
|
+
const files = await findFiles(pattern, { cwd });
|
|
212
|
+
let dirsToWatch;
|
|
213
|
+
if (files.length > 0) dirsToWatch = [...new Set(files.map((f) => dirname(f)))];
|
|
214
|
+
else {
|
|
215
|
+
const patternDir = dirname(pattern);
|
|
216
|
+
const absolutePatternDir = resolve(cwd, patternDir === "." ? "" : patternDir) || cwd;
|
|
217
|
+
dirsToWatch = [absolutePatternDir];
|
|
218
|
+
logger?.debug(`No DO files yet, watching pattern directory: ${absolutePatternDir}`);
|
|
219
|
+
}
|
|
220
|
+
logger?.info(`Watching ${files.length} DO file(s) in ${dirsToWatch.length} director(ies)...`);
|
|
221
|
+
const isWindows = process.platform === "win32";
|
|
222
|
+
chokidarWatcher = chokidar.watch(dirsToWatch, {
|
|
223
|
+
ignoreInitial: true,
|
|
224
|
+
usePolling: isWindows,
|
|
225
|
+
interval: isWindows ? 300 : void 0,
|
|
226
|
+
awaitWriteFinish: {
|
|
227
|
+
stabilityThreshold: 100,
|
|
228
|
+
pollInterval: 50
|
|
229
|
+
},
|
|
230
|
+
depth: 0
|
|
231
|
+
});
|
|
232
|
+
const normalizePath = (p) => {
|
|
233
|
+
let normalized = p.replace(/\\/g, "/");
|
|
234
|
+
if (isWindows && /^[a-zA-Z]:/.test(normalized)) normalized = normalized[0].toLowerCase() + normalized.slice(1);
|
|
235
|
+
return normalized;
|
|
236
|
+
};
|
|
237
|
+
const isMatch = picomatch(pattern, {
|
|
238
|
+
dot: true,
|
|
239
|
+
matchBase: false
|
|
240
|
+
});
|
|
241
|
+
const matchesPattern = (filePath) => {
|
|
242
|
+
const normalizedPath = normalizePath(filePath);
|
|
243
|
+
return isMatch(relative(normalizePath(cwd), normalizedPath));
|
|
244
|
+
};
|
|
245
|
+
let isRebuilding = false;
|
|
246
|
+
let pendingRebuild = null;
|
|
247
|
+
let rebuildTimeout = null;
|
|
248
|
+
const scheduleRebuild = (changedPath) => {
|
|
249
|
+
if (rebuildTimeout) clearTimeout(rebuildTimeout);
|
|
250
|
+
rebuildTimeout = setTimeout(() => {
|
|
251
|
+
triggerRebuild(changedPath);
|
|
252
|
+
}, 150);
|
|
253
|
+
};
|
|
254
|
+
const triggerRebuild = async (changedPath) => {
|
|
255
|
+
if (isRebuilding) {
|
|
256
|
+
pendingRebuild = changedPath;
|
|
257
|
+
logger?.debug(`Rebuild already in progress, queuing: ${changedPath}`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
isRebuilding = true;
|
|
261
|
+
try {
|
|
262
|
+
logger?.info(`DO file changed: ${changedPath}`);
|
|
263
|
+
logger?.info("Rebuilding DOs...");
|
|
264
|
+
const startTime = Date.now();
|
|
265
|
+
result = await build();
|
|
266
|
+
const elapsed = Date.now() - startTime;
|
|
267
|
+
logger?.success(`DO rebuild complete (${elapsed}ms)`);
|
|
268
|
+
await onRebuild?.(result);
|
|
269
|
+
} catch (error) {
|
|
270
|
+
logger?.error("DO rebuild failed:", error);
|
|
271
|
+
} finally {
|
|
272
|
+
isRebuilding = false;
|
|
273
|
+
if (pendingRebuild) {
|
|
274
|
+
const nextPath = pendingRebuild;
|
|
275
|
+
pendingRebuild = null;
|
|
276
|
+
triggerRebuild(nextPath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
chokidarWatcher.on("change", (filePath) => {
|
|
281
|
+
if (matchesPattern(filePath)) {
|
|
282
|
+
logger?.debug(`File changed: ${filePath}`);
|
|
283
|
+
scheduleRebuild(filePath);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
chokidarWatcher.on("add", (filePath) => {
|
|
287
|
+
if (matchesPattern(filePath)) {
|
|
288
|
+
logger?.debug(`File added: ${filePath}`);
|
|
289
|
+
scheduleRebuild(filePath);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
chokidarWatcher.on("unlink", (filePath) => {
|
|
293
|
+
if (matchesPattern(filePath)) {
|
|
294
|
+
logger?.debug(`File removed: ${filePath}`);
|
|
295
|
+
scheduleRebuild(filePath);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
chokidarWatcher.on("ready", () => {
|
|
299
|
+
logger?.info("DO file watcher ready");
|
|
300
|
+
});
|
|
301
|
+
chokidarWatcher.on("error", (error) => {
|
|
302
|
+
logger?.error("DO file watcher error:", error);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Stop watching
|
|
307
|
+
*/
|
|
308
|
+
async function close() {
|
|
309
|
+
if (watcher) {
|
|
310
|
+
await watcher.close();
|
|
311
|
+
watcher = null;
|
|
312
|
+
}
|
|
313
|
+
if (chokidarWatcher) {
|
|
314
|
+
await chokidarWatcher.close();
|
|
315
|
+
chokidarWatcher = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Get the latest result
|
|
320
|
+
*/
|
|
321
|
+
function getResult() {
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
build,
|
|
326
|
+
watch,
|
|
327
|
+
close,
|
|
328
|
+
getResult
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/dev-server/miniflare-log.ts
|
|
333
|
+
const ANSI_ESCAPE_REGEX = /\u001B\[[0-9;]*m/g;
|
|
334
|
+
const COMPATIBILITY_DATE_FALLBACK_REGEX = /^The latest compatibility date supported by the installed Cloudflare Workers Runtime is "([^"]+)", but you've requested "([^"]+)"\. Falling back to "([^"]+)"\.\.\.$/;
|
|
335
|
+
const MINIFLARE_LOG_LEVEL_FALLBACKS = {
|
|
336
|
+
WARN: 2,
|
|
337
|
+
DEBUG: 4
|
|
338
|
+
};
|
|
339
|
+
function normalizeMiniflareMessage(message) {
|
|
340
|
+
return message.replace(ANSI_ESCAPE_REGEX, "").replace(/\s+/g, " ").trim();
|
|
341
|
+
}
|
|
342
|
+
function formatCompatibilityDateFallbackNotice(message) {
|
|
343
|
+
const normalizedMessage = normalizeMiniflareMessage(message);
|
|
344
|
+
const match = COMPATIBILITY_DATE_FALLBACK_REGEX.exec(normalizedMessage);
|
|
345
|
+
if (!match) return null;
|
|
346
|
+
const [, _supportedDate, requestedDate, fallbackDate] = match;
|
|
347
|
+
return `Using latest supported Cloudflare Workers Runtime compatibility date ${fallbackDate} (requested ${requestedDate})`;
|
|
348
|
+
}
|
|
349
|
+
function resolveMiniflareLogLevel(logLevel, levelName) {
|
|
350
|
+
return logLevel?.[levelName] ?? MINIFLARE_LOG_LEVEL_FALLBACKS[levelName];
|
|
351
|
+
}
|
|
352
|
+
function createMiniflareLog(BaseLog, logLevel, levelName, logger) {
|
|
353
|
+
if (!BaseLog) return;
|
|
354
|
+
return createCompatibilityAwareMiniflareLog(BaseLog, resolveMiniflareLogLevel(logLevel, levelName), logger);
|
|
355
|
+
}
|
|
356
|
+
function createCompatibilityAwareMiniflareLog(BaseLog, level, logger) {
|
|
357
|
+
const log = new BaseLog(level);
|
|
358
|
+
const originalWarn = log.warn.bind(log);
|
|
359
|
+
const originalInfo = log.info.bind(log);
|
|
360
|
+
log.warn = (message) => {
|
|
361
|
+
const notice = formatCompatibilityDateFallbackNotice(message);
|
|
362
|
+
if (!notice) {
|
|
363
|
+
originalWarn(message);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (logger) {
|
|
367
|
+
logger.info(notice);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
originalInfo(notice);
|
|
371
|
+
};
|
|
372
|
+
return log;
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/bridge/gateway-runtime.ts
|
|
376
|
+
/**
|
|
377
|
+
* Shared gateway helpers (base64, R2 serializers, email) and the RPC method
|
|
378
|
+
* dispatcher. Designed to be embedded verbatim at the top level of a worker
|
|
379
|
+
* module. All symbols are declared with `function`/`const` so they are
|
|
380
|
+
* hoisted in both embedding sites.
|
|
381
|
+
*/
|
|
382
|
+
const GATEWAY_RUNTIME_JS = `
|
|
383
|
+
const RAW_EMAIL = 'EmailMessage::raw'
|
|
384
|
+
|
|
385
|
+
// Inline body cap for proxied DO/service-fetch responses. Matches
|
|
386
|
+
// HTTP_TRANSFER_THRESHOLD in wire.ts (512 KB). A larger body would exceed
|
|
387
|
+
// workerd's ~1 MB WebSocket message limit once base64-encoded into the rpc.ok
|
|
388
|
+
// frame, so it is rejected with a clear error rather than silently truncated.
|
|
389
|
+
const HTTP_TRANSFER_THRESHOLD = 512 * 1024
|
|
390
|
+
|
|
391
|
+
function arrayBufferToBase64(buffer) {
|
|
392
|
+
const bytes = new Uint8Array(buffer)
|
|
393
|
+
let binary = ''
|
|
394
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i])
|
|
395
|
+
return btoa(binary)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function base64ToArrayBuffer(base64) {
|
|
399
|
+
const binary = atob(base64)
|
|
400
|
+
const bytes = new Uint8Array(binary.length)
|
|
401
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
402
|
+
return bytes.buffer
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Binary WsData frame codec (10-byte header: kind:u8, id:u32 LE, seq:u32 LE,
|
|
406
|
+
// flags:u8 + payload). MUST match wire.ts encodeBinaryFrame/decodeBinaryFrame so
|
|
407
|
+
// the bridge client and this gateway agree on the WS-data wire format
|
|
408
|
+
// (BinaryKind.WsData = 2, BinaryFlags.TEXT = 2).
|
|
409
|
+
function encodeWsDataFrame(wid, flags, payload) {
|
|
410
|
+
const frame = new Uint8Array(10 + payload.byteLength)
|
|
411
|
+
const view = new DataView(frame.buffer)
|
|
412
|
+
view.setUint8(0, 2)
|
|
413
|
+
view.setUint32(1, wid, true)
|
|
414
|
+
view.setUint32(5, 0, true)
|
|
415
|
+
view.setUint8(9, flags)
|
|
416
|
+
frame.set(payload, 10)
|
|
417
|
+
return frame
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function decodeWsDataFrame(buffer) {
|
|
421
|
+
const bytes = new Uint8Array(buffer)
|
|
422
|
+
if (bytes.byteLength < 10) return null
|
|
423
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
424
|
+
return {
|
|
425
|
+
kind: view.getUint8(0),
|
|
426
|
+
id: view.getUint32(1, true),
|
|
427
|
+
flags: view.getUint8(9),
|
|
428
|
+
payload: bytes.subarray(10)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function serializeR2Object(obj) {
|
|
433
|
+
if (!obj) return null
|
|
434
|
+
return {
|
|
435
|
+
__type: 'R2Object',
|
|
436
|
+
key: obj.key,
|
|
437
|
+
version: obj.version,
|
|
438
|
+
size: obj.size,
|
|
439
|
+
etag: obj.etag,
|
|
440
|
+
httpEtag: obj.httpEtag,
|
|
441
|
+
checksums: obj.checksums,
|
|
442
|
+
uploaded: obj.uploaded?.toISOString(),
|
|
443
|
+
httpMetadata: obj.httpMetadata,
|
|
444
|
+
customMetadata: obj.customMetadata,
|
|
445
|
+
range: obj.range,
|
|
446
|
+
storageClass: obj.storageClass
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function serializeR2ObjectBody(obj, bodyData) {
|
|
451
|
+
if (!obj) return null
|
|
452
|
+
return {
|
|
453
|
+
__type: 'R2ObjectBody',
|
|
454
|
+
key: obj.key,
|
|
455
|
+
version: obj.version,
|
|
456
|
+
size: obj.size,
|
|
457
|
+
etag: obj.etag,
|
|
458
|
+
httpEtag: obj.httpEtag,
|
|
459
|
+
checksums: obj.checksums,
|
|
460
|
+
uploaded: obj.uploaded?.toISOString(),
|
|
461
|
+
httpMetadata: obj.httpMetadata,
|
|
462
|
+
customMetadata: obj.customMetadata,
|
|
463
|
+
range: obj.range,
|
|
464
|
+
storageClass: obj.storageClass,
|
|
465
|
+
bodyData
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function serializeR2Objects(result) {
|
|
470
|
+
if (!result) return null
|
|
471
|
+
return {
|
|
472
|
+
objects: result.objects.map(serializeR2Object),
|
|
473
|
+
truncated: result.truncated,
|
|
474
|
+
cursor: result.cursor,
|
|
475
|
+
delimitedPrefixes: result.delimitedPrefixes
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function serializeResponse(response) {
|
|
480
|
+
let body = null
|
|
481
|
+
if (response.body) {
|
|
482
|
+
const bytes = await response.arrayBuffer()
|
|
483
|
+
if (bytes.byteLength > HTTP_TRANSFER_THRESHOLD) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
'[devflare][bridge] Response body (' + bytes.byteLength + ' bytes) exceeds the '
|
|
486
|
+
+ HTTP_TRANSFER_THRESHOLD + '-byte inline limit. DO and service-binding fetch responses '
|
|
487
|
+
+ 'are delivered inline over the bridge WebSocket and cannot be streamed locally; keep '
|
|
488
|
+
+ 'proxied response bodies under the limit (large R2 objects are exempt — read them through '
|
|
489
|
+
+ 'the R2 binding, which uses the HTTP transfer side-channel).'
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
if (bytes.byteLength > 0) {
|
|
493
|
+
body = { type: 'bytes', data: arrayBufferToBase64(bytes) }
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
status: response.status,
|
|
498
|
+
statusText: response.statusText,
|
|
499
|
+
headers: [...response.headers.entries()],
|
|
500
|
+
body
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function deserializeRequest(serializedReq) {
|
|
505
|
+
return new Request(serializedReq.url, {
|
|
506
|
+
method: serializedReq.method,
|
|
507
|
+
headers: serializedReq.headers,
|
|
508
|
+
body: serializedReq.body?.type === 'bytes'
|
|
509
|
+
? base64ToArrayBuffer(serializedReq.body.data)
|
|
510
|
+
: undefined,
|
|
511
|
+
redirect: serializedReq.redirect
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function createEmailMessageRaw(raw) {
|
|
516
|
+
if (typeof raw === 'string' || raw instanceof ReadableStream) {
|
|
517
|
+
return raw
|
|
518
|
+
}
|
|
519
|
+
if (raw instanceof ArrayBuffer || raw instanceof Uint8Array) {
|
|
520
|
+
return new Response(raw).body
|
|
521
|
+
}
|
|
522
|
+
throw new Error('Unsupported EmailMessage raw payload')
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function isDurableObjectNamespace(binding) {
|
|
526
|
+
return !!binding
|
|
527
|
+
&& typeof binding.idFromName === 'function'
|
|
528
|
+
&& typeof binding.idFromString === 'function'
|
|
529
|
+
&& typeof binding.newUniqueId === 'function'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Resolve a (possibly jurisdiction-scoped) DurableObjectNamespace.
|
|
534
|
+
* Forwards the jurisdiction faithfully when the binding supports it
|
|
535
|
+
* (real Cloudflare), and degrades to the plain binding otherwise
|
|
536
|
+
* (miniflare/workerd local emulation, which does not pin jurisdictions).
|
|
537
|
+
*/
|
|
538
|
+
function resolveDoNamespace(binding, jurisdiction) {
|
|
539
|
+
if (jurisdiction && typeof binding.jurisdiction === 'function') {
|
|
540
|
+
return binding.jurisdiction(jurisdiction)
|
|
541
|
+
}
|
|
542
|
+
return binding
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Execute an RPC method against the gateway's bindings.
|
|
547
|
+
*
|
|
548
|
+
* Method format: "binding.operation". Operations must be namespaced by
|
|
549
|
+
* binding kind (e.g. "kv.get", "r2.head", "d1.stmt.first", "do.fetch",
|
|
550
|
+
* "service.fetch", "queue.send", "email.send", "ai.run"). Bare verbs and the legacy
|
|
551
|
+
* "stmt.*" / "stub.*" sub-prefixes were removed in B3-final and now throw.
|
|
552
|
+
* Method vocabulary must stay in sync with the canonical server in
|
|
553
|
+
* src/bridge/server.ts.
|
|
554
|
+
*/
|
|
555
|
+
async function executeRpcMethod(method, params, env, _ctx) {
|
|
556
|
+
const parts = method.split('.')
|
|
557
|
+
if (parts.length < 2) throw new Error('Invalid method format: ' + method)
|
|
558
|
+
|
|
559
|
+
const bindingName = parts[0]
|
|
560
|
+
const operation = parts.slice(1).join('.')
|
|
561
|
+
const binding = env[bindingName]
|
|
562
|
+
|
|
563
|
+
if (!binding) throw new Error('Binding not found: ' + bindingName)
|
|
564
|
+
|
|
565
|
+
const isNamespaced =
|
|
566
|
+
operation.indexOf('kv.') === 0 ||
|
|
567
|
+
operation.indexOf('r2.') === 0 ||
|
|
568
|
+
operation.indexOf('d1.') === 0 ||
|
|
569
|
+
operation.indexOf('do.') === 0 ||
|
|
570
|
+
operation.indexOf('service.') === 0 ||
|
|
571
|
+
operation.indexOf('queue.') === 0 ||
|
|
572
|
+
operation.indexOf('email.') === 0 ||
|
|
573
|
+
operation.indexOf('ai.') === 0 ||
|
|
574
|
+
operation.indexOf('workflow.') === 0 ||
|
|
575
|
+
operation.indexOf('var.') === 0
|
|
576
|
+
if (!isNamespaced) {
|
|
577
|
+
throw new Error(createUnsupportedBridgeOperationErrorMessage(bindingName, operation))
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// KV
|
|
581
|
+
if (operation === 'kv.get') return binding.get(params[0], params[1])
|
|
582
|
+
if (operation === 'kv.put') return binding.put(params[0], params[1], params[2])
|
|
583
|
+
if (operation === 'kv.delete') return binding.delete(params[0])
|
|
584
|
+
if (operation === 'kv.list') return binding.list(params[0])
|
|
585
|
+
if (operation === 'kv.getWithMetadata') return binding.getWithMetadata(params[0], params[1])
|
|
586
|
+
|
|
587
|
+
// DO get (returns DOStub reference)
|
|
588
|
+
if (operation === 'do.get') {
|
|
589
|
+
return { __type: 'DOStub', binding: bindingName, id: params[0] }
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// R2
|
|
593
|
+
if (operation === 'r2.head') return serializeR2Object(await binding.head(params[0]))
|
|
594
|
+
if (operation === 'r2.get') {
|
|
595
|
+
const obj = await binding.get(params[0], params[1])
|
|
596
|
+
if (!obj) return null
|
|
597
|
+
const body = await obj.arrayBuffer()
|
|
598
|
+
return serializeR2ObjectBody(obj, arrayBufferToBase64(body))
|
|
599
|
+
}
|
|
600
|
+
if (operation === 'r2.put') {
|
|
601
|
+
let value = params[1]
|
|
602
|
+
if (value && typeof value === 'object') {
|
|
603
|
+
if (value.__type === 'ArrayBuffer' || value.__type === 'Uint8Array') {
|
|
604
|
+
value = base64ToArrayBuffer(value.data)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return serializeR2Object(await binding.put(params[0], value, params[2]))
|
|
608
|
+
}
|
|
609
|
+
if (operation === 'r2.delete') return binding.delete(params[0])
|
|
610
|
+
if (operation === 'r2.list') return serializeR2Objects(await binding.list(params[0]))
|
|
611
|
+
|
|
612
|
+
// D1
|
|
613
|
+
if (operation === 'd1.exec') return binding.exec(params[0])
|
|
614
|
+
if (operation === 'd1.batch') {
|
|
615
|
+
const statements = params[0].map((s) => binding.prepare(s.sql).bind(...(s.bindings || [])))
|
|
616
|
+
return binding.batch(statements)
|
|
617
|
+
}
|
|
618
|
+
if (operation.indexOf('d1.stmt.') === 0) {
|
|
619
|
+
const mode = operation.split('.')[2]
|
|
620
|
+
const [sql, ...rest] = params
|
|
621
|
+
let bindings = rest
|
|
622
|
+
let extraParam
|
|
623
|
+
if (mode === 'first' || mode === 'raw') {
|
|
624
|
+
extraParam = rest[rest.length - 1]
|
|
625
|
+
bindings = rest.slice(0, -1)
|
|
626
|
+
}
|
|
627
|
+
let stmt = binding.prepare(sql)
|
|
628
|
+
if (bindings.length > 0) stmt = stmt.bind(...bindings)
|
|
629
|
+
if (mode === 'first') {
|
|
630
|
+
if (typeof extraParam === 'string' && extraParam.length > 0) return stmt.first(extraParam)
|
|
631
|
+
return stmt.first()
|
|
632
|
+
}
|
|
633
|
+
if (mode === 'all') return stmt.all()
|
|
634
|
+
if (mode === 'run') return stmt.run()
|
|
635
|
+
if (mode === 'raw') return stmt.raw(extraParam)
|
|
636
|
+
throw new Error('Unknown stmt mode: ' + mode)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Durable Objects
|
|
640
|
+
if (operation === 'do.idFromName') {
|
|
641
|
+
const ns = resolveDoNamespace(binding, params[1])
|
|
642
|
+
const id = ns.idFromName(params[0])
|
|
643
|
+
return { __type: 'DOId', hex: id.toString() }
|
|
644
|
+
}
|
|
645
|
+
if (operation === 'do.idFromString') {
|
|
646
|
+
const id = binding.idFromString(params[0])
|
|
647
|
+
return { __type: 'DOId', hex: id.toString() }
|
|
648
|
+
}
|
|
649
|
+
if (operation === 'do.newUniqueId') {
|
|
650
|
+
const ns = resolveDoNamespace(binding, params[1])
|
|
651
|
+
const id = ns.newUniqueId(params[0])
|
|
652
|
+
return { __type: 'DOId', hex: id.toString() }
|
|
653
|
+
}
|
|
654
|
+
if (operation === 'do.fetch') {
|
|
655
|
+
const [, serializedId, serializedReq] = params
|
|
656
|
+
const id = binding.idFromString(serializedId.hex)
|
|
657
|
+
const stub = binding.get(id)
|
|
658
|
+
const response = await stub.fetch(new Request(serializedReq.url, {
|
|
659
|
+
method: serializedReq.method,
|
|
660
|
+
headers: serializedReq.headers,
|
|
661
|
+
body: serializedReq.body?.type === 'bytes'
|
|
662
|
+
? base64ToArrayBuffer(serializedReq.body.data)
|
|
663
|
+
: undefined
|
|
664
|
+
}))
|
|
665
|
+
return serializeResponse(response)
|
|
666
|
+
}
|
|
667
|
+
if (operation === 'do.rpc') {
|
|
668
|
+
const [, serializedId, methodName, args] = params
|
|
669
|
+
const id = binding.idFromString(serializedId.hex)
|
|
670
|
+
const stub = binding.get(id)
|
|
671
|
+
const response = await stub.fetch(new Request('http://do/_rpc', {
|
|
672
|
+
method: 'POST',
|
|
673
|
+
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
body: JSON.stringify({ method: methodName, params: args })
|
|
675
|
+
}))
|
|
676
|
+
const result = await response.json()
|
|
677
|
+
if (!result.ok) throw new Error(result.error?.message || 'RPC failed')
|
|
678
|
+
return result.result
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Service Bindings
|
|
682
|
+
if (operation === 'service.fetch') {
|
|
683
|
+
if (!binding || typeof binding.fetch !== 'function') {
|
|
684
|
+
throw new Error('Service binding ' + bindingName + ' does not support fetch()')
|
|
685
|
+
}
|
|
686
|
+
const response = await binding.fetch(deserializeRequest(params[0]))
|
|
687
|
+
return serializeResponse(response)
|
|
688
|
+
}
|
|
689
|
+
if (operation === 'service.rpc') {
|
|
690
|
+
const methodName = params[0]
|
|
691
|
+
if (typeof methodName !== 'string') {
|
|
692
|
+
throw new Error('Service binding ' + bindingName + ' RPC method name must be a string')
|
|
693
|
+
}
|
|
694
|
+
const args = Array.isArray(params[1]) ? params[1] : []
|
|
695
|
+
const method = binding && binding[methodName]
|
|
696
|
+
if (typeof method !== 'function') {
|
|
697
|
+
throw new Error('Service binding ' + bindingName + ' does not support ' + methodName + '()')
|
|
698
|
+
}
|
|
699
|
+
return method.apply(binding, args)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Queues
|
|
703
|
+
if (operation === 'queue.send') return binding.send(params[0], params[1])
|
|
704
|
+
if (operation === 'queue.sendBatch') return binding.sendBatch(params[0], params[1])
|
|
705
|
+
|
|
706
|
+
// Send Email
|
|
707
|
+
if (operation === 'email.send') {
|
|
708
|
+
if (binding && typeof binding.send === 'function') {
|
|
709
|
+
const message = params[0]
|
|
710
|
+
if (message && typeof message === 'object' && 'from' in message && 'to' in message && 'raw' in message) {
|
|
711
|
+
return binding.send({
|
|
712
|
+
from: message.from,
|
|
713
|
+
to: message.to,
|
|
714
|
+
[RAW_EMAIL]: createEmailMessageRaw(message.raw)
|
|
715
|
+
})
|
|
716
|
+
}
|
|
717
|
+
return binding.send(message)
|
|
718
|
+
}
|
|
719
|
+
return { ok: true, simulated: true }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Workflows
|
|
723
|
+
if (operation === 'workflow.create') {
|
|
724
|
+
return serializeWorkflowInstance(await binding.create(params[0]))
|
|
725
|
+
}
|
|
726
|
+
if (operation === 'workflow.get') {
|
|
727
|
+
return serializeWorkflowInstance(await binding.get(params[0]))
|
|
728
|
+
}
|
|
729
|
+
if (operation === 'workflow.status') {
|
|
730
|
+
return (await binding.get(params[0])).status()
|
|
731
|
+
}
|
|
732
|
+
if (operation === 'workflow.pause') {
|
|
733
|
+
return (await binding.get(params[0])).pause()
|
|
734
|
+
}
|
|
735
|
+
if (operation === 'workflow.resume') {
|
|
736
|
+
return (await binding.get(params[0])).resume()
|
|
737
|
+
}
|
|
738
|
+
if (operation === 'workflow.terminate') {
|
|
739
|
+
return (await binding.get(params[0])).terminate()
|
|
740
|
+
}
|
|
741
|
+
if (operation === 'workflow.restart') {
|
|
742
|
+
return (await binding.get(params[0])).restart()
|
|
743
|
+
}
|
|
744
|
+
if (operation === 'workflow.sendEvent') {
|
|
745
|
+
return (await binding.get(params[0])).sendEvent(params[1])
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// AI / generic run()
|
|
749
|
+
if (operation === 'ai.run') {
|
|
750
|
+
if (typeof binding.run !== 'function') {
|
|
751
|
+
throw new Error('Binding ' + bindingName + ' does not support run(): ' + method)
|
|
752
|
+
}
|
|
753
|
+
return binding.run(params[0], params[1])
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
throw new Error('Unknown operation: ' + method)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function createUnsupportedBridgeOperationErrorMessage(bindingName, operation) {
|
|
760
|
+
const base = "[devflare][bridge] Unsupported bridge operation '" + operation + "' for binding '" + bindingName + "'."
|
|
761
|
+
if (operation === 'fetch') {
|
|
762
|
+
return base + ' Devflare could not dispatch fetch() for this binding through the local bridge. '
|
|
763
|
+
+ 'Expected Cloudflare API: env.' + bindingName + '.fetch(request). '
|
|
764
|
+
+ 'If this came from SvelteKit platform.env, make sure the binding is declared as a service binding; '
|
|
765
|
+
+ 'this is a Devflare local bridge issue when service bindings fall back to a bare fetch operation.'
|
|
766
|
+
}
|
|
767
|
+
if (operation === 'toString') {
|
|
768
|
+
return base + ' A platform.env value was coerced to a string through the bridge. '
|
|
769
|
+
+ 'For SvelteKit local dev, declared vars should be plain string values and missing env names should read as undefined.'
|
|
770
|
+
}
|
|
771
|
+
return base + ' Bare verbs and the legacy stmt.*/stub.* sub-prefixes are not supported; '
|
|
772
|
+
+ 'use the namespaced form (e.g. kv.get, r2.put, d1.stmt.first, do.fetch, service.fetch).'
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function serializeWorkflowInstance(instance) {
|
|
776
|
+
return {
|
|
777
|
+
__type: 'WorkflowInstance',
|
|
778
|
+
id: instance.id
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ---------------------------------------------------------------------------
|
|
783
|
+
// WebSocket bridge (shared with src/bridge/server.ts in shape)
|
|
784
|
+
// ---------------------------------------------------------------------------
|
|
785
|
+
// NOTE: wsProxies is intentionally created per handleBridgeWebSocket call so
|
|
786
|
+
// state never leaks across connections or across gateway-script regenerations.
|
|
787
|
+
|
|
788
|
+
async function handleBridgeRpcCall(msg, ws, env, ctx) {
|
|
789
|
+
try {
|
|
790
|
+
const result = await executeRpcMethod(msg.method, msg.params, env, ctx)
|
|
791
|
+
ws.send(JSON.stringify({ t: 'rpc.ok', id: msg.id, result }))
|
|
792
|
+
} catch (error) {
|
|
793
|
+
ws.send(JSON.stringify({
|
|
794
|
+
t: 'rpc.err',
|
|
795
|
+
id: msg.id,
|
|
796
|
+
error: {
|
|
797
|
+
code: error?.code || 'INTERNAL_ERROR',
|
|
798
|
+
message: error?.message || String(error)
|
|
799
|
+
}
|
|
800
|
+
}))
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function handleBridgeWsOpen(msg, ws, env, wsProxies) {
|
|
805
|
+
try {
|
|
806
|
+
const binding = env[msg.target.binding]
|
|
807
|
+
const id = binding.idFromString(msg.target.id)
|
|
808
|
+
const stub = binding.get(id)
|
|
809
|
+
|
|
810
|
+
const headers = new Headers(msg.target.headers || [])
|
|
811
|
+
headers.set('Upgrade', 'websocket')
|
|
812
|
+
|
|
813
|
+
const response = await stub.fetch(new Request(msg.target.url, { method: 'GET', headers }))
|
|
814
|
+
const doWs = response.webSocket
|
|
815
|
+
|
|
816
|
+
if (!doWs) {
|
|
817
|
+
ws.send(JSON.stringify({
|
|
818
|
+
t: 'rpc.err',
|
|
819
|
+
id: 'ws_' + msg.wid,
|
|
820
|
+
error: { code: 'WS_FAILED', message: 'No WebSocket returned' }
|
|
821
|
+
}))
|
|
822
|
+
return
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
doWs.accept()
|
|
826
|
+
wsProxies.set(msg.wid, { doWs })
|
|
827
|
+
|
|
828
|
+
doWs.addEventListener('message', (event) => {
|
|
829
|
+
const isText = typeof event.data === 'string'
|
|
830
|
+
const payload = isText
|
|
831
|
+
? new TextEncoder().encode(event.data)
|
|
832
|
+
: new Uint8Array(event.data)
|
|
833
|
+
const flags = isText ? 2 : 0
|
|
834
|
+
ws.send(encodeWsDataFrame(msg.wid, flags, payload))
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
doWs.addEventListener('close', (event) => {
|
|
838
|
+
ws.send(JSON.stringify({ t: 'ws.close', wid: msg.wid, code: event.code, reason: event.reason }))
|
|
839
|
+
wsProxies.delete(msg.wid)
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
ws.send(JSON.stringify({ t: 'ws.opened', wid: msg.wid }))
|
|
843
|
+
} catch (error) {
|
|
844
|
+
ws.send(JSON.stringify({
|
|
845
|
+
t: 'rpc.err',
|
|
846
|
+
id: 'ws_' + msg.wid,
|
|
847
|
+
error: { code: 'WS_FAILED', message: error.message }
|
|
848
|
+
}))
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function handleBridgeWsClose(msg, wsProxies) {
|
|
853
|
+
const proxy = wsProxies.get(msg.wid)
|
|
854
|
+
if (proxy) {
|
|
855
|
+
proxy.doWs.close(msg.code, msg.reason)
|
|
856
|
+
wsProxies.delete(msg.wid)
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Relay an inbound binary WsData frame (client -> DO). Mirrors server.ts
|
|
861
|
+
// handleBinaryMessage: decode the frame, look up the proxy by ws id, and forward
|
|
862
|
+
// the payload to the DO socket honoring the TEXT flag.
|
|
863
|
+
function handleBridgeBinaryMessage(buffer, wsProxies) {
|
|
864
|
+
const frame = decodeWsDataFrame(buffer)
|
|
865
|
+
if (!frame || frame.kind !== 2) return
|
|
866
|
+
const proxy = wsProxies.get(frame.id)
|
|
867
|
+
if (!proxy) return
|
|
868
|
+
if ((frame.flags & 2) !== 0) {
|
|
869
|
+
proxy.doWs.send(new TextDecoder().decode(frame.payload))
|
|
870
|
+
} else {
|
|
871
|
+
proxy.doWs.send(frame.payload)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function handleBridgeJsonMessage(data, ws, env, ctx, wsProxies) {
|
|
876
|
+
const msg = JSON.parse(data)
|
|
877
|
+
switch (msg.t) {
|
|
878
|
+
case 'hello':
|
|
879
|
+
// v2 handshake — acknowledge with welcome echoing the negotiated
|
|
880
|
+
// capability intersection. The gateway advertises only what it
|
|
881
|
+
// implements end-to-end: 'ws-relay' (binary WsData relay to/from DO
|
|
882
|
+
// sockets) and 'http-transfer' (the R2 transfer HTTP side-channel).
|
|
883
|
+
// 'streams' is intentionally NOT advertised — large DO/service-fetch
|
|
884
|
+
// responses are inlined, not streamed, in this gateway.
|
|
885
|
+
ws.send(JSON.stringify({
|
|
886
|
+
t: 'welcome',
|
|
887
|
+
protocolVersion: 2,
|
|
888
|
+
capabilities: ['ws-relay', 'http-transfer']
|
|
889
|
+
.filter((c) => Array.isArray(msg.capabilities) && msg.capabilities.includes(c))
|
|
890
|
+
.sort()
|
|
891
|
+
}))
|
|
892
|
+
break
|
|
893
|
+
case 'rpc.call':
|
|
894
|
+
await handleBridgeRpcCall(msg, ws, env, ctx)
|
|
895
|
+
break
|
|
896
|
+
case 'ws.open':
|
|
897
|
+
await handleBridgeWsOpen(msg, ws, env, wsProxies)
|
|
898
|
+
break
|
|
899
|
+
case 'ws.close':
|
|
900
|
+
handleBridgeWsClose(msg, wsProxies)
|
|
901
|
+
break
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function handleBridgeWebSocket(request, env, ctx) {
|
|
906
|
+
const { 0: client, 1: server } = new WebSocketPair()
|
|
907
|
+
server.accept()
|
|
908
|
+
|
|
909
|
+
// Per-connection state: recreated for every bridge client so reloads and
|
|
910
|
+
// concurrent clients never share WS proxy entries.
|
|
911
|
+
const wsProxies = new Map()
|
|
912
|
+
|
|
913
|
+
server.addEventListener('message', async (event) => {
|
|
914
|
+
try {
|
|
915
|
+
if (typeof event.data === 'string') {
|
|
916
|
+
await handleBridgeJsonMessage(event.data, server, env, ctx, wsProxies)
|
|
917
|
+
} else {
|
|
918
|
+
handleBridgeBinaryMessage(event.data, wsProxies)
|
|
919
|
+
}
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.error('[Gateway] Error:', error)
|
|
922
|
+
}
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
server.addEventListener('close', () => {
|
|
926
|
+
for (const proxy of wsProxies.values()) {
|
|
927
|
+
// Best-effort cleanup: the DO-side WS may already be closed or in an
|
|
928
|
+
// invalid state; any throw here would abort sibling closes. Surface
|
|
929
|
+
// the swallowed error when DEVFLARE_DEBUG_BRIDGE is enabled.
|
|
930
|
+
try { proxy.doWs.close() } catch (error) {
|
|
931
|
+
if (globalThis.DEVFLARE_DEBUG_BRIDGE) {
|
|
932
|
+
console.warn('[devflare:bridge] proxy.doWs.close() failed', error)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
wsProxies.clear()
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
return new Response(null, { status: 101, webSocket: client })
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// ---------------------------------------------------------------------------
|
|
943
|
+
// HTTP transfer for R2 bodies (shared with src/bridge/server.ts in shape)
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
|
|
946
|
+
async function handleHttpTransfer(request, env, url) {
|
|
947
|
+
const transferIdEncoded = url.pathname.split('/').pop()
|
|
948
|
+
const transferId = decodeURIComponent(transferIdEncoded || '')
|
|
949
|
+
const [binding, ...keyParts] = transferId.split(':')
|
|
950
|
+
const key = keyParts.join(':')
|
|
951
|
+
const bucket = env[binding]
|
|
952
|
+
|
|
953
|
+
if (!bucket) return new Response('Bucket not found: ' + binding, { status: 404 })
|
|
954
|
+
|
|
955
|
+
if (request.method === 'PUT' || request.method === 'POST') {
|
|
956
|
+
const result = await bucket.put(key, request.body)
|
|
957
|
+
return new Response(JSON.stringify(serializeR2Object(result)), {
|
|
958
|
+
headers: { 'Content-Type': 'application/json' }
|
|
959
|
+
})
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (request.method === 'GET') {
|
|
963
|
+
const object = await bucket.get(key)
|
|
964
|
+
if (!object) return new Response('Not found', { status: 404 })
|
|
965
|
+
return new Response(object.body, {
|
|
966
|
+
headers: {
|
|
967
|
+
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
|
|
968
|
+
'Content-Length': String(object.size)
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return new Response('Method not allowed', { status: 405 })
|
|
974
|
+
}
|
|
975
|
+
`;
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/bridge/miniflare.ts
|
|
978
|
+
function isIgnorableMiniflareDisposeError(error) {
|
|
979
|
+
if (!(error instanceof Error)) return false;
|
|
980
|
+
const details = error;
|
|
981
|
+
return details.code === "EBADF" && details.syscall === "kill";
|
|
982
|
+
}
|
|
983
|
+
//#endregion
|
|
984
|
+
//#region src/cli/wrangler-auth.ts
|
|
985
|
+
const execAsync = promisify(exec);
|
|
986
|
+
/**
|
|
987
|
+
* Check if the config contains any bindings that require remote access
|
|
988
|
+
*/
|
|
989
|
+
function detectRemoteBindings(config) {
|
|
990
|
+
const remoteBindings = [];
|
|
991
|
+
const bindings = config.bindings;
|
|
992
|
+
if (!bindings) return remoteBindings;
|
|
993
|
+
if (bindings.ai) remoteBindings.push(`AI (binding: ${bindings.ai.binding})`);
|
|
994
|
+
if (bindings.vectorize) for (const [name] of Object.entries(bindings.vectorize)) remoteBindings.push(`Vectorize (binding: ${name})`);
|
|
995
|
+
return remoteBindings;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Check if wrangler is logged in by running `wrangler whoami`
|
|
999
|
+
*
|
|
1000
|
+
* Returns:
|
|
1001
|
+
* - loggedIn: true if user is authenticated
|
|
1002
|
+
* - accountId: the account ID if available
|
|
1003
|
+
* - email: the email if available
|
|
1004
|
+
* - error: error message if check failed
|
|
1005
|
+
*/
|
|
1006
|
+
async function checkWranglerAuth() {
|
|
1007
|
+
try {
|
|
1008
|
+
const { stdout, stderr } = await execAsync("bunx wrangler whoami", { timeout: 15e3 });
|
|
1009
|
+
const output = stdout + stderr;
|
|
1010
|
+
if (output.includes("not authenticated") || output.includes("Not logged in") || output.includes("wrangler login")) return {
|
|
1011
|
+
loggedIn: false,
|
|
1012
|
+
error: "Not logged in to Wrangler"
|
|
1013
|
+
};
|
|
1014
|
+
const emailMatch = output.match(/email[:\s]+([^\s!]+)/i);
|
|
1015
|
+
const accountMatch = output.match(/Account\s+ID[:\s]+([a-f0-9]+)/i);
|
|
1016
|
+
return {
|
|
1017
|
+
loggedIn: true,
|
|
1018
|
+
email: emailMatch?.[1],
|
|
1019
|
+
accountId: accountMatch?.[1]
|
|
1020
|
+
};
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1023
|
+
if (msg.includes("ENOENT") || msg.includes("not found")) return {
|
|
1024
|
+
loggedIn: false,
|
|
1025
|
+
error: "Wrangler not installed. Run: npm install -g wrangler"
|
|
1026
|
+
};
|
|
1027
|
+
return {
|
|
1028
|
+
loggedIn: false,
|
|
1029
|
+
error: msg
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Check if remote bindings are properly configured
|
|
1035
|
+
*
|
|
1036
|
+
* Returns warnings if:
|
|
1037
|
+
* - Config has remote-only bindings but no accountId
|
|
1038
|
+
* - Config has remote-only bindings but user is not logged in to wrangler
|
|
1039
|
+
*/
|
|
1040
|
+
async function checkRemoteBindingRequirements(config) {
|
|
1041
|
+
const remoteBindings = detectRemoteBindings(config);
|
|
1042
|
+
if (remoteBindings.length === 0) return {
|
|
1043
|
+
hasRemoteBindings: false,
|
|
1044
|
+
remoteBindings: [],
|
|
1045
|
+
missingAccountId: false,
|
|
1046
|
+
notLoggedIn: false
|
|
1047
|
+
};
|
|
1048
|
+
return {
|
|
1049
|
+
hasRemoteBindings: true,
|
|
1050
|
+
remoteBindings,
|
|
1051
|
+
missingAccountId: !config.accountId,
|
|
1052
|
+
notLoggedIn: !(await checkWranglerAuth()).loggedIn
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/dev-server/d1-migrations.ts
|
|
1057
|
+
const MIGRATION_RETRY_DELAYS_MS = [
|
|
1058
|
+
500,
|
|
1059
|
+
1e3,
|
|
1060
|
+
1500,
|
|
1061
|
+
2e3
|
|
1062
|
+
];
|
|
1063
|
+
function collectMigrationStatements(sql) {
|
|
1064
|
+
return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n").split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1065
|
+
}
|
|
1066
|
+
function hashSql(sql) {
|
|
1067
|
+
return createHash("sha256").update(sql).digest("hex");
|
|
1068
|
+
}
|
|
1069
|
+
function getErrorMessage(error) {
|
|
1070
|
+
return error instanceof Error ? error.message : String(error);
|
|
1071
|
+
}
|
|
1072
|
+
async function waitForRetry(delayMs) {
|
|
1073
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, delayMs));
|
|
1074
|
+
}
|
|
1075
|
+
async function applyMigrationsToBinding(options) {
|
|
1076
|
+
const { bindingName, statements, files, miniflarePort, logger } = options;
|
|
1077
|
+
let lastError;
|
|
1078
|
+
for (let attempt = 0; attempt <= MIGRATION_RETRY_DELAYS_MS.length; attempt++) {
|
|
1079
|
+
if (attempt > 0) await waitForRetry(MIGRATION_RETRY_DELAYS_MS[attempt - 1]);
|
|
1080
|
+
try {
|
|
1081
|
+
const response = await fetch(`http://127.0.0.1:${miniflarePort}/_devflare/migrate`, {
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
headers: { "Content-Type": "application/json" },
|
|
1084
|
+
body: JSON.stringify({
|
|
1085
|
+
bindingName,
|
|
1086
|
+
statements,
|
|
1087
|
+
files
|
|
1088
|
+
})
|
|
1089
|
+
});
|
|
1090
|
+
if (!response.ok) {
|
|
1091
|
+
const text = await response.text();
|
|
1092
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1093
|
+
}
|
|
1094
|
+
const result = await response.json();
|
|
1095
|
+
if (result.success) {
|
|
1096
|
+
if (Array.isArray(result.warnings)) for (const warning of result.warnings) console.warn(`[devflare] D1 migration file "${warning.filename}" for binding ${bindingName} has changed since it was applied; skipping re-apply to protect existing data.`);
|
|
1097
|
+
const appliedCount = result.applied?.length ?? 0;
|
|
1098
|
+
const skippedCount = result.skipped?.length ?? 0;
|
|
1099
|
+
if (appliedCount > 0 || skippedCount > 0) logger?.success(`D1 migrations for ${bindingName}: ${appliedCount} applied, ${skippedCount} skipped`);
|
|
1100
|
+
else logger?.success(`D1 migrations applied to ${bindingName}`);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
throw new Error(result.error || "Unknown error");
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
lastError = error;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
logger?.warn(`Failed to apply migrations to ${bindingName}: ${getErrorMessage(lastError)}`);
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Run D1 migrations from migrations/ directory.
|
|
1112
|
+
*
|
|
1113
|
+
* Resolution per D1 binding (in order):
|
|
1114
|
+
* 1. `<cwd>/migrations/<BINDING_NAME>/*.sql` — per-binding directory.
|
|
1115
|
+
* NOTE: if the per-binding directory EXISTS but contains no .sql files,
|
|
1116
|
+
* the binding is skipped — the shared fallback is NOT used.
|
|
1117
|
+
* 2. `<cwd>/migrations/*.sql` — shared fallback, used ONLY when the
|
|
1118
|
+
* per-binding directory does not exist.
|
|
1119
|
+
* 3. Otherwise, skip the binding with a debug log.
|
|
1120
|
+
*
|
|
1121
|
+
* Applied-migration ledger:
|
|
1122
|
+
* The gateway maintains a `_devflare_migrations` table per D1 binding with
|
|
1123
|
+
* columns (filename TEXT PRIMARY KEY, applied_at TEXT NOT NULL,
|
|
1124
|
+
* sha256 TEXT NOT NULL). On each run the gateway, for every file we send:
|
|
1125
|
+
* - if filename is present AND sha256 matches — skip silently
|
|
1126
|
+
* - if filename is present but sha256 differs — surface a warning that
|
|
1127
|
+
* the client turns into a `console.warn`; the file is skipped to
|
|
1128
|
+
* protect existing data (there is no force-reapply flag today)
|
|
1129
|
+
* - if filename is absent — apply the file's SQL, then record the entry
|
|
1130
|
+
* We pass each file (filename + sha256 + statements) in a single
|
|
1131
|
+
* /_devflare/migrate request so ledger read/write stays in-process inside
|
|
1132
|
+
* workerd.
|
|
1133
|
+
*
|
|
1134
|
+
* Uses the gateway worker HTTP endpoint to run migrations inside workerd.
|
|
1135
|
+
*/
|
|
1136
|
+
async function runD1Migrations(options) {
|
|
1137
|
+
const { cwd, config, miniflarePort, logger } = options;
|
|
1138
|
+
if (!config?.bindings?.d1) return;
|
|
1139
|
+
const { existsSync, readdirSync, readFileSync, statSync } = await import("node:fs");
|
|
1140
|
+
const migrationsDir = resolve(cwd, "migrations");
|
|
1141
|
+
if (!existsSync(migrationsDir)) {
|
|
1142
|
+
logger?.debug("No migrations/ directory found, skipping D1 migrations");
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const sharedFiles = readdirSync(migrationsDir).filter((file) => file.endsWith(".sql")).sort();
|
|
1146
|
+
let sharedFileEntries = null;
|
|
1147
|
+
if (sharedFiles.length > 0) {
|
|
1148
|
+
sharedFileEntries = [];
|
|
1149
|
+
for (const file of sharedFiles) {
|
|
1150
|
+
const sql = readFileSync(resolve(migrationsDir, file), "utf-8");
|
|
1151
|
+
const fileStatements = collectMigrationStatements(sql);
|
|
1152
|
+
sharedFileEntries.push({
|
|
1153
|
+
filename: file,
|
|
1154
|
+
sha256: hashSql(sql),
|
|
1155
|
+
statements: fileStatements
|
|
1156
|
+
});
|
|
1157
|
+
logger?.debug(`Shared file ${file}: ${fileStatements.length} statement(s)`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
for (const [bindingName] of Object.entries(config.bindings.d1)) {
|
|
1161
|
+
const perBindingDir = resolve(migrationsDir, bindingName);
|
|
1162
|
+
const hasPerBindingDir = existsSync(perBindingDir) && statSync(perBindingDir).isDirectory();
|
|
1163
|
+
let files = [];
|
|
1164
|
+
let sourceLabel = "";
|
|
1165
|
+
if (hasPerBindingDir) {
|
|
1166
|
+
const perBindingFiles = readdirSync(perBindingDir).filter((file) => file.endsWith(".sql")).sort();
|
|
1167
|
+
if (perBindingFiles.length === 0) {
|
|
1168
|
+
logger?.debug(`No SQL migration files in migrations/${bindingName}/, skipping ${bindingName}`);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
for (const file of perBindingFiles) {
|
|
1172
|
+
const sql = readFileSync(resolve(perBindingDir, file), "utf-8");
|
|
1173
|
+
const fileStatements = collectMigrationStatements(sql);
|
|
1174
|
+
files.push({
|
|
1175
|
+
filename: file,
|
|
1176
|
+
sha256: hashSql(sql),
|
|
1177
|
+
statements: fileStatements
|
|
1178
|
+
});
|
|
1179
|
+
logger?.debug(`File ${bindingName}/${file}: ${fileStatements.length} statement(s)`);
|
|
1180
|
+
}
|
|
1181
|
+
sourceLabel = `migrations/${bindingName}/`;
|
|
1182
|
+
} else if (sharedFileEntries !== null) {
|
|
1183
|
+
files = sharedFileEntries;
|
|
1184
|
+
sourceLabel = "migrations/ [shared fallback]";
|
|
1185
|
+
} else {
|
|
1186
|
+
logger?.debug(`No migrations found for ${bindingName}, skipping`);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
const statements = files.flatMap((file) => file.statements);
|
|
1190
|
+
logger?.info(`Running ${files.length} D1 migration(s) for ${bindingName} (from ${sourceLabel})`);
|
|
1191
|
+
if (statements.length === 0) {
|
|
1192
|
+
logger?.debug(`No executable D1 migration statements for ${bindingName}`);
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
await applyMigrationsToBinding({
|
|
1196
|
+
bindingName,
|
|
1197
|
+
statements,
|
|
1198
|
+
files,
|
|
1199
|
+
miniflarePort,
|
|
1200
|
+
logger
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
//#endregion
|
|
1205
|
+
//#region src/dev-server/dev-server-state.ts
|
|
1206
|
+
/**
|
|
1207
|
+
* Build a fresh `DevServerState` with all handles in their not-yet-started
|
|
1208
|
+
* positions. `enableVite` is the user's request — `start()` may downgrade it
|
|
1209
|
+
* later via `resolveViteIntegration`.
|
|
1210
|
+
*/
|
|
1211
|
+
function createDevServerState(initial) {
|
|
1212
|
+
return {
|
|
1213
|
+
enableVite: initial.enableVite,
|
|
1214
|
+
miniflare: null,
|
|
1215
|
+
doBundler: null,
|
|
1216
|
+
workerSourceWatcher: null,
|
|
1217
|
+
workerWatchTargets: [],
|
|
1218
|
+
viteProcess: null,
|
|
1219
|
+
config: null,
|
|
1220
|
+
serviceBindingResolution: null,
|
|
1221
|
+
browserShim: null,
|
|
1222
|
+
browserShimPort: initial.browserShimPort ?? 8788,
|
|
1223
|
+
mainWorkerSurfacePaths: {
|
|
1224
|
+
fetch: null,
|
|
1225
|
+
queue: null,
|
|
1226
|
+
scheduled: null,
|
|
1227
|
+
email: null,
|
|
1228
|
+
tail: null
|
|
1229
|
+
},
|
|
1230
|
+
resolvedWorkerConfigPath: null,
|
|
1231
|
+
mainWorkerScriptPath: null,
|
|
1232
|
+
bundledMainWorkerScriptPath: null,
|
|
1233
|
+
workflowEntrypointScript: "",
|
|
1234
|
+
currentDoResult: null,
|
|
1235
|
+
mainWorkerRoutes: null,
|
|
1236
|
+
generatedViteConfigPath: null
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Tear down everything in `state` in the same order the legacy `stop()`
|
|
1241
|
+
* function used. After this returns, every handle on `state` is `null` again
|
|
1242
|
+
* and the local sendEmail bindings have been cleared.
|
|
1243
|
+
*
|
|
1244
|
+
* Order is important and intentionally mirrors the production sequence:
|
|
1245
|
+
* 1. DO bundler (stop rebuilds before Miniflare goes away)
|
|
1246
|
+
* 2. Worker source watcher (stop FS callbacks before Miniflare disposal)
|
|
1247
|
+
* 3. Miniflare itself
|
|
1248
|
+
* 4. Vite child process (after Miniflare so requests cannot race shutdown)
|
|
1249
|
+
* 5. Browser shim
|
|
1250
|
+
* 6. Local sendEmail registry reset
|
|
1251
|
+
*/
|
|
1252
|
+
async function disposeDevServerState(state) {
|
|
1253
|
+
if (state.doBundler) {
|
|
1254
|
+
await state.doBundler.close();
|
|
1255
|
+
state.doBundler = null;
|
|
1256
|
+
}
|
|
1257
|
+
if (state.workerSourceWatcher) {
|
|
1258
|
+
await state.workerSourceWatcher.close();
|
|
1259
|
+
state.workerSourceWatcher = null;
|
|
1260
|
+
}
|
|
1261
|
+
if (state.miniflare) {
|
|
1262
|
+
try {
|
|
1263
|
+
await state.miniflare.dispose();
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
if (!isIgnorableMiniflareDisposeError(error)) throw error;
|
|
1266
|
+
}
|
|
1267
|
+
state.miniflare = null;
|
|
1268
|
+
}
|
|
1269
|
+
if (state.viteProcess) {
|
|
1270
|
+
await stopSpawnedProcessTree(state.viteProcess);
|
|
1271
|
+
state.viteProcess = null;
|
|
1272
|
+
}
|
|
1273
|
+
if (state.browserShim) {
|
|
1274
|
+
await state.browserShim.stop();
|
|
1275
|
+
state.browserShim = null;
|
|
1276
|
+
}
|
|
1277
|
+
clearLocalSendEmailBindings();
|
|
1278
|
+
}
|
|
1279
|
+
//#endregion
|
|
1280
|
+
//#region src/browser-shim/binding-worker.ts
|
|
1281
|
+
const MAX_CHUNK_SIZE = 1048575;
|
|
1282
|
+
/**
|
|
1283
|
+
* Generate the browser binding worker script
|
|
1284
|
+
* @param browserShimUrl - URL of the external browser shim server (e.g., http://127.0.0.1:8788)
|
|
1285
|
+
* @param debug - Enable debug logging (default: false)
|
|
1286
|
+
*/
|
|
1287
|
+
function getBrowserBindingScript(browserShimUrl, debug = false) {
|
|
1288
|
+
return `
|
|
1289
|
+
// Browser Binding Worker — Proxies puppeteer requests to external browser shim
|
|
1290
|
+
// Handles WebSocket upgrades using WebSocketPair for @cloudflare/puppeteer compatibility
|
|
1291
|
+
|
|
1292
|
+
const BROWSER_SHIM_URL = ${JSON.stringify(browserShimUrl)}
|
|
1293
|
+
const MAX_CHUNK_SIZE = ${MAX_CHUNK_SIZE}
|
|
1294
|
+
const DEBUG = ${debug}
|
|
1295
|
+
const log = (...args) => DEBUG && console.log('[BrowserBinding]', ...args)
|
|
1296
|
+
|
|
1297
|
+
export default {
|
|
1298
|
+
async fetch(request, env, ctx) {
|
|
1299
|
+
const url = new URL(request.url)
|
|
1300
|
+
const upgradeHeader = request.headers.get('Upgrade')
|
|
1301
|
+
const isWebSocket = upgradeHeader && upgradeHeader.toLowerCase() === 'websocket'
|
|
1302
|
+
|
|
1303
|
+
log('Request:', url.pathname, isWebSocket ? '(WebSocket)' : '(HTTP)')
|
|
1304
|
+
|
|
1305
|
+
// Handle WebSocket upgrade for DevTools connection
|
|
1306
|
+
if (url.pathname === '/v1/connectDevtools' && isWebSocket) {
|
|
1307
|
+
return handleDevToolsWebSocket(request, url)
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Proxy all other requests to the browser shim server
|
|
1311
|
+
return proxyToBrowserShim(request, url)
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Proxy HTTP requests to the external browser shim server
|
|
1316
|
+
async function proxyToBrowserShim(request, url) {
|
|
1317
|
+
const shimUrl = new URL(url.pathname + url.search, BROWSER_SHIM_URL)
|
|
1318
|
+
|
|
1319
|
+
log('Proxying to:', shimUrl.toString())
|
|
1320
|
+
|
|
1321
|
+
const response = await fetch(shimUrl.toString(), {
|
|
1322
|
+
method: request.method,
|
|
1323
|
+
headers: request.headers,
|
|
1324
|
+
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
log('Response:', response.status)
|
|
1328
|
+
|
|
1329
|
+
// Return the response as-is
|
|
1330
|
+
return new Response(response.body, {
|
|
1331
|
+
status: response.status,
|
|
1332
|
+
statusText: response.statusText,
|
|
1333
|
+
headers: response.headers
|
|
1334
|
+
})
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Validate WebSocket close code to be in valid range
|
|
1338
|
+
function validateCloseCode(code) {
|
|
1339
|
+
if (typeof code !== 'number' || isNaN(code)) return 1000
|
|
1340
|
+
if (code < 1000 || code > 4999) return 1000
|
|
1341
|
+
return code
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Split a message into chunks following @cloudflare/puppeteer protocol
|
|
1345
|
+
// First chunk has 4-byte LE length header, subsequent chunks are raw payload
|
|
1346
|
+
function messageToChunks(message) {
|
|
1347
|
+
const data = typeof message === 'string'
|
|
1348
|
+
? new TextEncoder().encode(message)
|
|
1349
|
+
: new Uint8Array(message)
|
|
1350
|
+
|
|
1351
|
+
const chunks = []
|
|
1352
|
+
const totalLength = data.length
|
|
1353
|
+
let offset = 0
|
|
1354
|
+
let isFirst = true
|
|
1355
|
+
|
|
1356
|
+
while (offset < totalLength) {
|
|
1357
|
+
const remaining = totalLength - offset
|
|
1358
|
+
let chunkSize
|
|
1359
|
+
|
|
1360
|
+
if (isFirst) {
|
|
1361
|
+
// First chunk: 4-byte header + payload
|
|
1362
|
+
chunkSize = Math.min(remaining, MAX_CHUNK_SIZE - 4)
|
|
1363
|
+
const chunk = new Uint8Array(chunkSize + 4)
|
|
1364
|
+
new DataView(chunk.buffer).setUint32(0, totalLength, true) // little-endian
|
|
1365
|
+
chunk.set(data.subarray(offset, offset + chunkSize), 4)
|
|
1366
|
+
chunks.push(chunk)
|
|
1367
|
+
isFirst = false
|
|
1368
|
+
} else {
|
|
1369
|
+
// Subsequent chunks: raw payload only
|
|
1370
|
+
chunkSize = Math.min(remaining, MAX_CHUNK_SIZE)
|
|
1371
|
+
const chunk = data.subarray(offset, offset + chunkSize)
|
|
1372
|
+
chunks.push(chunk)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
offset += chunkSize
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
return chunks
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Reassemble chunks back into a complete message
|
|
1382
|
+
// Returns null if more chunks are needed
|
|
1383
|
+
function chunksToMessage(chunks) {
|
|
1384
|
+
if (chunks.length === 0) return null
|
|
1385
|
+
|
|
1386
|
+
// First chunk must have 4-byte header
|
|
1387
|
+
const firstChunk = chunks[0]
|
|
1388
|
+
if (firstChunk.length < 4) return null
|
|
1389
|
+
|
|
1390
|
+
const expectedLength = new DataView(firstChunk.buffer, firstChunk.byteOffset).getUint32(0, true)
|
|
1391
|
+
|
|
1392
|
+
// Calculate total received payload
|
|
1393
|
+
let totalReceived = firstChunk.length - 4 // first chunk payload (minus header)
|
|
1394
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
1395
|
+
totalReceived += chunks[i].length
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (totalReceived < expectedLength) {
|
|
1399
|
+
return null // Need more chunks
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Reassemble the message
|
|
1403
|
+
const assembled = new Uint8Array(expectedLength)
|
|
1404
|
+
let offset = 0
|
|
1405
|
+
|
|
1406
|
+
// Copy first chunk payload (skip 4-byte header)
|
|
1407
|
+
const firstPayload = firstChunk.subarray(4)
|
|
1408
|
+
assembled.set(firstPayload, offset)
|
|
1409
|
+
offset += firstPayload.length
|
|
1410
|
+
|
|
1411
|
+
// Copy remaining chunks
|
|
1412
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
1413
|
+
const chunk = chunks[i]
|
|
1414
|
+
const toCopy = Math.min(chunk.length, expectedLength - offset)
|
|
1415
|
+
assembled.set(chunk.subarray(0, toCopy), offset)
|
|
1416
|
+
offset += toCopy
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
return new TextDecoder().decode(assembled)
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Handle WebSocket upgrade for DevTools connection
|
|
1423
|
+
// Creates a WebSocketPair and proxies to Chrome's DevTools WebSocket
|
|
1424
|
+
async function handleDevToolsWebSocket(request, url) {
|
|
1425
|
+
const sessionId = url.searchParams.get('browser_session')
|
|
1426
|
+
if (!sessionId) {
|
|
1427
|
+
return new Response('browser_session parameter required', { status: 400 })
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
log('DevTools WebSocket request for session:', sessionId)
|
|
1431
|
+
|
|
1432
|
+
// Get session info from browser shim (includes Chrome's wsEndpoint)
|
|
1433
|
+
const sessionUrl = new URL('/v1/session/' + sessionId, BROWSER_SHIM_URL)
|
|
1434
|
+
|
|
1435
|
+
// Add timeout for session fetch
|
|
1436
|
+
const controller = new AbortController()
|
|
1437
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
1438
|
+
|
|
1439
|
+
let sessionRes
|
|
1440
|
+
try {
|
|
1441
|
+
sessionRes = await fetch(sessionUrl.toString(), { signal: controller.signal })
|
|
1442
|
+
} catch (e) {
|
|
1443
|
+
DEBUG && console.error('[BrowserBinding] Session fetch timeout or error:', e.message)
|
|
1444
|
+
return new Response('Session fetch timeout', { status: 504 })
|
|
1445
|
+
} finally {
|
|
1446
|
+
clearTimeout(timeout)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (!sessionRes.ok) {
|
|
1450
|
+
DEBUG && console.error('[BrowserBinding] Session not found:', sessionId)
|
|
1451
|
+
return new Response('Session not found', { status: 404 })
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const sessionInfo = await sessionRes.json()
|
|
1455
|
+
const wsEndpoint = sessionInfo.wsEndpoint
|
|
1456
|
+
|
|
1457
|
+
if (!wsEndpoint) {
|
|
1458
|
+
DEBUG && console.error('[BrowserBinding] No wsEndpoint in session info')
|
|
1459
|
+
return new Response('No wsEndpoint for session', { status: 500 })
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
log('Connecting to Chrome DevTools:', wsEndpoint)
|
|
1463
|
+
|
|
1464
|
+
// Connect to Chrome's DevTools WebSocket
|
|
1465
|
+
// Chrome uses ws:// but fetch expects http:// for WebSocket upgrade
|
|
1466
|
+
const chromeUrl = wsEndpoint.replace('ws://', 'http://').replace('wss://', 'https://')
|
|
1467
|
+
|
|
1468
|
+
const chromeRes = await fetch(chromeUrl, {
|
|
1469
|
+
headers: { Upgrade: 'websocket' }
|
|
1470
|
+
})
|
|
1471
|
+
|
|
1472
|
+
if (!chromeRes.webSocket) {
|
|
1473
|
+
DEBUG && console.error('[BrowserBinding] Failed to connect to Chrome DevTools')
|
|
1474
|
+
return new Response('Failed to connect to Chrome DevTools', { status: 502 })
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const chromeWs = chromeRes.webSocket
|
|
1478
|
+
chromeWs.accept()
|
|
1479
|
+
|
|
1480
|
+
log('Connected to Chrome DevTools')
|
|
1481
|
+
|
|
1482
|
+
// Create WebSocketPair for client connection
|
|
1483
|
+
const { 0: client, 1: server } = new WebSocketPair()
|
|
1484
|
+
server.accept()
|
|
1485
|
+
|
|
1486
|
+
// Chunk buffer for reassembling multi-chunk messages from puppeteer
|
|
1487
|
+
let chunks = []
|
|
1488
|
+
const MAX_BUFFER_SIZE = 50 * 1024 * 1024 // 50MB max buffer
|
|
1489
|
+
let bufferSize = 0
|
|
1490
|
+
|
|
1491
|
+
// Proxy messages from client (puppeteer) to Chrome
|
|
1492
|
+
// Handle multi-chunk framing protocol
|
|
1493
|
+
server.addEventListener('message', (event) => {
|
|
1494
|
+
// Keep-alive ping from puppeteer
|
|
1495
|
+
if (event.data === 'ping') {
|
|
1496
|
+
return
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Handle binary data (chunked protocol)
|
|
1500
|
+
if (event.data instanceof ArrayBuffer) {
|
|
1501
|
+
const chunk = new Uint8Array(event.data)
|
|
1502
|
+
bufferSize += chunk.length
|
|
1503
|
+
|
|
1504
|
+
// Prevent unbounded buffering
|
|
1505
|
+
if (bufferSize > MAX_BUFFER_SIZE) {
|
|
1506
|
+
DEBUG && console.error('[BrowserBinding] Buffer overflow, closing connection')
|
|
1507
|
+
server.close(1009, 'Message too big')
|
|
1508
|
+
chromeWs.close(1009, 'Message too big')
|
|
1509
|
+
return
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
chunks.push(chunk)
|
|
1513
|
+
|
|
1514
|
+
// Try to reassemble complete message
|
|
1515
|
+
const message = chunksToMessage(chunks)
|
|
1516
|
+
if (message !== null) {
|
|
1517
|
+
// Send complete message to Chrome
|
|
1518
|
+
if (chromeWs.readyState === 1) { // OPEN
|
|
1519
|
+
chromeWs.send(message)
|
|
1520
|
+
}
|
|
1521
|
+
// Clear buffer
|
|
1522
|
+
chunks = []
|
|
1523
|
+
bufferSize = 0
|
|
1524
|
+
}
|
|
1525
|
+
} else if (typeof event.data === 'string') {
|
|
1526
|
+
// Shouldn't happen in normal protocol, but handle it
|
|
1527
|
+
if (chromeWs.readyState === 1) {
|
|
1528
|
+
chromeWs.send(event.data)
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
})
|
|
1532
|
+
|
|
1533
|
+
// Proxy messages from Chrome to client (puppeteer)
|
|
1534
|
+
// Split into chunks following the multi-chunk protocol
|
|
1535
|
+
chromeWs.addEventListener('message', (event) => {
|
|
1536
|
+
if (server.readyState !== 1) return // Not OPEN
|
|
1537
|
+
|
|
1538
|
+
// Split message into chunks
|
|
1539
|
+
const outChunks = messageToChunks(event.data)
|
|
1540
|
+
for (const chunk of outChunks) {
|
|
1541
|
+
server.send(chunk)
|
|
1542
|
+
}
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
// Handle close events with validated codes
|
|
1546
|
+
server.addEventListener('close', (event) => {
|
|
1547
|
+
log('Client WebSocket closed:', event.code)
|
|
1548
|
+
const code = validateCloseCode(event.code)
|
|
1549
|
+
try {
|
|
1550
|
+
if (chromeWs.readyState === 1 || chromeWs.readyState === 0) {
|
|
1551
|
+
chromeWs.close(code, event.reason || '')
|
|
1552
|
+
}
|
|
1553
|
+
} catch {}
|
|
1554
|
+
})
|
|
1555
|
+
|
|
1556
|
+
chromeWs.addEventListener('close', (event) => {
|
|
1557
|
+
log('Chrome WebSocket closed:', event.code)
|
|
1558
|
+
const code = validateCloseCode(event.code)
|
|
1559
|
+
try {
|
|
1560
|
+
if (server.readyState === 1 || server.readyState === 0) {
|
|
1561
|
+
server.close(code, event.reason || '')
|
|
1562
|
+
}
|
|
1563
|
+
} catch {}
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
// Handle errors
|
|
1567
|
+
server.addEventListener('error', (event) => {
|
|
1568
|
+
DEBUG && console.error('[BrowserBinding] Client WebSocket error')
|
|
1569
|
+
try { chromeWs.close(1011, 'Client error') } catch {}
|
|
1570
|
+
})
|
|
1571
|
+
|
|
1572
|
+
chromeWs.addEventListener('error', (event) => {
|
|
1573
|
+
DEBUG && console.error('[BrowserBinding] Chrome WebSocket error')
|
|
1574
|
+
try { server.close(1011, 'Chrome error') } catch {}
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
log('WebSocket proxy established')
|
|
1578
|
+
|
|
1579
|
+
// Return Cloudflare-style WebSocket response
|
|
1580
|
+
return new Response(null, {
|
|
1581
|
+
status: 101,
|
|
1582
|
+
webSocket: client
|
|
1583
|
+
})
|
|
1584
|
+
}
|
|
1585
|
+
`;
|
|
1586
|
+
}
|
|
1587
|
+
//#endregion
|
|
1588
|
+
//#region src/dev-server/gateway-script.ts
|
|
1589
|
+
/**
|
|
1590
|
+
* Generates the dev-server gateway worker script inline.
|
|
1591
|
+
*
|
|
1592
|
+
* All in-sandbox RPC behavior (method dispatch, error envelope, serialization,
|
|
1593
|
+
* WebSocket bridge, HTTP transfer) lives in `GATEWAY_RUNTIME_JS` and is shared
|
|
1594
|
+
* with `src/bridge/miniflare.ts`. The canonical TypeScript equivalent lives in
|
|
1595
|
+
* `src/bridge/server.ts`.
|
|
1596
|
+
*
|
|
1597
|
+
* This file only owns the pieces that are genuinely dev-server-specific:
|
|
1598
|
+
* - WebSocket route matching & DO WebSocket forwarding (`WS_ROUTES`)
|
|
1599
|
+
* - D1 migration endpoint
|
|
1600
|
+
* - Inbound email ingestion endpoint
|
|
1601
|
+
* - Service-binding fallthrough to the app worker
|
|
1602
|
+
*
|
|
1603
|
+
* @param wsRoutes - WebSocket routes for DO proxying
|
|
1604
|
+
* @param debug - Enable debug logging in gateway
|
|
1605
|
+
* @param appServiceBindingName - Service binding name for the app worker (if any)
|
|
1606
|
+
*/
|
|
1607
|
+
function getGatewayScript(wsRoutes = [], debug = false, appServiceBindingName = null) {
|
|
1608
|
+
return `
|
|
1609
|
+
${GATEWAY_RUNTIME_JS}
|
|
1610
|
+
|
|
1611
|
+
// Bridge Gateway Worker — Dev Server
|
|
1612
|
+
// Dev-server-specific overlay on top of the shared GATEWAY_RUNTIME_JS:
|
|
1613
|
+
// WS route DO forwarding, D1 migration, email ingest, app-worker fallthrough.
|
|
1614
|
+
|
|
1615
|
+
const DEBUG = ${debug}
|
|
1616
|
+
const log = (...args) => DEBUG && console.log('[Gateway]', ...args)
|
|
1617
|
+
|
|
1618
|
+
const WS_ROUTES = ${JSON.stringify(wsRoutes)}
|
|
1619
|
+
const APP_SERVICE_BINDING = ${JSON.stringify(appServiceBindingName)}
|
|
1620
|
+
|
|
1621
|
+
export default {
|
|
1622
|
+
async fetch(request, env, ctx) {
|
|
1623
|
+
const url = new URL(request.url)
|
|
1624
|
+
const isWebSocket = request.headers.get('Upgrade') === 'websocket'
|
|
1625
|
+
|
|
1626
|
+
if (isWebSocket) {
|
|
1627
|
+
const matchedRoute = matchWsRoute(url.pathname)
|
|
1628
|
+
if (matchedRoute) {
|
|
1629
|
+
return handleDoWebSocket(request, env, url, matchedRoute)
|
|
1630
|
+
}
|
|
1631
|
+
return handleBridgeWebSocket(request, env, ctx)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
if (url.pathname.startsWith('/_devflare/transfer/')) {
|
|
1635
|
+
return handleHttpTransfer(request, env, url)
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (url.pathname === '/_devflare/migrate' && request.method === 'POST') {
|
|
1639
|
+
return handleMigration(request, env)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
if (url.pathname === '/cdn-cgi/handler/email' && request.method === 'POST') {
|
|
1643
|
+
return handleEmailIncoming(request, env, ctx, url)
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (url.pathname === '/_devflare/health') {
|
|
1647
|
+
return new Response(JSON.stringify({
|
|
1648
|
+
ok: true,
|
|
1649
|
+
bindings: Object.keys(env),
|
|
1650
|
+
wsRoutes: WS_ROUTES
|
|
1651
|
+
}), { headers: { 'Content-Type': 'application/json' } })
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
if (APP_SERVICE_BINDING) {
|
|
1655
|
+
const appWorker = env[APP_SERVICE_BINDING]
|
|
1656
|
+
if (appWorker && typeof appWorker.fetch === 'function') {
|
|
1657
|
+
return appWorker.fetch(request)
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return new Response('Devflare Bridge Gateway', { status: 200 })
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
async function handleMigration(request, env) {
|
|
1666
|
+
try {
|
|
1667
|
+
const { bindingName, statements, files } = await request.json()
|
|
1668
|
+
log('Migration request for binding:', bindingName, 'statements count:', statements?.length, 'files:', files?.length, 'bindings:', Object.keys(env))
|
|
1669
|
+
const db = env[bindingName]
|
|
1670
|
+
if (!db) {
|
|
1671
|
+
return Response.json({ error: 'Binding not found: ' + bindingName }, { status: 404 })
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
// Ledger-aware path: when the client sends per-file metadata, we track
|
|
1675
|
+
// applied migrations in a \`_devflare_migrations\` table and skip files
|
|
1676
|
+
// whose filename+sha256 is already recorded. Files with a drifting hash
|
|
1677
|
+
// are reported as warnings and skipped — we refuse to re-apply to avoid
|
|
1678
|
+
// stomping on user data.
|
|
1679
|
+
if (Array.isArray(files) && files.length > 0) {
|
|
1680
|
+
try {
|
|
1681
|
+
await db.prepare(
|
|
1682
|
+
'CREATE TABLE IF NOT EXISTS _devflare_migrations (filename TEXT PRIMARY KEY, applied_at TEXT NOT NULL, sha256 TEXT NOT NULL)'
|
|
1683
|
+
).run()
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
const msg = error?.message || String(error)
|
|
1686
|
+
log('Failed to ensure migration ledger:', msg)
|
|
1687
|
+
return Response.json({ error: 'Failed to ensure migration ledger: ' + msg }, { status: 500 })
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
let ledgerRows = []
|
|
1691
|
+
try {
|
|
1692
|
+
const ledger = await db.prepare('SELECT filename, sha256 FROM _devflare_migrations').all()
|
|
1693
|
+
ledgerRows = ledger?.results || []
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
log('Failed to read migration ledger:', error?.message || String(error))
|
|
1696
|
+
}
|
|
1697
|
+
const ledgerByFilename = new Map()
|
|
1698
|
+
for (const row of ledgerRows) {
|
|
1699
|
+
ledgerByFilename.set(row.filename, row.sha256)
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
const applied = []
|
|
1703
|
+
const skipped = []
|
|
1704
|
+
const warnings = []
|
|
1705
|
+
const results = []
|
|
1706
|
+
|
|
1707
|
+
for (const file of files) {
|
|
1708
|
+
const existingHash = ledgerByFilename.get(file.filename)
|
|
1709
|
+
if (existingHash === file.sha256) {
|
|
1710
|
+
skipped.push(file.filename)
|
|
1711
|
+
continue
|
|
1712
|
+
}
|
|
1713
|
+
if (existingHash && existingHash !== file.sha256) {
|
|
1714
|
+
warnings.push({
|
|
1715
|
+
filename: file.filename,
|
|
1716
|
+
message: 'sha256 drifted since last apply; skipped'
|
|
1717
|
+
})
|
|
1718
|
+
skipped.push(file.filename)
|
|
1719
|
+
continue
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
let fileFailed = false
|
|
1723
|
+
for (const sql of file.statements || []) {
|
|
1724
|
+
try {
|
|
1725
|
+
log('Running migration SQL:', sql.slice(0, 80))
|
|
1726
|
+
await db.prepare(sql).run()
|
|
1727
|
+
results.push({ sql: sql.slice(0, 50), success: true })
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
const msg = error?.message || String(error)
|
|
1730
|
+
log('Migration SQL error:', msg)
|
|
1731
|
+
if (msg.includes('already exists')) {
|
|
1732
|
+
results.push({ sql: sql.slice(0, 50), success: true, skipped: true })
|
|
1733
|
+
} else {
|
|
1734
|
+
results.push({ sql: sql.slice(0, 50), success: false, error: msg })
|
|
1735
|
+
fileFailed = true
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (!fileFailed) {
|
|
1741
|
+
try {
|
|
1742
|
+
await db.prepare(
|
|
1743
|
+
'INSERT OR REPLACE INTO _devflare_migrations (filename, applied_at, sha256) VALUES (?, ?, ?)'
|
|
1744
|
+
).bind(file.filename, new Date().toISOString(), file.sha256).run()
|
|
1745
|
+
applied.push(file.filename)
|
|
1746
|
+
} catch (error) {
|
|
1747
|
+
log('Failed to record migration in ledger:', error?.message || String(error))
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
return Response.json({ success: true, results, applied, skipped, warnings })
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Legacy path: flat statement list, no ledger tracking.
|
|
1756
|
+
const results = []
|
|
1757
|
+
for (const sql of statements) {
|
|
1758
|
+
try {
|
|
1759
|
+
log('Running migration SQL:', sql.slice(0, 80))
|
|
1760
|
+
await db.prepare(sql).run()
|
|
1761
|
+
results.push({ sql: sql.slice(0, 50), success: true })
|
|
1762
|
+
log('Migration SQL succeeded')
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
const msg = error?.message || String(error)
|
|
1765
|
+
log('Migration SQL error:', msg)
|
|
1766
|
+
if (msg.includes('already exists')) {
|
|
1767
|
+
results.push({ sql: sql.slice(0, 50), success: true, skipped: true })
|
|
1768
|
+
} else {
|
|
1769
|
+
results.push({ sql: sql.slice(0, 50), success: false, error: msg })
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
try {
|
|
1775
|
+
const tables = await db.prepare(\"SELECT name FROM sqlite_master WHERE type='table'\").all()
|
|
1776
|
+
log('Tables after migration:', JSON.stringify(tables))
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
log('Error listing tables:', e.message)
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return Response.json({ success: true, results })
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
return Response.json({ error: error?.message || String(error) }, { status: 500 })
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async function handleEmailIncoming(request, env, ctx, url) {
|
|
1788
|
+
try {
|
|
1789
|
+
const from = url.searchParams.get('from') || 'unknown@example.com'
|
|
1790
|
+
const to = url.searchParams.get('to') || 'worker@example.com'
|
|
1791
|
+
const rawBody = await request.text()
|
|
1792
|
+
|
|
1793
|
+
log('Email incoming:', { from, to, bodyLength: rawBody.length })
|
|
1794
|
+
|
|
1795
|
+
if (APP_SERVICE_BINDING) {
|
|
1796
|
+
const appWorker = env[APP_SERVICE_BINDING]
|
|
1797
|
+
if (appWorker && typeof appWorker.fetch === 'function') {
|
|
1798
|
+
const response = await appWorker.fetch(new Request('http://devflare.internal/_devflare/internal/email', {
|
|
1799
|
+
method: 'POST',
|
|
1800
|
+
headers: {
|
|
1801
|
+
'x-devflare-event': 'email',
|
|
1802
|
+
'x-devflare-email-from': from,
|
|
1803
|
+
'x-devflare-email-to': to,
|
|
1804
|
+
'content-type': request.headers.get('content-type') || 'text/plain'
|
|
1805
|
+
},
|
|
1806
|
+
body: rawBody
|
|
1807
|
+
}))
|
|
1808
|
+
|
|
1809
|
+
if (!response.ok) {
|
|
1810
|
+
return response
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
return new Response(JSON.stringify({ ok: true, from, to }), {
|
|
1816
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1817
|
+
})
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
console.error('[Gateway] Email handler error:', error)
|
|
1820
|
+
return Response.json({ error: error?.message || String(error) }, { status: 500 })
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function matchWsRoute(pathname) {
|
|
1825
|
+
for (const route of WS_ROUTES) {
|
|
1826
|
+
if (pathname === route.pattern || pathname.startsWith(route.pattern + '?')) {
|
|
1827
|
+
return route
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return null
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
async function handleDoWebSocket(request, env, url, route) {
|
|
1834
|
+
try {
|
|
1835
|
+
const namespace = env[route.doNamespace]
|
|
1836
|
+
if (!namespace) {
|
|
1837
|
+
console.error('[Gateway] DO namespace not found:', route.doNamespace)
|
|
1838
|
+
return new Response('DO namespace not found: ' + route.doNamespace, { status: 500 })
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
const idValue = url.searchParams.get(route.idParam) || 'default'
|
|
1842
|
+
const doId = namespace.idFromName(idValue)
|
|
1843
|
+
const stub = namespace.get(doId)
|
|
1844
|
+
|
|
1845
|
+
const forwardUrl = new URL(route.forwardPath, url.origin)
|
|
1846
|
+
url.searchParams.forEach((v, k) => forwardUrl.searchParams.set(k, v))
|
|
1847
|
+
|
|
1848
|
+
log('Forwarding WebSocket to DO:', route.doNamespace, 'id:', idValue, 'path:', forwardUrl.pathname)
|
|
1849
|
+
|
|
1850
|
+
return stub.fetch(forwardUrl.toString(), {
|
|
1851
|
+
method: request.method,
|
|
1852
|
+
headers: request.headers
|
|
1853
|
+
})
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
console.error('[Gateway] Error forwarding to DO:', error)
|
|
1856
|
+
return new Response('Error forwarding to DO: ' + error.message, { status: 500 })
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
`;
|
|
1860
|
+
}
|
|
1861
|
+
//#endregion
|
|
1862
|
+
//#region src/dev-server/miniflare-worker-config.ts
|
|
1863
|
+
const DEFAULT_MODULE_RULES = [
|
|
1864
|
+
{
|
|
1865
|
+
type: "ESModule",
|
|
1866
|
+
include: [
|
|
1867
|
+
"**/*.ts",
|
|
1868
|
+
"**/*.tsx",
|
|
1869
|
+
"**/*.mts",
|
|
1870
|
+
"**/*.mjs"
|
|
1871
|
+
]
|
|
1872
|
+
},
|
|
1873
|
+
{
|
|
1874
|
+
type: "CommonJS",
|
|
1875
|
+
include: ["**/*.js", "**/*.cjs"]
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
type: "ESModule",
|
|
1879
|
+
include: ["**/*.jsx"]
|
|
1880
|
+
}
|
|
1881
|
+
];
|
|
1882
|
+
function toMiniflareModuleRule(rule) {
|
|
1883
|
+
return {
|
|
1884
|
+
type: rule.type,
|
|
1885
|
+
include: rule.globs,
|
|
1886
|
+
...rule.fallthrough !== void 0 && { fallthrough: rule.fallthrough }
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Build the per-worker `serviceBindings` map. Combines user-declared
|
|
1891
|
+
* `bindings.services` (config) with any extra bindings the caller wants to
|
|
1892
|
+
* inject (e.g. internal gateway -> app routing).
|
|
1893
|
+
*/
|
|
1894
|
+
function buildServiceBindings(bindings, extraBindings = {}) {
|
|
1895
|
+
const serviceBindings = {};
|
|
1896
|
+
if (bindings.services) for (const [bindingName, serviceConfig] of Object.entries(bindings.services)) serviceBindings[bindingName] = {
|
|
1897
|
+
name: serviceConfig.service,
|
|
1898
|
+
...serviceConfig.entrypoint && { entrypoint: serviceConfig.entrypoint }
|
|
1899
|
+
};
|
|
1900
|
+
for (const [bindingName, target] of Object.entries(extraBindings)) serviceBindings[bindingName] = target;
|
|
1901
|
+
return Object.keys(serviceBindings).length > 0 ? serviceBindings : void 0;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Build a single worker config object for Miniflare's `workers` array.
|
|
1905
|
+
* All inputs are passed explicitly (no closures over the caller's locals).
|
|
1906
|
+
*/
|
|
1907
|
+
function makeMiniflareWorker(context, options) {
|
|
1908
|
+
const { cwd, loadedConfig, bindings, sendEmailConfig, rateLimitsConfig, versionMetadataConfig, workerLoadersConfig, mtlsCertificatesConfig, dispatchNamespacesConfig, workflowsConfig, pipelinesConfig, hyperdrivesConfig, imagesConfig, mediaConfig, streamConfig, flagshipConfig, analyticsEngineConfig, tailConsumersConfig, artifactsConfig, aiSearchNamespacesConfig, aiSearchInstancesConfig, secretsStoreConfig, localSecretWrappedBindingConfig, queueProducers } = context;
|
|
1909
|
+
const baseFlags = loadedConfig.compatibilityFlags ?? [];
|
|
1910
|
+
const compatFlags = baseFlags.includes("nodejs_compat") ? baseFlags : [...baseFlags, "nodejs_compat"];
|
|
1911
|
+
const workerBindings = loadedConfig.vars ?? {};
|
|
1912
|
+
const localWrappedBindings = { ...localSecretWrappedBindingConfig?.wrappedBindings ?? {} };
|
|
1913
|
+
const workerConfig = {
|
|
1914
|
+
name: options.name,
|
|
1915
|
+
modules: true,
|
|
1916
|
+
compatibilityDate: loadedConfig.compatibilityDate,
|
|
1917
|
+
compatibilityFlags: compatFlags,
|
|
1918
|
+
...bindings.kv && { kvNamespaces: Object.fromEntries(Object.entries(bindings.kv).map(([bindingName, bindingConfig]) => {
|
|
1919
|
+
return [bindingName, getLocalKVNamespaceIdentifier(bindingConfig)];
|
|
1920
|
+
})) },
|
|
1921
|
+
...bindings.r2 && { r2Buckets: Object.fromEntries(Object.entries(bindings.r2).map(([bindingName, bindingConfig]) => {
|
|
1922
|
+
return [bindingName, normalizeR2Binding(bindingConfig).bucketName];
|
|
1923
|
+
})) },
|
|
1924
|
+
...bindings.d1 && { d1Databases: Object.fromEntries(Object.entries(bindings.d1).map(([bindingName, bindingConfig]) => {
|
|
1925
|
+
return [bindingName, getLocalD1DatabaseIdentifier(bindingConfig)];
|
|
1926
|
+
})) },
|
|
1927
|
+
...Object.keys(workerBindings).length > 0 && { bindings: workerBindings },
|
|
1928
|
+
...sendEmailConfig && { email: sendEmailConfig },
|
|
1929
|
+
...rateLimitsConfig && { ratelimits: rateLimitsConfig },
|
|
1930
|
+
...versionMetadataConfig && { versionMetadata: versionMetadataConfig },
|
|
1931
|
+
...workerLoadersConfig && { workerLoaders: workerLoadersConfig },
|
|
1932
|
+
...mtlsCertificatesConfig && { mtlsCertificates: mtlsCertificatesConfig },
|
|
1933
|
+
...dispatchNamespacesConfig && { dispatchNamespaces: dispatchNamespacesConfig },
|
|
1934
|
+
...workflowsConfig && { workflows: workflowsConfig },
|
|
1935
|
+
...pipelinesConfig && { pipelines: pipelinesConfig },
|
|
1936
|
+
...hyperdrivesConfig && { hyperdrives: hyperdrivesConfig },
|
|
1937
|
+
...imagesConfig && { images: imagesConfig },
|
|
1938
|
+
...mediaConfig && { media: mediaConfig },
|
|
1939
|
+
...streamConfig && { stream: streamConfig },
|
|
1940
|
+
...flagshipConfig && { flagship: flagshipConfig },
|
|
1941
|
+
...analyticsEngineConfig && { analyticsEngineDatasets: analyticsEngineConfig },
|
|
1942
|
+
...tailConsumersConfig && { tails: tailConsumersConfig },
|
|
1943
|
+
...artifactsConfig && { artifacts: artifactsConfig },
|
|
1944
|
+
...aiSearchNamespacesConfig && { aiSearchNamespaces: aiSearchNamespacesConfig },
|
|
1945
|
+
...aiSearchInstancesConfig && { aiSearchInstances: aiSearchInstancesConfig },
|
|
1946
|
+
...secretsStoreConfig && { secretsStoreSecrets: secretsStoreConfig },
|
|
1947
|
+
...Object.keys(localWrappedBindings).length > 0 && { wrappedBindings: localWrappedBindings },
|
|
1948
|
+
...queueProducers && { queueProducers },
|
|
1949
|
+
...options.queueConsumers && { queueConsumers: options.queueConsumers },
|
|
1950
|
+
...options.triggers && { triggers: options.triggers }
|
|
1951
|
+
};
|
|
1952
|
+
if (options.scriptPath) {
|
|
1953
|
+
workerConfig.scriptPath = options.scriptPath;
|
|
1954
|
+
workerConfig.modulesRoot = loadedConfig.baseDir ? resolve(cwd, loadedConfig.baseDir) : cwd;
|
|
1955
|
+
workerConfig.modulesRules = [...loadedConfig.rules?.map(toMiniflareModuleRule) ?? [], ...DEFAULT_MODULE_RULES];
|
|
1956
|
+
}
|
|
1957
|
+
if (options.script) workerConfig.script = options.script;
|
|
1958
|
+
if (options.durableObjects && Object.keys(options.durableObjects).length > 0) workerConfig.durableObjects = options.durableObjects;
|
|
1959
|
+
if (options.serviceBindings && Object.keys(options.serviceBindings).length > 0) workerConfig.serviceBindings = options.serviceBindings;
|
|
1960
|
+
return workerConfig;
|
|
1961
|
+
}
|
|
1962
|
+
//#endregion
|
|
1963
|
+
//#region src/dev-server/worker-surface-paths.ts
|
|
1964
|
+
const DEFAULT_TRANSPORT_ENTRY_FILES = [
|
|
1965
|
+
"src/transport.ts",
|
|
1966
|
+
"src/transport.js",
|
|
1967
|
+
"src/transport.mts",
|
|
1968
|
+
"src/transport.mjs"
|
|
1969
|
+
];
|
|
1970
|
+
const resolveMainWorkerSurfacePaths = resolveWorkerSurfacePaths;
|
|
1971
|
+
function addWorkerWatchRoots(roots, cwd, configuredPath, defaultEntries) {
|
|
1972
|
+
if (configuredPath === false || configuredPath === null) return;
|
|
1973
|
+
if (typeof configuredPath === "string" && configuredPath) {
|
|
1974
|
+
roots.add(dirname(resolve(cwd, configuredPath)));
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
for (const defaultEntry of defaultEntries) roots.add(dirname(resolve(cwd, defaultEntry)));
|
|
1978
|
+
}
|
|
1979
|
+
function collectWorkerWatchRoots(cwd, config, mainWorkerSurfacePaths) {
|
|
1980
|
+
const roots = /* @__PURE__ */ new Set();
|
|
1981
|
+
for (const surfacePath of Object.values(mainWorkerSurfacePaths)) if (surfacePath) roots.add(dirname(surfacePath));
|
|
1982
|
+
addWorkerWatchRoots(roots, cwd, config.files?.fetch, DEFAULT_FETCH_ENTRY_FILES);
|
|
1983
|
+
addWorkerWatchRoots(roots, cwd, config.files?.queue, DEFAULT_QUEUE_ENTRY_FILES);
|
|
1984
|
+
addWorkerWatchRoots(roots, cwd, config.files?.scheduled, DEFAULT_SCHEDULED_ENTRY_FILES);
|
|
1985
|
+
addWorkerWatchRoots(roots, cwd, config.files?.email, DEFAULT_EMAIL_ENTRY_FILES);
|
|
1986
|
+
addWorkerWatchRoots(roots, cwd, config.files?.transport, DEFAULT_TRANSPORT_ENTRY_FILES);
|
|
1987
|
+
const routeDirectory = getRouteDirectoryCandidate(cwd, config);
|
|
1988
|
+
if (routeDirectory) roots.add(routeDirectory.absoluteDir);
|
|
1989
|
+
return [...roots];
|
|
1990
|
+
}
|
|
1991
|
+
//#endregion
|
|
1992
|
+
//#region src/dev-server/miniflare-dev-config.ts
|
|
1993
|
+
const INTERNAL_APP_SERVICE_BINDING = "__DEVFLARE_APP";
|
|
1994
|
+
/**
|
|
1995
|
+
* Build the complete Miniflare configuration for the dev server.
|
|
1996
|
+
*
|
|
1997
|
+
* IMPORTANT: When using multi-worker setup, ALL workers must go in the
|
|
1998
|
+
* `workers` array. The FIRST worker is the entrypoint and receives all
|
|
1999
|
+
* HTTP requests. Top-level script/modules options are NOT used when
|
|
2000
|
+
* workers array is present.
|
|
2001
|
+
*/
|
|
2002
|
+
function buildMiniflareDevConfig(input) {
|
|
2003
|
+
const { config: loadedConfig, cwd, miniflarePort, miniflareHost = "127.0.0.1", persist, enableVite, debug, mainWorkerSurfacePaths, mainWorkerRoutes, mainWorkerScriptPath, bundledMainWorkerScriptPath, workflowEntrypointScript, browserShimPort, doResult, serviceBindingResolution, logger } = input;
|
|
2004
|
+
const bindings = loadedConfig.bindings ?? {};
|
|
2005
|
+
const persistPath = resolve(cwd, ".devflare/data");
|
|
2006
|
+
const appWorkerName = loadedConfig.name;
|
|
2007
|
+
const shouldRunMainWorker = !enableVite && (hasWorkerSurfacePaths(mainWorkerSurfacePaths) || Boolean(mainWorkerRoutes?.routes.length));
|
|
2008
|
+
const queueProducers = buildQueueProducers(bindings);
|
|
2009
|
+
const queueConsumers = buildQueueConsumers(bindings);
|
|
2010
|
+
const sharedOptions = {
|
|
2011
|
+
port: miniflarePort,
|
|
2012
|
+
host: miniflareHost,
|
|
2013
|
+
kvPersist: persist ? `${persistPath}/kv` : void 0,
|
|
2014
|
+
r2Persist: persist ? `${persistPath}/r2` : void 0,
|
|
2015
|
+
d1Persist: persist ? `${persistPath}/d1` : void 0,
|
|
2016
|
+
durableObjectsPersist: persist ? `${persistPath}/do` : void 0,
|
|
2017
|
+
workflowsPersist: persist ? `${persistPath}/workflows` : void 0,
|
|
2018
|
+
imagesPersist: persist ? `${persistPath}/images` : void 0,
|
|
2019
|
+
streamPersist: persist ? `${persistPath}/stream` : void 0
|
|
2020
|
+
};
|
|
2021
|
+
const localBindingShimServiceConfig = buildLocalBindingShimServiceConfig(loadedConfig);
|
|
2022
|
+
const createServiceBindings = (extraBindings = {}) => buildServiceBindings(bindings, {
|
|
2023
|
+
...serviceBindingResolution?.primaryServiceBindings ?? {},
|
|
2024
|
+
...localBindingShimServiceConfig.serviceBindings,
|
|
2025
|
+
...extraBindings
|
|
2026
|
+
});
|
|
2027
|
+
const sendEmailConfig = buildSendEmailConfig(bindings);
|
|
2028
|
+
const rateLimitsConfig = buildRateLimitsConfig(bindings);
|
|
2029
|
+
const versionMetadataConfig = buildVersionMetadataConfig(bindings);
|
|
2030
|
+
const workerLoadersConfig = buildWorkerLoadersConfig(bindings);
|
|
2031
|
+
const mtlsCertificatesConfig = buildMtlsCertificatesConfig(bindings);
|
|
2032
|
+
const dispatchNamespacesConfig = buildDispatchNamespacesConfig(bindings);
|
|
2033
|
+
const workflowsConfig = buildWorkflowsConfig(bindings);
|
|
2034
|
+
const pipelinesConfig = buildPipelinesConfig(bindings);
|
|
2035
|
+
const hyperdrivesConfig = buildHyperdrivesConfig(bindings);
|
|
2036
|
+
const imagesConfig = bindings.images ? void 0 : buildImagesConfig(bindings);
|
|
2037
|
+
const mediaConfig = bindings.media ? void 0 : buildMediaConfig(bindings);
|
|
2038
|
+
const streamConfig = buildStreamConfig(bindings);
|
|
2039
|
+
const flagshipConfig = buildFlagshipConfig(bindings);
|
|
2040
|
+
const analyticsEngineConfig = buildAnalyticsEngineConfig(bindings);
|
|
2041
|
+
const tailConsumersConfig = buildTailConsumersConfig(loadedConfig);
|
|
2042
|
+
const artifactsConfig = buildArtifactsConfig(bindings);
|
|
2043
|
+
const aiSearchNamespacesConfig = buildAiSearchNamespacesConfig(bindings);
|
|
2044
|
+
const aiSearchInstancesConfig = buildAiSearchInstancesConfig(bindings);
|
|
2045
|
+
const localSecretWrappedBindingConfig = buildLocalSecretWrappedBindingConfig(loadedConfig, cwd);
|
|
2046
|
+
const localSecretBindingNames = new Set(localSecretWrappedBindingConfig.localBindingNames);
|
|
2047
|
+
const workerContext = {
|
|
2048
|
+
cwd,
|
|
2049
|
+
loadedConfig,
|
|
2050
|
+
bindings,
|
|
2051
|
+
sendEmailConfig,
|
|
2052
|
+
rateLimitsConfig,
|
|
2053
|
+
versionMetadataConfig,
|
|
2054
|
+
workerLoadersConfig,
|
|
2055
|
+
mtlsCertificatesConfig,
|
|
2056
|
+
dispatchNamespacesConfig,
|
|
2057
|
+
workflowsConfig,
|
|
2058
|
+
pipelinesConfig,
|
|
2059
|
+
hyperdrivesConfig,
|
|
2060
|
+
imagesConfig,
|
|
2061
|
+
mediaConfig,
|
|
2062
|
+
streamConfig,
|
|
2063
|
+
flagshipConfig,
|
|
2064
|
+
analyticsEngineConfig,
|
|
2065
|
+
tailConsumersConfig,
|
|
2066
|
+
artifactsConfig,
|
|
2067
|
+
aiSearchNamespacesConfig,
|
|
2068
|
+
aiSearchInstancesConfig,
|
|
2069
|
+
secretsStoreConfig: buildSecretsStoreConfig(bindings, loadedConfig.secretsStoreId, localSecretBindingNames),
|
|
2070
|
+
localSecretWrappedBindingConfig,
|
|
2071
|
+
queueProducers
|
|
2072
|
+
};
|
|
2073
|
+
const createWorkerConfig = (options) => makeMiniflareWorker(workerContext, options);
|
|
2074
|
+
const gatewayWorker = createWorkerConfig({
|
|
2075
|
+
name: "gateway",
|
|
2076
|
+
script: [workflowEntrypointScript, getGatewayScript(loadedConfig.wsRoutes, debug, shouldRunMainWorker ? INTERNAL_APP_SERVICE_BINDING : null)].filter(Boolean).join("\n\n"),
|
|
2077
|
+
serviceBindings: shouldRunMainWorker ? createServiceBindings({ [INTERNAL_APP_SERVICE_BINDING]: { name: appWorkerName } }) : createServiceBindings()
|
|
2078
|
+
});
|
|
2079
|
+
gatewayWorker.routes = ["*"];
|
|
2080
|
+
const hasDurableObjectBundles = !!doResult && doResult.bundles.size > 0;
|
|
2081
|
+
const browserBindingName = getSingleBrowserBindingName(bindings.browser);
|
|
2082
|
+
const needsBrowserWorker = Boolean(browserBindingName && (hasDurableObjectBundles || shouldRunMainWorker));
|
|
2083
|
+
const workers = [];
|
|
2084
|
+
const durableObjects = {};
|
|
2085
|
+
const browserShimUrl = `http://127.0.0.1:${browserShimPort}`;
|
|
2086
|
+
const browserWorkerName = "browser-binding";
|
|
2087
|
+
if (shouldRunMainWorker && mainWorkerScriptPath) {
|
|
2088
|
+
const mainWorkerServiceBindings = createServiceBindings(browserBindingName ? { [browserBindingName]: { name: browserWorkerName } } : {});
|
|
2089
|
+
const mainWorkerConfig = createWorkerConfig({
|
|
2090
|
+
name: appWorkerName,
|
|
2091
|
+
scriptPath: bundledMainWorkerScriptPath ?? mainWorkerScriptPath,
|
|
2092
|
+
serviceBindings: mainWorkerServiceBindings,
|
|
2093
|
+
queueConsumers,
|
|
2094
|
+
triggers: loadedConfig.triggers?.crons?.length ? { crons: loadedConfig.triggers.crons } : void 0
|
|
2095
|
+
});
|
|
2096
|
+
workers.push(mainWorkerConfig);
|
|
2097
|
+
}
|
|
2098
|
+
if (doResult) for (const [bindingName, bundlePath] of doResult.bundles) {
|
|
2099
|
+
const className = doResult.classes.get(bindingName);
|
|
2100
|
+
if (!className) continue;
|
|
2101
|
+
const workerName = `do-${bindingName.toLowerCase()}`;
|
|
2102
|
+
const workerConfig = createWorkerConfig({
|
|
2103
|
+
name: workerName,
|
|
2104
|
+
scriptPath: bundlePath,
|
|
2105
|
+
durableObjects: { [bindingName]: className },
|
|
2106
|
+
serviceBindings: createServiceBindings(browserBindingName ? { [browserBindingName]: { name: browserWorkerName } } : {})
|
|
2107
|
+
});
|
|
2108
|
+
if (browserBindingName) logger?.debug(`DO ${workerName} has browser service binding: ${browserBindingName} → ${browserWorkerName}`);
|
|
2109
|
+
logger?.debug(`DO ${workerName} config:`, JSON.stringify(workerConfig, null, 2));
|
|
2110
|
+
workers.push(workerConfig);
|
|
2111
|
+
durableObjects[bindingName] = {
|
|
2112
|
+
className,
|
|
2113
|
+
scriptName: workerName
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
if (needsBrowserWorker) {
|
|
2117
|
+
const browserWorker = createWorkerConfig({
|
|
2118
|
+
name: browserWorkerName,
|
|
2119
|
+
script: getBrowserBindingScript(browserShimUrl, debug)
|
|
2120
|
+
});
|
|
2121
|
+
workers.push(browserWorker);
|
|
2122
|
+
logger?.info(`Browser binding worker configured: ${browserBindingName} → ${browserShimUrl}`);
|
|
2123
|
+
}
|
|
2124
|
+
if (Object.keys(durableObjects).length > 0) {
|
|
2125
|
+
gatewayWorker.durableObjects = durableObjects;
|
|
2126
|
+
if (shouldRunMainWorker) {
|
|
2127
|
+
const mainWorker = workers.find((worker) => worker.name === appWorkerName);
|
|
2128
|
+
if (mainWorker) mainWorker.durableObjects = durableObjects;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
return {
|
|
2132
|
+
...sharedOptions,
|
|
2133
|
+
workers: [
|
|
2134
|
+
gatewayWorker,
|
|
2135
|
+
...workers,
|
|
2136
|
+
...serviceBindingResolution?.workers ?? [],
|
|
2137
|
+
...localSecretWrappedBindingConfig.workers,
|
|
2138
|
+
...localBindingShimServiceConfig.workers
|
|
2139
|
+
]
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
//#endregion
|
|
2143
|
+
//#region src/dev-server/reload-queue.ts
|
|
2144
|
+
function createReloadQueue({ reload, logger }) {
|
|
2145
|
+
let running = null;
|
|
2146
|
+
let pending = null;
|
|
2147
|
+
async function runOnce() {
|
|
2148
|
+
try {
|
|
2149
|
+
await reload();
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
logger?.error("[devflare dev] reload failed:", error);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
function schedule() {
|
|
2155
|
+
if (!running) {
|
|
2156
|
+
running = runOnce().finally(() => {
|
|
2157
|
+
running = null;
|
|
2158
|
+
});
|
|
2159
|
+
return running;
|
|
2160
|
+
}
|
|
2161
|
+
if (!pending) pending = running.then(() => {
|
|
2162
|
+
pending = null;
|
|
2163
|
+
running = runOnce().finally(() => {
|
|
2164
|
+
running = null;
|
|
2165
|
+
});
|
|
2166
|
+
return running;
|
|
2167
|
+
});
|
|
2168
|
+
return pending;
|
|
2169
|
+
}
|
|
2170
|
+
async function drain() {
|
|
2171
|
+
while (running || pending) if (pending) await pending;
|
|
2172
|
+
else if (running) await running;
|
|
2173
|
+
}
|
|
2174
|
+
return {
|
|
2175
|
+
schedule,
|
|
2176
|
+
drain
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region src/dev-server/runtime-stdio.ts
|
|
2181
|
+
function writeStdout(logger, message) {
|
|
2182
|
+
if (typeof logger?.log === "function") {
|
|
2183
|
+
logger.log(message);
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (typeof logger?.info === "function") {
|
|
2187
|
+
logger.info(message);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
console.log(message);
|
|
2191
|
+
}
|
|
2192
|
+
function writeStderr(logger, message) {
|
|
2193
|
+
if (typeof logger?.error === "function") {
|
|
2194
|
+
logger.error(message);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
console.error(message);
|
|
2198
|
+
}
|
|
2199
|
+
function createRuntimeStdioForwarder(logger) {
|
|
2200
|
+
return (stdout, stderr) => {
|
|
2201
|
+
createInterface({ input: stdout }).on("line", (data) => {
|
|
2202
|
+
writeStdout(logger, data);
|
|
2203
|
+
});
|
|
2204
|
+
createInterface({ input: stderr }).on("line", (data) => {
|
|
2205
|
+
writeStderr(logger, data);
|
|
2206
|
+
});
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/browser-shim/server.ts
|
|
2211
|
+
/**
|
|
2212
|
+
* Devflare's **local browser-rendering shim**.
|
|
2213
|
+
*
|
|
2214
|
+
* Accepts only loopback browser origins (e.g. `http://127.0.0.1:*`,
|
|
2215
|
+
* `http://localhost:*`) plus origin-less tool traffic (Puppeteer, curl, and
|
|
2216
|
+
* other non-browser clients that do not send an `Origin` header). Cross-origin
|
|
2217
|
+
* browser traffic is rejected at the request boundary.
|
|
2218
|
+
*
|
|
2219
|
+
* This is NOT a user-facing app route — it is devflare's protected helper
|
|
2220
|
+
* endpoint used by the local Browser Rendering binding to satisfy the
|
|
2221
|
+
* `@cloudflare/puppeteer` contract during local dev. The loopback-only posture
|
|
2222
|
+
* applies to this shim only and does not apply to the user's normal worker
|
|
2223
|
+
* routes.
|
|
2224
|
+
*/
|
|
2225
|
+
let cachedExecutablePath = null;
|
|
2226
|
+
/**
|
|
2227
|
+
* Default Chrome flags used when launching headless Chrome for local
|
|
2228
|
+
* browser-rendering emulation. Each flag is included for a deliberate reason;
|
|
2229
|
+
* edit cautiously.
|
|
2230
|
+
*
|
|
2231
|
+
* NOTE: `--no-sandbox` / `--disable-setuid-sandbox` are intentionally NOT part
|
|
2232
|
+
* of the defaults. Disabling the sandbox removes the primary boundary between
|
|
2233
|
+
* untrusted web content and the host and must be opted into explicitly via
|
|
2234
|
+
* `BrowserShimOptions.allowNoSandbox`.
|
|
2235
|
+
*/
|
|
2236
|
+
const DEFAULT_CHROME_FLAGS = [
|
|
2237
|
+
"--disable-dev-shm-usage",
|
|
2238
|
+
"--disable-gpu",
|
|
2239
|
+
"--disable-software-rasterizer",
|
|
2240
|
+
"--disable-extensions",
|
|
2241
|
+
"--disable-background-networking",
|
|
2242
|
+
"--disable-background-timer-throttling",
|
|
2243
|
+
"--disable-backgrounding-occluded-windows",
|
|
2244
|
+
"--disable-renderer-backgrounding",
|
|
2245
|
+
"--disable-features=TranslateUI",
|
|
2246
|
+
"--disable-ipc-flooding-protection",
|
|
2247
|
+
"--disable-default-apps",
|
|
2248
|
+
"--mute-audio",
|
|
2249
|
+
"--js-flags=--max-old-space-size=4096"
|
|
2250
|
+
];
|
|
2251
|
+
/**
|
|
2252
|
+
* Flags appended only when `allowNoSandbox` is explicitly enabled. Kept in a
|
|
2253
|
+
* separate constant so callers and tests can assert they are opt-in.
|
|
2254
|
+
*/
|
|
2255
|
+
const NO_SANDBOX_FLAGS = ["--no-sandbox", "--disable-setuid-sandbox"];
|
|
2256
|
+
/**
|
|
2257
|
+
* Resolve the Chrome argv for a shim launch. Exported for testability.
|
|
2258
|
+
*/
|
|
2259
|
+
function resolveChromeFlags(options = {}) {
|
|
2260
|
+
const flags = [...DEFAULT_CHROME_FLAGS];
|
|
2261
|
+
if (options.allowNoSandbox) flags.unshift(...NO_SANDBOX_FLAGS);
|
|
2262
|
+
return flags;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Create a download progress logger that emits at most one "start" line and
|
|
2266
|
+
* exactly one "complete" line per download. Avoids the previous heuristic
|
|
2267
|
+
* percent-spam which could log the same bucket multiple times.
|
|
2268
|
+
*
|
|
2269
|
+
* The returned callback matches `@puppeteer/browsers` `downloadProgressCallback`.
|
|
2270
|
+
*/
|
|
2271
|
+
function createDownloadProgressLogger(logger, label = "Chrome") {
|
|
2272
|
+
const state = {
|
|
2273
|
+
started: false,
|
|
2274
|
+
completed: false,
|
|
2275
|
+
progress: {
|
|
2276
|
+
bytesReceived: 0,
|
|
2277
|
+
totalBytes: 0
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
return {
|
|
2281
|
+
onProgress(downloadedBytes, totalBytes) {
|
|
2282
|
+
if (state.completed) return;
|
|
2283
|
+
state.progress.bytesReceived = downloadedBytes;
|
|
2284
|
+
state.progress.totalBytes = totalBytes;
|
|
2285
|
+
if (!state.started) {
|
|
2286
|
+
state.started = true;
|
|
2287
|
+
logger?.info(`[BrowserShim] Downloading ${label}...`);
|
|
2288
|
+
}
|
|
2289
|
+
if (totalBytes > 0 && downloadedBytes >= totalBytes) {
|
|
2290
|
+
state.completed = true;
|
|
2291
|
+
logger?.info(`[BrowserShim] ${label} download complete`);
|
|
2292
|
+
}
|
|
2293
|
+
},
|
|
2294
|
+
finalize() {
|
|
2295
|
+
if (!state.started || state.completed) return;
|
|
2296
|
+
state.completed = true;
|
|
2297
|
+
logger?.info(`[BrowserShim] ${label} download complete`);
|
|
2298
|
+
},
|
|
2299
|
+
get progress() {
|
|
2300
|
+
return state.progress;
|
|
2301
|
+
},
|
|
2302
|
+
get started() {
|
|
2303
|
+
return state.started;
|
|
2304
|
+
},
|
|
2305
|
+
get completed() {
|
|
2306
|
+
return state.completed;
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Get or install Chrome Headless Shell
|
|
2312
|
+
* Uses a shared cache directory so Chrome is only installed once globally
|
|
2313
|
+
*/
|
|
2314
|
+
async function ensureChrome(cacheDir, logger) {
|
|
2315
|
+
if (cachedExecutablePath && existsSync(cachedExecutablePath)) return cachedExecutablePath;
|
|
2316
|
+
const platform = detectBrowserPlatform();
|
|
2317
|
+
if (!platform) throw new Error("Could not detect browser platform");
|
|
2318
|
+
const buildId = await resolveBuildId(Browser.CHROMEHEADLESSSHELL, platform, "stable");
|
|
2319
|
+
logger?.debug(`[BrowserShim] Resolved Chrome Headless Shell build: ${buildId}`);
|
|
2320
|
+
const progressLogger = createDownloadProgressLogger(logger, "Chrome");
|
|
2321
|
+
const installedBrowser = await install({
|
|
2322
|
+
browser: Browser.CHROMEHEADLESSSHELL,
|
|
2323
|
+
buildId,
|
|
2324
|
+
cacheDir,
|
|
2325
|
+
downloadProgressCallback: (downloadedBytes, totalBytes) => {
|
|
2326
|
+
progressLogger.onProgress(downloadedBytes, totalBytes);
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
progressLogger.finalize();
|
|
2330
|
+
cachedExecutablePath = installedBrowser.executablePath;
|
|
2331
|
+
logger?.success(`[BrowserShim] Chrome ready: ${installedBrowser.executablePath}`);
|
|
2332
|
+
return installedBrowser.executablePath;
|
|
2333
|
+
}
|
|
2334
|
+
function createBrowserShim(options = {}) {
|
|
2335
|
+
const { port = 8788, host = "127.0.0.1", logger, verbose = false, keepAlive = 6e4, cacheDir = join$1(homedir(), ".devflare", "chrome"), allowNoSandbox = false } = options;
|
|
2336
|
+
const chromeLaunchArgs = resolveChromeFlags({ allowNoSandbox });
|
|
2337
|
+
if (allowNoSandbox) logger?.warn("[BrowserShim] Launching Chrome with --no-sandbox (allowNoSandbox=true). Only use this in trusted CI/rootless environments.");
|
|
2338
|
+
let server = null;
|
|
2339
|
+
let executablePath = null;
|
|
2340
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2341
|
+
const history = [];
|
|
2342
|
+
let WebSocketServerClass = null;
|
|
2343
|
+
let WebSocketClass = null;
|
|
2344
|
+
const maxRequestBodyBytes = 1024 * 1024;
|
|
2345
|
+
function getRequestOrigin(req) {
|
|
2346
|
+
const origin = req.headers.origin;
|
|
2347
|
+
if (typeof origin === "string") return origin;
|
|
2348
|
+
if (Array.isArray(origin) && origin[0]) return origin[0];
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2351
|
+
function isLoopbackOrigin(origin) {
|
|
2352
|
+
try {
|
|
2353
|
+
const url = new URL(origin);
|
|
2354
|
+
return url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1" || url.hostname === "[::1]";
|
|
2355
|
+
} catch {
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
function applyCorsHeaders(req, res) {
|
|
2360
|
+
const origin = getRequestOrigin(req);
|
|
2361
|
+
if (!origin) return true;
|
|
2362
|
+
if (!isLoopbackOrigin(origin)) {
|
|
2363
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2364
|
+
res.end(JSON.stringify({ error: "Forbidden origin" }));
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
2368
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2369
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
2370
|
+
res.setHeader("Vary", "Origin");
|
|
2371
|
+
return true;
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Launch a new browser and create a session
|
|
2375
|
+
*/
|
|
2376
|
+
async function acquireSession(acquireOptions) {
|
|
2377
|
+
if (!executablePath) throw new Error("Chrome not initialized");
|
|
2378
|
+
const browser = await puppeteerCore.launch({
|
|
2379
|
+
executablePath,
|
|
2380
|
+
headless: true,
|
|
2381
|
+
protocolTimeout: 12e4,
|
|
2382
|
+
args: chromeLaunchArgs
|
|
2383
|
+
});
|
|
2384
|
+
const wsEndpoint = browser.wsEndpoint();
|
|
2385
|
+
const sessionId = crypto.randomUUID();
|
|
2386
|
+
const session = {
|
|
2387
|
+
sessionId,
|
|
2388
|
+
browser,
|
|
2389
|
+
wsEndpoint,
|
|
2390
|
+
startTime: Date.now()
|
|
2391
|
+
};
|
|
2392
|
+
sessions.set(sessionId, session);
|
|
2393
|
+
const timeout = acquireOptions?.keep_alive ?? keepAlive;
|
|
2394
|
+
if (timeout > 0) session.idleTimeout = setTimeout(async () => {
|
|
2395
|
+
const s = sessions.get(sessionId);
|
|
2396
|
+
if (s && !s.connectionId) await closeSession(sessionId, 2, "BrowserIdle");
|
|
2397
|
+
}, timeout);
|
|
2398
|
+
if (verbose) logger?.debug(`[BrowserShim] Acquired session ${sessionId}`);
|
|
2399
|
+
return { sessionId };
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Close a browser session
|
|
2403
|
+
*/
|
|
2404
|
+
async function closeSession(sessionId, closeReason = 1, closeReasonText = "NormalClosure") {
|
|
2405
|
+
const session = sessions.get(sessionId);
|
|
2406
|
+
if (!session) return;
|
|
2407
|
+
if (session.idleTimeout) clearTimeout(session.idleTimeout);
|
|
2408
|
+
try {
|
|
2409
|
+
await session.browser.close();
|
|
2410
|
+
} catch {}
|
|
2411
|
+
sessions.delete(sessionId);
|
|
2412
|
+
history.unshift({
|
|
2413
|
+
sessionId,
|
|
2414
|
+
startTime: session.startTime,
|
|
2415
|
+
endTime: Date.now(),
|
|
2416
|
+
closeReason,
|
|
2417
|
+
closeReasonText
|
|
2418
|
+
});
|
|
2419
|
+
if (history.length > 100) history.pop();
|
|
2420
|
+
if (verbose) logger?.debug(`[BrowserShim] Closed session ${sessionId}: ${closeReasonText}`);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Handle HTTP requests
|
|
2424
|
+
*/
|
|
2425
|
+
async function handleRequest(req, res) {
|
|
2426
|
+
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
2427
|
+
const method = req.method || "GET";
|
|
2428
|
+
logger?.debug(`[BrowserShim] ${method} ${url.pathname}${url.search ? url.search : ""}`);
|
|
2429
|
+
if (!applyCorsHeaders(req, res)) return;
|
|
2430
|
+
if (method === "OPTIONS") {
|
|
2431
|
+
res.writeHead(204);
|
|
2432
|
+
res.end();
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
if (url.pathname === "/v1/acquire" && (method === "POST" || method === "GET")) {
|
|
2436
|
+
try {
|
|
2437
|
+
let acquireOptions = {};
|
|
2438
|
+
if (method === "GET") {
|
|
2439
|
+
const keepAlive = url.searchParams.get("keep_alive");
|
|
2440
|
+
if (keepAlive) acquireOptions.keep_alive = Number.parseInt(keepAlive, 10);
|
|
2441
|
+
} else try {
|
|
2442
|
+
const body = await readBody(req);
|
|
2443
|
+
acquireOptions = JSON.parse(body);
|
|
2444
|
+
} catch {}
|
|
2445
|
+
sendJson(res, 200, await acquireSession(acquireOptions));
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
const msg = error instanceof Error ? error.message : "Failed to acquire browser";
|
|
2448
|
+
logger?.error(`[BrowserShim] Acquire failed: ${msg}`);
|
|
2449
|
+
sendJson(res, 500, { error: msg });
|
|
2450
|
+
}
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
if (url.pathname === "/v1/sessions" && method === "GET") {
|
|
2454
|
+
sendJson(res, 200, Array.from(sessions.values()).map((s) => ({
|
|
2455
|
+
sessionId: s.sessionId,
|
|
2456
|
+
startTime: s.startTime,
|
|
2457
|
+
connectionId: s.connectionId,
|
|
2458
|
+
connectionStartTime: s.connectionStartTime
|
|
2459
|
+
})));
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
if (url.pathname === "/v1/history" && method === "GET") {
|
|
2463
|
+
sendJson(res, 200, history.slice(0, 50));
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
if (url.pathname === "/v1/limits" && method === "GET") {
|
|
2467
|
+
sendJson(res, 200, {
|
|
2468
|
+
activeSessions: Array.from(sessions.keys()).map((id) => ({ id })),
|
|
2469
|
+
allowedBrowserAcquisitions: 10,
|
|
2470
|
+
maxConcurrentSessions: 10,
|
|
2471
|
+
timeUntilNextAllowedBrowserAcquisition: 0
|
|
2472
|
+
});
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (url.pathname.startsWith("/v1/session/") && method === "GET") {
|
|
2476
|
+
const sessionId = url.pathname.slice(12);
|
|
2477
|
+
const session = sessions.get(sessionId);
|
|
2478
|
+
if (!session) {
|
|
2479
|
+
sendJson(res, 404, { error: "Session not found" });
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
sendJson(res, 200, {
|
|
2483
|
+
sessionId: session.sessionId,
|
|
2484
|
+
wsEndpoint: session.wsEndpoint,
|
|
2485
|
+
startTime: session.startTime,
|
|
2486
|
+
connectionId: session.connectionId,
|
|
2487
|
+
connectionStartTime: session.connectionStartTime
|
|
2488
|
+
});
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (url.pathname === "/_devflare/browser/health") {
|
|
2492
|
+
sendJson(res, 200, {
|
|
2493
|
+
ok: true,
|
|
2494
|
+
activeSessions: sessions.size,
|
|
2495
|
+
historySize: history.length,
|
|
2496
|
+
executablePath
|
|
2497
|
+
});
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (url.pathname === "/v1/connectDevtools") {
|
|
2501
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
2502
|
+
res.end("WebSocket upgrade required");
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2506
|
+
res.end("Not found");
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* Read request body as string
|
|
2510
|
+
*/
|
|
2511
|
+
function readBody(req) {
|
|
2512
|
+
return new Promise((resolve, reject) => {
|
|
2513
|
+
const chunks = [];
|
|
2514
|
+
let totalBytes = 0;
|
|
2515
|
+
req.on("data", (chunk) => {
|
|
2516
|
+
totalBytes += chunk.length;
|
|
2517
|
+
if (totalBytes > maxRequestBodyBytes) {
|
|
2518
|
+
req.destroy();
|
|
2519
|
+
reject(/* @__PURE__ */ new Error(`Request body exceeds ${maxRequestBodyBytes} bytes`));
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
chunks.push(chunk);
|
|
2523
|
+
});
|
|
2524
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
2525
|
+
req.on("error", reject);
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Send JSON response
|
|
2530
|
+
*/
|
|
2531
|
+
function sendJson(res, status, data) {
|
|
2532
|
+
const body = JSON.stringify(data);
|
|
2533
|
+
res.writeHead(status, {
|
|
2534
|
+
"Content-Type": "application/json",
|
|
2535
|
+
"Content-Length": Buffer.byteLength(body)
|
|
2536
|
+
});
|
|
2537
|
+
res.end(body);
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* Start the browser shim server
|
|
2541
|
+
*/
|
|
2542
|
+
async function start() {
|
|
2543
|
+
logger?.info("[BrowserShim] Ensuring Chrome Headless Shell is available...");
|
|
2544
|
+
executablePath = await ensureChrome(cacheDir, logger);
|
|
2545
|
+
try {
|
|
2546
|
+
const wsModule = await import("ws");
|
|
2547
|
+
WebSocketServerClass = wsModule.WebSocketServer || wsModule.default?.WebSocketServer;
|
|
2548
|
+
WebSocketClass = wsModule.WebSocket || wsModule.default?.WebSocket || wsModule.default;
|
|
2549
|
+
} catch {
|
|
2550
|
+
logger?.warn("[BrowserShim] ws package not found, WebSocket proxy disabled");
|
|
2551
|
+
logger?.warn("[BrowserShim] Install with: npm install ws");
|
|
2552
|
+
}
|
|
2553
|
+
server = createServer((req, res) => {
|
|
2554
|
+
handleRequest(req, res).catch((error) => {
|
|
2555
|
+
logger?.error("[BrowserShim] Request error:", error);
|
|
2556
|
+
res.writeHead(500);
|
|
2557
|
+
res.end("Internal server error");
|
|
2558
|
+
});
|
|
2559
|
+
});
|
|
2560
|
+
if (WebSocketServerClass) {
|
|
2561
|
+
const wss = new WebSocketServerClass({ noServer: true });
|
|
2562
|
+
server.on("upgrade", (request, socket, head) => {
|
|
2563
|
+
const origin = getRequestOrigin(request);
|
|
2564
|
+
if (origin && !isLoopbackOrigin(origin)) {
|
|
2565
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
2566
|
+
socket.destroy();
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const url = new URL(request.url || "/", `http://${host}:${port}`);
|
|
2570
|
+
if (url.pathname !== "/v1/connectDevtools") {
|
|
2571
|
+
socket.destroy();
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
const sessionId = url.searchParams.get("browser_session");
|
|
2575
|
+
if (!sessionId) {
|
|
2576
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
2577
|
+
socket.destroy();
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const session = sessions.get(sessionId);
|
|
2581
|
+
if (!session) {
|
|
2582
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
2583
|
+
socket.destroy();
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
const connectionId = crypto.randomUUID();
|
|
2587
|
+
session.connectionId = connectionId;
|
|
2588
|
+
session.connectionStartTime = Date.now();
|
|
2589
|
+
if (session.idleTimeout) {
|
|
2590
|
+
clearTimeout(session.idleTimeout);
|
|
2591
|
+
session.idleTimeout = void 0;
|
|
2592
|
+
}
|
|
2593
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
2594
|
+
if (verbose) logger?.debug(`[BrowserShim] WebSocket connected for session ${sessionId}`);
|
|
2595
|
+
const chromeWs = new WebSocketClass(session.wsEndpoint);
|
|
2596
|
+
let chromeConnected = false;
|
|
2597
|
+
const connectTimeout = setTimeout(() => {
|
|
2598
|
+
if (!chromeConnected) {
|
|
2599
|
+
logger?.error("[BrowserShim] Chrome connection timeout");
|
|
2600
|
+
try {
|
|
2601
|
+
ws.close(1011, "Chrome connection timeout");
|
|
2602
|
+
chromeWs.close();
|
|
2603
|
+
} catch {}
|
|
2604
|
+
closeSession(sessionId, 5, "ChromeConnectionTimeout").catch(() => {});
|
|
2605
|
+
}
|
|
2606
|
+
}, 1e4);
|
|
2607
|
+
chromeWs.on("open", () => {
|
|
2608
|
+
chromeConnected = true;
|
|
2609
|
+
clearTimeout(connectTimeout);
|
|
2610
|
+
if (verbose) logger?.debug("[BrowserShim] Connected to Chrome DevTools");
|
|
2611
|
+
});
|
|
2612
|
+
chromeWs.on("message", (data) => {
|
|
2613
|
+
if (ws.readyState === 1) ws.send(data);
|
|
2614
|
+
});
|
|
2615
|
+
chromeWs.on("close", (code, reason) => {
|
|
2616
|
+
if (verbose) logger?.debug(`[BrowserShim] Chrome WS closed: ${code}`);
|
|
2617
|
+
const validCode = typeof code === "number" && code >= 1e3 && code <= 4999 ? code : 1e3;
|
|
2618
|
+
try {
|
|
2619
|
+
ws.close(validCode, reason?.toString?.() || "");
|
|
2620
|
+
} catch {}
|
|
2621
|
+
closeSession(sessionId, 2, "ChromeDisconnected").catch((err) => {
|
|
2622
|
+
logger?.error("[BrowserShim] Error closing session after Chrome disconnect:", err);
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
chromeWs.on("error", (error) => {
|
|
2626
|
+
logger?.error("[BrowserShim] Chrome WS error:", error.message);
|
|
2627
|
+
try {
|
|
2628
|
+
ws.close(1011, "Chrome WebSocket error");
|
|
2629
|
+
} catch {}
|
|
2630
|
+
closeSession(sessionId, 4, "ChromeError").catch((err) => {
|
|
2631
|
+
logger?.error("[BrowserShim] Error closing session after Chrome error:", err);
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
ws.on("message", (data) => {
|
|
2635
|
+
if (chromeWs.readyState === 1) chromeWs.send(data);
|
|
2636
|
+
});
|
|
2637
|
+
ws.on("close", (code, reason) => {
|
|
2638
|
+
if (verbose) logger?.debug(`[BrowserShim] Client WS closed for session ${sessionId}`);
|
|
2639
|
+
const validCode = typeof code === "number" && code >= 1e3 && code <= 4999 ? code : 1e3;
|
|
2640
|
+
try {
|
|
2641
|
+
chromeWs.close(validCode, reason?.toString?.() || "");
|
|
2642
|
+
} catch {}
|
|
2643
|
+
const s = sessions.get(sessionId);
|
|
2644
|
+
if (s && s.connectionId === connectionId) {
|
|
2645
|
+
s.connectionId = void 0;
|
|
2646
|
+
s.connectionStartTime = void 0;
|
|
2647
|
+
closeSession(sessionId, 1, "ClientDisconnected").catch((err) => {
|
|
2648
|
+
logger?.error("[BrowserShim] Error closing session after disconnect:", err);
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
ws.on("error", (error) => {
|
|
2653
|
+
logger?.error("[BrowserShim] Client WS error:", error.message);
|
|
2654
|
+
try {
|
|
2655
|
+
chromeWs.close();
|
|
2656
|
+
} catch {}
|
|
2657
|
+
});
|
|
2658
|
+
});
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
await new Promise((resolve, reject) => {
|
|
2662
|
+
server.on("error", reject);
|
|
2663
|
+
server.listen(port, host, () => {
|
|
2664
|
+
resolve();
|
|
2665
|
+
});
|
|
2666
|
+
});
|
|
2667
|
+
logger?.success(`Browser shim server ready on http://${host}:${port}`);
|
|
2668
|
+
}
|
|
2669
|
+
/**
|
|
2670
|
+
* Stop the server and close all browsers
|
|
2671
|
+
*/
|
|
2672
|
+
async function stop() {
|
|
2673
|
+
for (const sessionId of Array.from(sessions.keys())) await closeSession(sessionId, 3, "ServerShutdown");
|
|
2674
|
+
if (server) {
|
|
2675
|
+
await new Promise((resolve) => {
|
|
2676
|
+
server.close(() => resolve());
|
|
2677
|
+
});
|
|
2678
|
+
server = null;
|
|
2679
|
+
}
|
|
2680
|
+
logger?.info("Browser shim server stopped");
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Get the server URL
|
|
2684
|
+
*/
|
|
2685
|
+
function getUrl() {
|
|
2686
|
+
return `http://${host}:${port}`;
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
start,
|
|
2690
|
+
stop,
|
|
2691
|
+
getUrl
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/dev-server/server-startup-helpers.ts
|
|
2696
|
+
/**
|
|
2697
|
+
* Emit informational/warning lines about detected worker handlers in
|
|
2698
|
+
* worker-only (no-Vite) mode.
|
|
2699
|
+
*/
|
|
2700
|
+
function logWorkerHandlerDetection(logger, enableVite, hasSurface, mainWorkerSurfacePaths, mainWorkerRoutes) {
|
|
2701
|
+
if (enableVite) return;
|
|
2702
|
+
if (hasSurface) {
|
|
2703
|
+
const detectedWorkerHandlers = Object.entries(mainWorkerSurfacePaths).filter(([, surfacePath]) => !!surfacePath).map(([surfaceName, surfacePath]) => `${surfaceName}=${surfacePath}`);
|
|
2704
|
+
const detectedRouteHandlers = mainWorkerRoutes?.routes.map((route) => `route=${route.filePath}`) ?? [];
|
|
2705
|
+
logger?.info(`Worker handlers detected: ${[...detectedWorkerHandlers, ...detectedRouteHandlers].join(", ")}`);
|
|
2706
|
+
} else logger?.warn("No local worker handler entry was found for worker-only mode");
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Emit warnings about remote-only bindings (AI, Vectorize) and any missing
|
|
2710
|
+
* prerequisites (accountId, wrangler login).
|
|
2711
|
+
*/
|
|
2712
|
+
function logRemoteBindingRequirements(logger, remoteCheck) {
|
|
2713
|
+
if (!remoteCheck.hasRemoteBindings) return;
|
|
2714
|
+
logger?.info("");
|
|
2715
|
+
logger?.warn("⚠️ Remote-only bindings detected:");
|
|
2716
|
+
for (const binding of remoteCheck.remoteBindings) logger?.warn(` • ${binding}`);
|
|
2717
|
+
logger?.info("");
|
|
2718
|
+
if (remoteCheck.missingAccountId) {
|
|
2719
|
+
logger?.warn("⚠️ WARN: accountId is not set in devflare.config.ts");
|
|
2720
|
+
logger?.warn(" Remote bindings (AI, Vectorize) require accountId to charge the correct account.");
|
|
2721
|
+
logger?.warn(" Add: accountId: 'your-cloudflare-account-id'");
|
|
2722
|
+
logger?.info("");
|
|
2723
|
+
}
|
|
2724
|
+
if (remoteCheck.notLoggedIn) {
|
|
2725
|
+
logger?.warn("⚠️ WARN: Not logged in to Wrangler");
|
|
2726
|
+
logger?.warn(" Remote bindings require authentication.");
|
|
2727
|
+
logger?.warn(" Run: bunx wrangler login");
|
|
2728
|
+
logger?.info("");
|
|
2729
|
+
}
|
|
2730
|
+
if (!remoteCheck.missingAccountId && !remoteCheck.notLoggedIn) {
|
|
2731
|
+
logger?.success("✓ Remote binding requirements met");
|
|
2732
|
+
logger?.info("");
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
function formatErrorMessage(error) {
|
|
2736
|
+
return error instanceof Error ? error.message : String(error);
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Resolve the path to watch for config changes. Prefers an explicit
|
|
2740
|
+
* `configPath` if it's directly accessible on disk, otherwise falls back to
|
|
2741
|
+
* the auto-discovered config path under `cwd`. Returns `null` when neither
|
|
2742
|
+
* is available.
|
|
2743
|
+
*/
|
|
2744
|
+
async function resolveWorkerConfigWatchPath(cwd, configPath) {
|
|
2745
|
+
if (configPath) {
|
|
2746
|
+
const explicitPath = resolve(cwd, configPath);
|
|
2747
|
+
const fs = await import("node:fs/promises");
|
|
2748
|
+
try {
|
|
2749
|
+
await fs.access(explicitPath);
|
|
2750
|
+
return explicitPath;
|
|
2751
|
+
} catch {}
|
|
2752
|
+
}
|
|
2753
|
+
return await resolveConfigPath(cwd) ?? null;
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Pretty-print the resolved Miniflare config (truncating long inline scripts)
|
|
2757
|
+
* before `Miniflare` is constructed. Only emits when verbose/debug logging is
|
|
2758
|
+
* requested by the caller.
|
|
2759
|
+
*/
|
|
2760
|
+
function logMiniflareConfigDiagnostics(logger, mfConfig) {
|
|
2761
|
+
logger?.info("=== MINIFLARE CONFIG DEBUG ===");
|
|
2762
|
+
logger?.info("Full config:", JSON.stringify(mfConfig, (key, value) => {
|
|
2763
|
+
if (key === "script" && typeof value === "string" && value.length > 200) return value.substring(0, 200) + "...[truncated]";
|
|
2764
|
+
return value;
|
|
2765
|
+
}, 2));
|
|
2766
|
+
if (mfConfig.workers) {
|
|
2767
|
+
logger?.info("Workers order:");
|
|
2768
|
+
for (const w of mfConfig.workers) {
|
|
2769
|
+
logger?.info(` → ${w.name}:`);
|
|
2770
|
+
logger?.info(` script: ${w.script ? "inline" : w.scriptPath}`);
|
|
2771
|
+
logger?.info(` browserRendering: ${JSON.stringify(w.browserRendering)}`);
|
|
2772
|
+
logger?.info(` durableObjects: ${JSON.stringify(w.durableObjects)}`);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
/**
|
|
2777
|
+
* After `Miniflare` is `ready`, query each declared worker's bindings and
|
|
2778
|
+
* log them. Best-effort: any per-worker failure is logged at debug and does
|
|
2779
|
+
* not abort the rest of the diagnostics.
|
|
2780
|
+
*/
|
|
2781
|
+
async function logMiniflareBindingDiagnostics(logger, miniflare, mfConfig) {
|
|
2782
|
+
try {
|
|
2783
|
+
const gatewayBindings = await miniflare.getBindings("gateway");
|
|
2784
|
+
logger?.info("Gateway worker bindings:", Object.keys(gatewayBindings));
|
|
2785
|
+
if (mfConfig.workers) {
|
|
2786
|
+
for (const w of mfConfig.workers) if (w.name !== "gateway") try {
|
|
2787
|
+
const doBindings = await miniflare.getBindings(w.name);
|
|
2788
|
+
logger?.info(`${w.name} worker bindings:`, Object.keys(doBindings));
|
|
2789
|
+
if ("BROWSER" in doBindings) logger?.success(`${w.name} has BROWSER binding!`);
|
|
2790
|
+
else logger?.warn(`${w.name} is MISSING BROWSER binding`);
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
logger?.debug(`Skipping binding diagnostics for ${w.name}: ${formatErrorMessage(error)}`);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
} catch (error) {
|
|
2796
|
+
logger?.debug(`Skipping Miniflare binding diagnostics: ${formatErrorMessage(error)}`);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* If the config declares a single browser-rendering binding, construct and
|
|
2801
|
+
* start a `BrowserShim` listening on `browserShimPort`. Returns `null` when
|
|
2802
|
+
* no browser binding is configured (no shim needed).
|
|
2803
|
+
*/
|
|
2804
|
+
async function maybeStartBrowserShim(config, options) {
|
|
2805
|
+
const browserBinding = getSingleBrowserBindingName(config.bindings?.browser);
|
|
2806
|
+
if (!browserBinding) return null;
|
|
2807
|
+
options.logger?.info(`Starting Browser Rendering shim (binding: ${browserBinding})...`);
|
|
2808
|
+
const shim = createBrowserShim({
|
|
2809
|
+
port: options.browserShimPort,
|
|
2810
|
+
host: "127.0.0.1",
|
|
2811
|
+
logger: options.logger,
|
|
2812
|
+
verbose: options.verbose
|
|
2813
|
+
});
|
|
2814
|
+
await shim.start();
|
|
2815
|
+
return shim;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* If the config declares a `files.durableObjects` glob, construct a
|
|
2819
|
+
* `DOBundler`, run an initial build, and start watching. Returns
|
|
2820
|
+
* `{ bundler, result }` (`{ null, null }` when no DO pattern is configured).
|
|
2821
|
+
* The `onRebuild` callback fires on each subsequent rebuild — typically used
|
|
2822
|
+
* to schedule a Miniflare reload.
|
|
2823
|
+
*/
|
|
2824
|
+
async function maybeStartDOBundler(config, options) {
|
|
2825
|
+
const doPattern = config.files?.durableObjects;
|
|
2826
|
+
if (typeof doPattern !== "string" || !doPattern) return {
|
|
2827
|
+
bundler: null,
|
|
2828
|
+
result: null
|
|
2829
|
+
};
|
|
2830
|
+
const outDir = resolve(options.cwd, ".devflare/do-bundles");
|
|
2831
|
+
const bundler = createDOBundler({
|
|
2832
|
+
cwd: options.cwd,
|
|
2833
|
+
pattern: doPattern,
|
|
2834
|
+
outDir,
|
|
2835
|
+
rolldownOptions: config.rolldown?.options,
|
|
2836
|
+
sourcemap: config.rolldown?.sourcemap,
|
|
2837
|
+
minify: config.rolldown?.minify,
|
|
2838
|
+
logger: options.logger,
|
|
2839
|
+
onRebuild: options.onRebuild
|
|
2840
|
+
});
|
|
2841
|
+
const result = await bundler.build();
|
|
2842
|
+
await bundler.watch();
|
|
2843
|
+
return {
|
|
2844
|
+
bundler,
|
|
2845
|
+
result
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Resolve whether Vite should run for this package, and if so, write the
|
|
2850
|
+
* generated Vite config under `.devflare/`. Returns:
|
|
2851
|
+
* - `{ enableVite: false, generatedViteConfigPath: null }` when Vite is
|
|
2852
|
+
* not requested or no Vite config exists for this package
|
|
2853
|
+
* - `{ enableVite: true, generatedViteConfigPath }` after writing the
|
|
2854
|
+
* generated config
|
|
2855
|
+
*/
|
|
2856
|
+
async function resolveViteIntegration(options) {
|
|
2857
|
+
if (!options.enableViteRequested) return {
|
|
2858
|
+
enableVite: false,
|
|
2859
|
+
generatedViteConfigPath: null
|
|
2860
|
+
};
|
|
2861
|
+
const viteMode = await resolveViteMode(options.cwd, { requested: true });
|
|
2862
|
+
if (!viteMode.enableVite) {
|
|
2863
|
+
options.logger?.info("Vite disabled: no vite config found for this package");
|
|
2864
|
+
return {
|
|
2865
|
+
enableVite: false,
|
|
2866
|
+
generatedViteConfigPath: null
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
const generatedViteConfigPath = await writeGeneratedViteConfig({
|
|
2870
|
+
cwd: options.cwd,
|
|
2871
|
+
configPath: options.configPath,
|
|
2872
|
+
localConfigPath: viteMode.viteConfigPath,
|
|
2873
|
+
bridgePort: options.miniflarePort
|
|
2874
|
+
});
|
|
2875
|
+
options.logger?.debug(`Generated Vite config → ${generatedViteConfigPath}`);
|
|
2876
|
+
return {
|
|
2877
|
+
enableVite: true,
|
|
2878
|
+
generatedViteConfigPath
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
//#endregion
|
|
2882
|
+
//#region src/dev-server/vite-process.ts
|
|
2883
|
+
/**
|
|
2884
|
+
* Start the Vite dev server process.
|
|
2885
|
+
*/
|
|
2886
|
+
async function startViteProcess(options) {
|
|
2887
|
+
const { cwd, configPath, vitePort, miniflarePort, generatedViteConfigPath, logger } = options;
|
|
2888
|
+
const args = [
|
|
2889
|
+
"vite",
|
|
2890
|
+
"dev",
|
|
2891
|
+
"--port",
|
|
2892
|
+
String(vitePort)
|
|
2893
|
+
];
|
|
2894
|
+
if (generatedViteConfigPath) args.push("--config", generatedViteConfigPath);
|
|
2895
|
+
const viteProcess = spawn("bunx", args, {
|
|
2896
|
+
cwd,
|
|
2897
|
+
stdio: [
|
|
2898
|
+
"inherit",
|
|
2899
|
+
"pipe",
|
|
2900
|
+
"pipe"
|
|
2901
|
+
],
|
|
2902
|
+
windowsHide: true,
|
|
2903
|
+
env: {
|
|
2904
|
+
...process.env,
|
|
2905
|
+
DEVFLARE_DEV: "true",
|
|
2906
|
+
DEVFLARE_BRIDGE_PORT: String(miniflarePort),
|
|
2907
|
+
...configPath ? { DEVFLARE_CONFIG_PATH: configPath } : {},
|
|
2908
|
+
FORCE_COLOR: "1"
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
const readyUrl = await waitForViteReady(viteProcess, {
|
|
2912
|
+
onStdout(chunk) {
|
|
2913
|
+
process.stdout.write(chunk);
|
|
2914
|
+
},
|
|
2915
|
+
onStderr(chunk) {
|
|
2916
|
+
process.stderr.write(chunk);
|
|
2917
|
+
}
|
|
2918
|
+
});
|
|
2919
|
+
if (readyUrl) {
|
|
2920
|
+
logger?.success(`Vite dev server started on ${readyUrl}`);
|
|
2921
|
+
return viteProcess;
|
|
2922
|
+
}
|
|
2923
|
+
logger?.warn("Vite process started, but the final local URL could not be confirmed yet");
|
|
2924
|
+
return viteProcess;
|
|
2925
|
+
}
|
|
2926
|
+
//#endregion
|
|
2927
|
+
//#region src/dev-server/worker-source-watcher.ts
|
|
2928
|
+
async function startWorkerSourceWatcher(options) {
|
|
2929
|
+
const { watchTargets, resolvedWorkerConfigPath, logger, onConfigChange, onWorkerChange } = options;
|
|
2930
|
+
if (watchTargets.length === 0) return null;
|
|
2931
|
+
const chokidar = await import("chokidar");
|
|
2932
|
+
const isWindows = process.platform === "win32";
|
|
2933
|
+
const ignoredSegments = [
|
|
2934
|
+
"/node_modules/",
|
|
2935
|
+
"/.git/",
|
|
2936
|
+
"/.devflare/",
|
|
2937
|
+
"/dist/"
|
|
2938
|
+
];
|
|
2939
|
+
const normalizePath = (filePath) => filePath.replace(/\\/g, "/");
|
|
2940
|
+
const isIgnoredPath = (filePath) => {
|
|
2941
|
+
const normalizedPath = normalizePath(filePath);
|
|
2942
|
+
return ignoredSegments.some((segment) => normalizedPath.includes(segment));
|
|
2943
|
+
};
|
|
2944
|
+
let reloadTimeout = null;
|
|
2945
|
+
let reloadInProgress = false;
|
|
2946
|
+
let pendingReloadPath = null;
|
|
2947
|
+
const flushPendingReload = async () => {
|
|
2948
|
+
if (!pendingReloadPath) return;
|
|
2949
|
+
const nextPath = pendingReloadPath;
|
|
2950
|
+
pendingReloadPath = null;
|
|
2951
|
+
await triggerReload(nextPath);
|
|
2952
|
+
};
|
|
2953
|
+
const triggerReload = async (filePath) => {
|
|
2954
|
+
if (reloadInProgress) {
|
|
2955
|
+
pendingReloadPath = filePath;
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
reloadInProgress = true;
|
|
2959
|
+
try {
|
|
2960
|
+
const normalizedConfigPath = resolvedWorkerConfigPath ? normalizePath(resolvedWorkerConfigPath) : null;
|
|
2961
|
+
if (normalizedConfigPath && normalizePath(filePath) === normalizedConfigPath) {
|
|
2962
|
+
logger?.info(`Devflare config changed: ${filePath}`);
|
|
2963
|
+
await onConfigChange();
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
logger?.info(`Worker source changed: ${filePath}`);
|
|
2967
|
+
await onWorkerChange();
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
logger?.error("Worker source reload failed:", error);
|
|
2970
|
+
} finally {
|
|
2971
|
+
reloadInProgress = false;
|
|
2972
|
+
await flushPendingReload();
|
|
2973
|
+
}
|
|
2974
|
+
};
|
|
2975
|
+
const scheduleReload = (filePath) => {
|
|
2976
|
+
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
2977
|
+
reloadTimeout = setTimeout(() => {
|
|
2978
|
+
reloadTimeout = null;
|
|
2979
|
+
triggerReload(filePath);
|
|
2980
|
+
}, 150);
|
|
2981
|
+
};
|
|
2982
|
+
const watcher = chokidar.watch(watchTargets, {
|
|
2983
|
+
ignoreInitial: true,
|
|
2984
|
+
usePolling: isWindows,
|
|
2985
|
+
interval: isWindows ? 300 : void 0,
|
|
2986
|
+
awaitWriteFinish: {
|
|
2987
|
+
stabilityThreshold: 100,
|
|
2988
|
+
pollInterval: 50
|
|
2989
|
+
},
|
|
2990
|
+
ignored: (filePath) => isIgnoredPath(filePath)
|
|
2991
|
+
});
|
|
2992
|
+
const onFileEvent = (filePath) => {
|
|
2993
|
+
if (isIgnoredPath(filePath)) return;
|
|
2994
|
+
scheduleReload(filePath);
|
|
2995
|
+
};
|
|
2996
|
+
watcher.on("change", onFileEvent);
|
|
2997
|
+
watcher.on("add", onFileEvent);
|
|
2998
|
+
watcher.on("unlink", onFileEvent);
|
|
2999
|
+
watcher.on("error", (error) => {
|
|
3000
|
+
logger?.error("Worker source watcher error:", error);
|
|
3001
|
+
});
|
|
3002
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
3003
|
+
const handleReady = () => {
|
|
3004
|
+
watcher.off("error", handleInitialError);
|
|
3005
|
+
logger?.info(`Worker source watcher ready (${watchTargets.length} target(s))`);
|
|
3006
|
+
resolvePromise();
|
|
3007
|
+
};
|
|
3008
|
+
const handleInitialError = (error) => {
|
|
3009
|
+
watcher.off("ready", handleReady);
|
|
3010
|
+
rejectPromise(error instanceof Error ? error : new Error(String(error)));
|
|
3011
|
+
};
|
|
3012
|
+
watcher.once("ready", handleReady);
|
|
3013
|
+
watcher.once("error", handleInitialError);
|
|
3014
|
+
});
|
|
3015
|
+
return watcher;
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Compute the diff between current and next watch-target lists and apply it
|
|
3019
|
+
* to the given watcher. Returns the deduped next-targets array which the
|
|
3020
|
+
* caller should store as the new "current" state.
|
|
3021
|
+
*/
|
|
3022
|
+
async function applyWatcherTargetDiff(watcher, currentTargets, nextTargets) {
|
|
3023
|
+
const nextSet = new Set(nextTargets);
|
|
3024
|
+
const targetsToRemove = currentTargets.filter((t) => !nextSet.has(t));
|
|
3025
|
+
const targetsToAdd = nextTargets.filter((t) => !currentTargets.includes(t));
|
|
3026
|
+
if (targetsToRemove.length > 0) await watcher.unwatch(targetsToRemove);
|
|
3027
|
+
if (targetsToAdd.length > 0) watcher.add(targetsToAdd);
|
|
3028
|
+
return nextTargets;
|
|
3029
|
+
}
|
|
3030
|
+
//#endregion
|
|
3031
|
+
//#region src/dev-server/server.ts
|
|
3032
|
+
function createDevServer(options) {
|
|
3033
|
+
const { cwd, configPath, vitePort = 5173, miniflarePort = 8787, miniflareHost = "127.0.0.1", enableVite: enableViteRequested = true, persist = true, logger, verbose = false, debug = process.env.DEVFLARE_DEBUG === "true" } = options;
|
|
3034
|
+
const state = createDevServerState({ enableVite: enableViteRequested });
|
|
3035
|
+
const reloadQueue = createReloadQueue({
|
|
3036
|
+
reload: async () => {
|
|
3037
|
+
if (!state.miniflare) return;
|
|
3038
|
+
const { Log, LogLevel } = await import("miniflare");
|
|
3039
|
+
const mfConfig = buildMiniflareConfig(state.currentDoResult);
|
|
3040
|
+
const log = createMiniflareLog(Log, LogLevel, "DEBUG", logger);
|
|
3041
|
+
if (log) mfConfig.log = log;
|
|
3042
|
+
mfConfig.handleRuntimeStdio = createRuntimeStdioForwarder(logger);
|
|
3043
|
+
logger?.info("Reloading Miniflare...");
|
|
3044
|
+
await state.miniflare.setOptions(mfConfig);
|
|
3045
|
+
logger?.success("Miniflare reloaded");
|
|
3046
|
+
},
|
|
3047
|
+
logger
|
|
3048
|
+
});
|
|
3049
|
+
async function bundleMainWorker() {
|
|
3050
|
+
if (!state.mainWorkerScriptPath || !state.config) {
|
|
3051
|
+
state.bundledMainWorkerScriptPath = null;
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
state.bundledMainWorkerScriptPath = await bundleWorkerEntry({
|
|
3055
|
+
cwd,
|
|
3056
|
+
inputFile: state.mainWorkerScriptPath,
|
|
3057
|
+
outFile: resolve(cwd, ".devflare", "worker-entrypoints", "main.js"),
|
|
3058
|
+
rolldownOptions: state.config.rolldown?.options,
|
|
3059
|
+
sourcemap: state.config.rolldown?.sourcemap,
|
|
3060
|
+
minify: state.config.rolldown?.minify,
|
|
3061
|
+
logger
|
|
3062
|
+
});
|
|
3063
|
+
logger?.debug(`Bundled main worker → ${state.bundledMainWorkerScriptPath}`);
|
|
3064
|
+
}
|
|
3065
|
+
function buildMiniflareConfig(doResult) {
|
|
3066
|
+
if (!state.config) throw new Error("Config not loaded");
|
|
3067
|
+
return buildMiniflareDevConfig({
|
|
3068
|
+
config: state.config,
|
|
3069
|
+
cwd,
|
|
3070
|
+
miniflarePort,
|
|
3071
|
+
miniflareHost,
|
|
3072
|
+
persist,
|
|
3073
|
+
enableVite: state.enableVite,
|
|
3074
|
+
debug,
|
|
3075
|
+
mainWorkerSurfacePaths: state.mainWorkerSurfacePaths,
|
|
3076
|
+
mainWorkerRoutes: state.mainWorkerRoutes,
|
|
3077
|
+
mainWorkerScriptPath: state.mainWorkerScriptPath,
|
|
3078
|
+
bundledMainWorkerScriptPath: state.bundledMainWorkerScriptPath,
|
|
3079
|
+
workflowEntrypointScript: state.workflowEntrypointScript,
|
|
3080
|
+
browserShimPort: state.browserShimPort,
|
|
3081
|
+
doResult,
|
|
3082
|
+
serviceBindingResolution: state.serviceBindingResolution,
|
|
3083
|
+
logger
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
async function bundleWorkflowEntrypoints() {
|
|
3087
|
+
if (!state.config) {
|
|
3088
|
+
state.workflowEntrypointScript = "";
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
state.workflowEntrypointScript = await bundleWorkflowEntrypointScript(state.config, cwd, { logger });
|
|
3092
|
+
}
|
|
3093
|
+
/**
|
|
3094
|
+
* Start Miniflare with current config
|
|
3095
|
+
*/
|
|
3096
|
+
async function startMiniflare(doResult) {
|
|
3097
|
+
const { Miniflare, Log, LogLevel } = await import("miniflare");
|
|
3098
|
+
const mfConfig = buildMiniflareConfig(doResult);
|
|
3099
|
+
const log = createMiniflareLog(Log, LogLevel, "DEBUG", logger);
|
|
3100
|
+
if (log) mfConfig.log = log;
|
|
3101
|
+
mfConfig.handleRuntimeStdio = createRuntimeStdioForwarder(logger);
|
|
3102
|
+
const shouldLogMiniflareDiagnostics = verbose || debug;
|
|
3103
|
+
if (shouldLogMiniflareDiagnostics) logMiniflareConfigDiagnostics(logger, mfConfig);
|
|
3104
|
+
state.miniflare = new Miniflare(mfConfig);
|
|
3105
|
+
await state.miniflare.ready;
|
|
3106
|
+
const displayHost = miniflareHost === "0.0.0.0" || miniflareHost === "::" ? "localhost" : miniflareHost;
|
|
3107
|
+
logger?.success(`Miniflare ready on http://${displayHost}:${miniflarePort}`);
|
|
3108
|
+
if (shouldLogMiniflareDiagnostics) await logMiniflareBindingDiagnostics(logger, state.miniflare, mfConfig);
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Reload Miniflare with updated DO bundles
|
|
3112
|
+
*/
|
|
3113
|
+
async function reloadMiniflare(doResult) {
|
|
3114
|
+
state.currentDoResult = doResult;
|
|
3115
|
+
await reloadQueue.schedule();
|
|
3116
|
+
}
|
|
3117
|
+
async function refreshWorkerOnlySurfaceState() {
|
|
3118
|
+
if (!state.config) return;
|
|
3119
|
+
state.mainWorkerSurfacePaths = await resolveMainWorkerSurfacePaths(cwd, state.config);
|
|
3120
|
+
state.mainWorkerRoutes = await discoverRoutes(cwd, state.config);
|
|
3121
|
+
const composedMainEntry = await prepareComposedWorkerEntrypoint(cwd, state.config, void 0, { devInternalEmail: true });
|
|
3122
|
+
state.mainWorkerScriptPath = composedMainEntry ? composedMainEntry : null;
|
|
3123
|
+
if (state.mainWorkerScriptPath) await bundleMainWorker();
|
|
3124
|
+
else state.bundledMainWorkerScriptPath = null;
|
|
3125
|
+
await syncWorkerWatchTargets();
|
|
3126
|
+
}
|
|
3127
|
+
function getWorkerWatchTargets() {
|
|
3128
|
+
if (state.enableVite || !state.config) return [];
|
|
3129
|
+
const targets = collectWorkerWatchRoots(cwd, state.config, state.mainWorkerSurfacePaths);
|
|
3130
|
+
if (state.resolvedWorkerConfigPath) targets.push(state.resolvedWorkerConfigPath);
|
|
3131
|
+
return [...new Set(targets)];
|
|
3132
|
+
}
|
|
3133
|
+
async function syncWorkerWatchTargets() {
|
|
3134
|
+
if (!state.workerSourceWatcher) return;
|
|
3135
|
+
state.workerWatchTargets = await applyWatcherTargetDiff(state.workerSourceWatcher, state.workerWatchTargets, getWorkerWatchTargets());
|
|
3136
|
+
}
|
|
3137
|
+
async function reloadWorkerOnlyConfig() {
|
|
3138
|
+
await loadRuntimeConfigWhenEnvReady();
|
|
3139
|
+
if (!state.config) return;
|
|
3140
|
+
setLocalSendEmailBindings(state.config.bindings?.sendEmail ?? {});
|
|
3141
|
+
await bundleWorkflowEntrypoints();
|
|
3142
|
+
await refreshWorkerOnlySurfaceState();
|
|
3143
|
+
await reloadMiniflare(state.currentDoResult);
|
|
3144
|
+
}
|
|
3145
|
+
async function loadRuntimeConfig() {
|
|
3146
|
+
state.resolvedWorkerConfigPath = await resolveWorkerConfigWatchPath(cwd, configPath);
|
|
3147
|
+
state.config = await applyLocalDevVarsToConfig(await resolveConfigEnvVars(await loadConfig({
|
|
3148
|
+
cwd,
|
|
3149
|
+
configFile: configPath
|
|
3150
|
+
}), {
|
|
3151
|
+
cwd,
|
|
3152
|
+
configPath: state.resolvedWorkerConfigPath ?? configPath,
|
|
3153
|
+
mode: "dev"
|
|
3154
|
+
}), {
|
|
3155
|
+
cwd,
|
|
3156
|
+
configPath: state.resolvedWorkerConfigPath ?? void 0
|
|
3157
|
+
});
|
|
3158
|
+
state.serviceBindingResolution = state.config.bindings?.services ? await resolveServiceBindings(state.config, state.resolvedWorkerConfigPath ? dirname(state.resolvedWorkerConfigPath) : cwd) : null;
|
|
3159
|
+
}
|
|
3160
|
+
async function waitForDotenvChange() {
|
|
3161
|
+
const configWatchPath = state.resolvedWorkerConfigPath ?? await resolveWorkerConfigWatchPath(cwd, configPath);
|
|
3162
|
+
const watchPaths = getDevflareDotenvPaths(configWatchPath ? dirname(configWatchPath) : cwd);
|
|
3163
|
+
const { watch } = await import("chokidar");
|
|
3164
|
+
await new Promise((resolveWait, rejectWait) => {
|
|
3165
|
+
const watcher = watch(watchPaths, {
|
|
3166
|
+
ignoreInitial: true,
|
|
3167
|
+
awaitWriteFinish: {
|
|
3168
|
+
stabilityThreshold: 100,
|
|
3169
|
+
pollInterval: 25
|
|
3170
|
+
}
|
|
3171
|
+
});
|
|
3172
|
+
const finish = () => {
|
|
3173
|
+
watcher.close().then(resolveWait, rejectWait);
|
|
3174
|
+
};
|
|
3175
|
+
watcher.on("add", finish);
|
|
3176
|
+
watcher.on("change", finish);
|
|
3177
|
+
watcher.on("unlink", finish);
|
|
3178
|
+
watcher.on("error", rejectWait);
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
async function loadRuntimeConfigWhenEnvReady() {
|
|
3182
|
+
while (true) try {
|
|
3183
|
+
await loadRuntimeConfig();
|
|
3184
|
+
return;
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
if (!(error instanceof EnvVarResolutionError)) throw error;
|
|
3187
|
+
logger?.warn(error.message);
|
|
3188
|
+
logger?.info("Devflare dev is waiting for .env or .env.dev to change before starting.");
|
|
3189
|
+
await waitForDotenvChange();
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
async function startWorkerSourceWatcher$1() {
|
|
3193
|
+
if (state.enableVite || !state.config) return;
|
|
3194
|
+
const watchTargets = getWorkerWatchTargets();
|
|
3195
|
+
if (watchTargets.length === 0) return;
|
|
3196
|
+
state.workerWatchTargets = watchTargets;
|
|
3197
|
+
state.workerSourceWatcher = await startWorkerSourceWatcher({
|
|
3198
|
+
watchTargets,
|
|
3199
|
+
resolvedWorkerConfigPath: state.resolvedWorkerConfigPath,
|
|
3200
|
+
logger,
|
|
3201
|
+
onConfigChange: reloadWorkerOnlyConfig,
|
|
3202
|
+
onWorkerChange: async () => {
|
|
3203
|
+
await refreshWorkerOnlySurfaceState();
|
|
3204
|
+
await reloadMiniflare(state.currentDoResult);
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Start the complete dev server
|
|
3210
|
+
*/
|
|
3211
|
+
async function start() {
|
|
3212
|
+
logger?.info("Starting unified dev server...");
|
|
3213
|
+
await loadRuntimeConfigWhenEnvReady();
|
|
3214
|
+
if (!state.config) throw new Error("Config not loaded");
|
|
3215
|
+
setLocalSendEmailBindings(state.config.bindings?.sendEmail ?? {});
|
|
3216
|
+
logger?.debug("Loaded config:", state.config.name);
|
|
3217
|
+
await bundleWorkflowEntrypoints();
|
|
3218
|
+
const viteIntegration = await resolveViteIntegration({
|
|
3219
|
+
cwd,
|
|
3220
|
+
configPath,
|
|
3221
|
+
miniflarePort,
|
|
3222
|
+
enableViteRequested: state.enableVite,
|
|
3223
|
+
logger
|
|
3224
|
+
});
|
|
3225
|
+
state.enableVite = viteIntegration.enableVite;
|
|
3226
|
+
state.generatedViteConfigPath = viteIntegration.generatedViteConfigPath;
|
|
3227
|
+
await refreshWorkerOnlySurfaceState();
|
|
3228
|
+
if (!state.enableVite && (hasWorkerSurfacePaths(state.mainWorkerSurfacePaths) || Boolean(state.mainWorkerRoutes?.routes.length))) logWorkerHandlerDetection(logger, state.enableVite, true, state.mainWorkerSurfacePaths, state.mainWorkerRoutes);
|
|
3229
|
+
else if (!state.enableVite) logWorkerHandlerDetection(logger, state.enableVite, false, state.mainWorkerSurfacePaths, state.mainWorkerRoutes);
|
|
3230
|
+
logRemoteBindingRequirements(logger, await checkRemoteBindingRequirements(state.config));
|
|
3231
|
+
state.browserShim = await maybeStartBrowserShim(state.config, {
|
|
3232
|
+
browserShimPort: state.browserShimPort,
|
|
3233
|
+
logger,
|
|
3234
|
+
verbose
|
|
3235
|
+
});
|
|
3236
|
+
const doInit = await maybeStartDOBundler(state.config, {
|
|
3237
|
+
cwd,
|
|
3238
|
+
logger,
|
|
3239
|
+
onRebuild: async (result) => {
|
|
3240
|
+
await reloadMiniflare(result);
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
state.doBundler = doInit.bundler;
|
|
3244
|
+
const doResult = doInit.result;
|
|
3245
|
+
state.currentDoResult = doResult;
|
|
3246
|
+
await startMiniflare(doResult);
|
|
3247
|
+
await startWorkerSourceWatcher$1();
|
|
3248
|
+
if (state.enableVite) state.viteProcess = await startViteProcess({
|
|
3249
|
+
cwd,
|
|
3250
|
+
configPath,
|
|
3251
|
+
vitePort,
|
|
3252
|
+
miniflarePort,
|
|
3253
|
+
generatedViteConfigPath: state.generatedViteConfigPath,
|
|
3254
|
+
logger
|
|
3255
|
+
});
|
|
3256
|
+
else logger?.info("Vite startup skipped (no effective Vite config found for this package)");
|
|
3257
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3258
|
+
await runD1Migrations({
|
|
3259
|
+
cwd,
|
|
3260
|
+
config: state.config,
|
|
3261
|
+
miniflarePort,
|
|
3262
|
+
logger
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Stop the dev server
|
|
3267
|
+
*/
|
|
3268
|
+
async function stop() {
|
|
3269
|
+
await disposeDevServerState(state);
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Get Miniflare instance
|
|
3273
|
+
*/
|
|
3274
|
+
function getMiniflare() {
|
|
3275
|
+
return state.miniflare;
|
|
3276
|
+
}
|
|
3277
|
+
return {
|
|
3278
|
+
start,
|
|
3279
|
+
stop,
|
|
3280
|
+
getMiniflare
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
//#endregion
|
|
3284
|
+
//#region src/cli/commands/dev.ts
|
|
3285
|
+
function readStringOption(value) {
|
|
3286
|
+
return typeof value === "string" ? value : void 0;
|
|
3287
|
+
}
|
|
3288
|
+
function parsePort(value, source) {
|
|
3289
|
+
const port = Number(value);
|
|
3290
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error(`${source} must be an integer between 1 and 65535`);
|
|
3291
|
+
return port;
|
|
3292
|
+
}
|
|
3293
|
+
function resolveSinglePort(runtimePort, bridgePort, source) {
|
|
3294
|
+
if (runtimePort && bridgePort && runtimePort !== bridgePort) throw new Error(`Conflicting Devflare runtime ports: ${source} runtime port is ${runtimePort}, but bridge port is ${bridgePort}. Use one value for both.`);
|
|
3295
|
+
const value = runtimePort ?? bridgePort;
|
|
3296
|
+
return value ? parsePort(value, source) : void 0;
|
|
3297
|
+
}
|
|
3298
|
+
function resolveDevRuntimePort(options, env = process.env, configPort) {
|
|
3299
|
+
return resolveSinglePort(readStringOption(options["runtime-port"]), readStringOption(options["bridge-port"]), "CLI") ?? resolveSinglePort(env.DEVFLARE_RUNTIME_PORT, env.DEVFLARE_BRIDGE_PORT, "environment") ?? configPort ?? 8787;
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Resolve the host the local Miniflare runtime instance binds to.
|
|
3303
|
+
*
|
|
3304
|
+
* Precedence: `--runtime-host` CLI flag > `DEVFLARE_RUNTIME_HOST` env var >
|
|
3305
|
+
* `server.host` config value > `127.0.0.1`.
|
|
3306
|
+
*/
|
|
3307
|
+
function resolveDevRuntimeHost(options, env = process.env, configHost) {
|
|
3308
|
+
const envHost = env.DEVFLARE_RUNTIME_HOST?.trim();
|
|
3309
|
+
return readStringOption(options["runtime-host"]) ?? (envHost ? envHost : void 0) ?? configHost ?? "127.0.0.1";
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Create a log writer that writes to both terminal and file
|
|
3313
|
+
*/
|
|
3314
|
+
async function createLogWriter(cwd, options) {
|
|
3315
|
+
if (!options.log && !options.logTemp) return null;
|
|
3316
|
+
const fs = await import("node:fs");
|
|
3317
|
+
let logPath;
|
|
3318
|
+
if (options.logTemp) logPath = resolve(cwd, ".log");
|
|
3319
|
+
else logPath = resolve(cwd, `.log-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19)}`);
|
|
3320
|
+
const fileStream = fs.createWriteStream(logPath, { flags: "w" });
|
|
3321
|
+
const ansiRegex = /\x1b\[[0-9;]*m/g;
|
|
3322
|
+
return {
|
|
3323
|
+
path: logPath,
|
|
3324
|
+
write(data, source) {
|
|
3325
|
+
const str = typeof data === "string" ? data : data.toString();
|
|
3326
|
+
if (!str.trim()) return;
|
|
3327
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 23);
|
|
3328
|
+
const prefix = source ? `[${timestamp}][${source.toUpperCase()}] ` : `[${timestamp}] `;
|
|
3329
|
+
const cleanStr = str.replace(ansiRegex, "");
|
|
3330
|
+
fileStream.write(prefix + cleanStr + (cleanStr.endsWith("\n") ? "" : "\n"));
|
|
3331
|
+
},
|
|
3332
|
+
close() {
|
|
3333
|
+
fileStream.end();
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
async function runDevCommand(parsed, logger, options) {
|
|
3338
|
+
const cwd = options.cwd || parsed.options.cwd || process.cwd();
|
|
3339
|
+
const configPath = parsed.options.config;
|
|
3340
|
+
const port = parsed.options.port;
|
|
3341
|
+
const logEnabled = parsed.options.log === true;
|
|
3342
|
+
const logTempEnabled = parsed.options["log-temp"] === true;
|
|
3343
|
+
const persistEnabled = parsed.options.persist === true;
|
|
3344
|
+
const debugEnabled = parsed.options.debug === true || process.env.DEVFLARE_DEBUG === "true";
|
|
3345
|
+
const verbose = parsed.options.verbose === true || debugEnabled;
|
|
3346
|
+
const theme = createCliTheme(parsed.options);
|
|
3347
|
+
const config = await loadConfig({
|
|
3348
|
+
cwd,
|
|
3349
|
+
configFile: configPath
|
|
3350
|
+
});
|
|
3351
|
+
let miniflarePort;
|
|
3352
|
+
let miniflareHost;
|
|
3353
|
+
try {
|
|
3354
|
+
miniflarePort = resolveDevRuntimePort(parsed.options, process.env, config.server?.port);
|
|
3355
|
+
miniflareHost = resolveDevRuntimeHost(parsed.options, process.env, config.server?.host);
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3358
|
+
return { exitCode: 1 };
|
|
3359
|
+
}
|
|
3360
|
+
const viteProject = resolveEffectiveViteProject(await detectViteProject(cwd), config);
|
|
3361
|
+
const logWriter = await createLogWriter(cwd, {
|
|
3362
|
+
log: logEnabled,
|
|
3363
|
+
logTemp: logTempEnabled
|
|
3364
|
+
});
|
|
3365
|
+
if (logWriter) {
|
|
3366
|
+
const logFile = relative(cwd, logWriter.path) || ".log";
|
|
3367
|
+
logLine(logger, `${dim("logging", theme)} ${logFile}`);
|
|
3368
|
+
}
|
|
3369
|
+
const devLogger = createConsola({ level: verbose ? 4 : 3 });
|
|
3370
|
+
if (logWriter) {
|
|
3371
|
+
const wrapLog = (original, prefix = "") => {
|
|
3372
|
+
return (message, ...args) => {
|
|
3373
|
+
original(message, ...args);
|
|
3374
|
+
const formatted = prefix ? `${prefix} ${[message, ...args].join(" ")}` : [message, ...args].join(" ");
|
|
3375
|
+
logWriter.write(formatted);
|
|
3376
|
+
};
|
|
3377
|
+
};
|
|
3378
|
+
Object.assign(devLogger.log, wrapLog(devLogger.log.bind(devLogger)));
|
|
3379
|
+
Object.assign(devLogger.info, wrapLog(devLogger.info.bind(devLogger)));
|
|
3380
|
+
Object.assign(devLogger.error, wrapLog(devLogger.error.bind(devLogger), "[ERROR]"));
|
|
3381
|
+
Object.assign(devLogger.warn, wrapLog(devLogger.warn.bind(devLogger), "[WARN]"));
|
|
3382
|
+
Object.assign(devLogger.success, wrapLog(devLogger.success.bind(devLogger), "[OK]"));
|
|
3383
|
+
Object.assign(devLogger.debug, wrapLog(devLogger.debug.bind(devLogger), "[DEBUG]"));
|
|
3384
|
+
}
|
|
3385
|
+
try {
|
|
3386
|
+
logLine(logger);
|
|
3387
|
+
if (viteProject.shouldStartVite) {
|
|
3388
|
+
logLine(logger, `${cyanBold("dev", theme)} ${dim("Unified Dev Server", theme)}`);
|
|
3389
|
+
logLine(logger, " ├─ Vite: Full HMR for frontend");
|
|
3390
|
+
logLine(logger, " ├─ Miniflare: All Cloudflare bindings");
|
|
3391
|
+
logLine(logger, " ├─ Rolldown: Worker + DO bundling with watch");
|
|
3392
|
+
logLine(logger, " └─ Bridge: WebSocket RPC connection");
|
|
3393
|
+
} else {
|
|
3394
|
+
logLine(logger, `${cyanBold("dev", theme)} ${dim("Worker Dev Server", theme)}`);
|
|
3395
|
+
logLine(logger, " ├─ Miniflare: All Cloudflare bindings");
|
|
3396
|
+
logLine(logger, " ├─ Rolldown: Worker + DO bundling with watch");
|
|
3397
|
+
logLine(logger, " └─ Vite: Disabled (no effective Vite config found)");
|
|
3398
|
+
if (viteProject.wantsViteIntegration) {
|
|
3399
|
+
logger.warn("Vite-related settings were detected, but no effective Vite config was available");
|
|
3400
|
+
logger.warn("Skipping Vite startup and running in worker-only mode");
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
logLine(logger);
|
|
3404
|
+
const devServer = createDevServer({
|
|
3405
|
+
cwd,
|
|
3406
|
+
configPath,
|
|
3407
|
+
vitePort: port ? Number.parseInt(port, 10) : 5173,
|
|
3408
|
+
miniflarePort,
|
|
3409
|
+
miniflareHost,
|
|
3410
|
+
enableVite: viteProject.shouldStartVite,
|
|
3411
|
+
persist: persistEnabled,
|
|
3412
|
+
logger: devLogger,
|
|
3413
|
+
verbose,
|
|
3414
|
+
debug: debugEnabled
|
|
3415
|
+
});
|
|
3416
|
+
let isCleaningUp = false;
|
|
3417
|
+
const cleanupHandlers = /* @__PURE__ */ new Map();
|
|
3418
|
+
const removeCleanupHandlers = () => {
|
|
3419
|
+
for (const [event, handler] of cleanupHandlers) process.off(event, handler);
|
|
3420
|
+
cleanupHandlers.clear();
|
|
3421
|
+
};
|
|
3422
|
+
const cleanup = async (exitCode, reason) => {
|
|
3423
|
+
if (isCleaningUp) return;
|
|
3424
|
+
isCleaningUp = true;
|
|
3425
|
+
removeCleanupHandlers();
|
|
3426
|
+
if (reason) {
|
|
3427
|
+
const message = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
|
|
3428
|
+
logger.error(message);
|
|
3429
|
+
}
|
|
3430
|
+
logLine(logger);
|
|
3431
|
+
logLine(logger, `${yellow("dev", theme)} ${dim("Shutting down…", theme)}`);
|
|
3432
|
+
try {
|
|
3433
|
+
await devServer.stop();
|
|
3434
|
+
} finally {
|
|
3435
|
+
logWriter?.close();
|
|
3436
|
+
process.exit(exitCode);
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
const registerCleanupHandler = (event, handler) => {
|
|
3440
|
+
cleanupHandlers.set(event, handler);
|
|
3441
|
+
process.on(event, handler);
|
|
3442
|
+
};
|
|
3443
|
+
registerCleanupHandler("SIGINT", () => {
|
|
3444
|
+
cleanup(0);
|
|
3445
|
+
});
|
|
3446
|
+
registerCleanupHandler("SIGTERM", () => {
|
|
3447
|
+
cleanup(0);
|
|
3448
|
+
});
|
|
3449
|
+
registerCleanupHandler("SIGHUP", () => {
|
|
3450
|
+
cleanup(0);
|
|
3451
|
+
});
|
|
3452
|
+
registerCleanupHandler("uncaughtException", (error) => {
|
|
3453
|
+
cleanup(1, error);
|
|
3454
|
+
});
|
|
3455
|
+
registerCleanupHandler("unhandledRejection", (reason) => {
|
|
3456
|
+
cleanup(1, reason);
|
|
3457
|
+
});
|
|
3458
|
+
await devServer.start();
|
|
3459
|
+
await new Promise(() => {});
|
|
3460
|
+
return { exitCode: 0 };
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
logWriter?.close();
|
|
3463
|
+
if (error instanceof Error) {
|
|
3464
|
+
logger.error("Dev server failed:", error.message);
|
|
3465
|
+
if (verbose) logger.error(error.stack);
|
|
3466
|
+
}
|
|
3467
|
+
return { exitCode: 1 };
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
//#endregion
|
|
3471
|
+
export { runDevCommand };
|