devflare 1.0.0-next.39 → 1.0.0-next.40

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