@astrale-os/adapter-cloudflare 0.4.10 → 0.5.0-beta.2

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 (128) hide show
  1. package/dist/build/codegen/identity.d.ts.map +1 -0
  2. package/dist/build/codegen/identity.js.map +1 -0
  3. package/dist/build/codegen/merge.d.ts.map +1 -0
  4. package/dist/build/codegen/merge.js.map +1 -0
  5. package/dist/build/codegen/worker.d.ts +12 -0
  6. package/dist/build/codegen/worker.d.ts.map +1 -0
  7. package/dist/build/codegen/worker.js +40 -0
  8. package/dist/build/codegen/worker.js.map +1 -0
  9. package/dist/{codegen → build/codegen}/wrangler.d.ts +6 -3
  10. package/dist/build/codegen/wrangler.d.ts.map +1 -0
  11. package/dist/{codegen → build/codegen}/wrangler.js +29 -10
  12. package/dist/build/codegen/wrangler.js.map +1 -0
  13. package/dist/build/frontend.d.ts +26 -0
  14. package/dist/build/frontend.d.ts.map +1 -0
  15. package/dist/build/frontend.js +263 -0
  16. package/dist/build/frontend.js.map +1 -0
  17. package/dist/{build.js → build/index.d.ts} +5 -4
  18. package/dist/build/index.d.ts.map +1 -0
  19. package/dist/{build.d.ts → build/index.js} +5 -5
  20. package/dist/build/index.js.map +1 -0
  21. package/dist/{parse-output.d.ts → build/parse-output.d.ts} +2 -0
  22. package/dist/build/parse-output.d.ts.map +1 -0
  23. package/dist/{parse-output.js → build/parse-output.js} +5 -0
  24. package/dist/build/parse-output.js.map +1 -0
  25. package/dist/build/prepare.d.ts +18 -0
  26. package/dist/build/prepare.d.ts.map +1 -0
  27. package/dist/build/prepare.js +117 -0
  28. package/dist/build/prepare.js.map +1 -0
  29. package/dist/build/worker.d.ts +15 -0
  30. package/dist/build/worker.d.ts.map +1 -0
  31. package/dist/build/worker.js +21 -0
  32. package/dist/build/worker.js.map +1 -0
  33. package/dist/{wrangler-cli.d.ts → build/wrangler-cli.d.ts} +8 -0
  34. package/dist/build/wrangler-cli.d.ts.map +1 -0
  35. package/dist/{wrangler-cli.js → build/wrangler-cli.js} +28 -4
  36. package/dist/build/wrangler-cli.js.map +1 -0
  37. package/dist/cloudflare.d.ts +21 -24
  38. package/dist/cloudflare.d.ts.map +1 -1
  39. package/dist/cloudflare.js +125 -191
  40. package/dist/cloudflare.js.map +1 -1
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/params.d.ts +13 -16
  46. package/dist/params.d.ts.map +1 -1
  47. package/dist/worker/index.d.ts +48 -0
  48. package/dist/worker/index.d.ts.map +1 -0
  49. package/dist/worker/index.js +186 -0
  50. package/dist/worker/index.js.map +1 -0
  51. package/package.json +13 -5
  52. package/src/build/codegen/worker.ts +53 -0
  53. package/src/{codegen → build/codegen}/wrangler.ts +34 -12
  54. package/src/build/frontend.ts +324 -0
  55. package/src/{build.ts → build/index.ts} +17 -4
  56. package/src/{parse-output.ts → build/parse-output.ts} +6 -0
  57. package/src/build/prepare.ts +161 -0
  58. package/src/build/worker.ts +37 -0
  59. package/src/{wrangler-cli.ts → build/wrangler-cli.ts} +44 -6
  60. package/src/cloudflare.ts +153 -223
  61. package/src/index.ts +4 -4
  62. package/src/params.ts +13 -17
  63. package/src/worker/index.ts +276 -0
  64. package/src/worker/tsconfig.json +10 -0
  65. package/template/astrale.config.ts +7 -7
  66. package/dist/assets-pack.d.ts +0 -8
  67. package/dist/assets-pack.d.ts.map +0 -1
  68. package/dist/assets-pack.js +0 -30
  69. package/dist/assets-pack.js.map +0 -1
  70. package/dist/build.d.ts.map +0 -1
  71. package/dist/build.js.map +0 -1
  72. package/dist/client.d.ts +0 -17
  73. package/dist/client.d.ts.map +0 -1
  74. package/dist/client.js +0 -59
  75. package/dist/client.js.map +0 -1
  76. package/dist/codegen/identity.d.ts.map +0 -1
  77. package/dist/codegen/identity.js.map +0 -1
  78. package/dist/codegen/merge.d.ts.map +0 -1
  79. package/dist/codegen/merge.js.map +0 -1
  80. package/dist/codegen/worker.d.ts +0 -51
  81. package/dist/codegen/worker.d.ts.map +0 -1
  82. package/dist/codegen/worker.js +0 -120
  83. package/dist/codegen/worker.js.map +0 -1
  84. package/dist/codegen/wrangler.d.ts.map +0 -1
  85. package/dist/codegen/wrangler.js.map +0 -1
  86. package/dist/parse-output.d.ts.map +0 -1
  87. package/dist/parse-output.js.map +0 -1
  88. package/dist/wrangler-cli.d.ts.map +0 -1
  89. package/dist/wrangler-cli.js.map +0 -1
  90. package/src/assets-pack.ts +0 -32
  91. package/src/client.ts +0 -89
  92. package/src/codegen/worker.ts +0 -136
  93. package/template/.env.example +0 -9
  94. package/template/.gitignore.template +0 -16
  95. package/template/CLAUDE.md +0 -13
  96. package/template/README.md +0 -47
  97. package/template/client/.gitignore.template +0 -4
  98. package/template/client/__tests__/app.test.tsx +0 -15
  99. package/template/client/components.json +0 -25
  100. package/template/client/index.html +0 -12
  101. package/template/client/package.json +0 -35
  102. package/template/client/src/app.tsx +0 -52
  103. package/template/client/src/components/ui/cn.ts +0 -6
  104. package/template/client/src/main.tsx +0 -9
  105. package/template/client/src/styles.css +0 -78
  106. package/template/client/tsconfig.json +0 -26
  107. package/template/client/vite.config.ts +0 -22
  108. package/template/client/vitest.config.ts +0 -13
  109. package/template/core/README.md +0 -7
  110. package/template/deps.ts +0 -9
  111. package/template/domain.ts +0 -11
  112. package/template/env.ts +0 -8
  113. package/template/icons.ts +0 -4
  114. package/template/integrations/README.md +0 -7
  115. package/template/oxlint.config.ts +0 -1
  116. package/template/package.json +0 -58
  117. package/template/pnpm-workspace.yaml +0 -31
  118. package/template/runtime/index.ts +0 -8
  119. package/template/schema/index.ts +0 -10
  120. package/template/simulation/README.md +0 -12
  121. package/template/tsconfig.json +0 -28
  122. package/template/vitest.config.ts +0 -7
  123. /package/dist/{codegen → build/codegen}/identity.d.ts +0 -0
  124. /package/dist/{codegen → build/codegen}/identity.js +0 -0
  125. /package/dist/{codegen → build/codegen}/merge.d.ts +0 -0
  126. /package/dist/{codegen → build/codegen}/merge.js +0 -0
  127. /package/src/{codegen → build/codegen}/identity.ts +0 -0
  128. /package/src/{codegen → build/codegen}/merge.ts +0 -0
package/src/cloudflare.ts CHANGED
@@ -2,44 +2,87 @@
2
2
  * `cloudflare(envs)` — the Cloudflare deployment adapter.
3
3
  *
4
4
  * Owns everything target-specific: it codegens the Worker entry + wrangler
5
- * config (the dev never sees a `wrangler.jsonc`), builds the optional client
5
+ * config (the dev never sees a `wrangler.jsonc`), materializes the optional frontend
6
6
  * SPA, and shells out to `wrangler`. `watch` → `wrangler dev` (local URL);
7
7
  * `deploy` → `wrangler deploy` (workers.dev or a custom-domain URL); secrets →
8
8
  * `wrangler secret bulk`. Both `watch` and `deploy` return the URL the CLI
9
9
  * prints and `astrale domain install <url>` consumes.
10
10
  */
11
11
 
12
- import type { DeployCtx, DomainAdapter, WatchCtx } from '@astrale-os/sdk'
12
+ import type { DomainAdapter } from '@astrale-os/sdk'
13
13
 
14
14
  import { defineAdapter } from '@astrale-os/sdk'
15
- import { mkdir, rename, writeFile } from 'node:fs/promises'
16
- import { join } from 'node:path'
17
15
 
18
- import type { CloudflareParams } from './params'
16
+ import type { CloudflareParams } from './params.js'
19
17
 
20
- import { buildClient, resolveClientDir } from './client'
21
- import { ensureIdentity } from './codegen/identity'
22
- import { generateWorkerEntry } from './codegen/worker'
23
- import { generateWranglerConfig } from './codegen/wrangler'
24
- import { bulkPutSecrets, runWranglerDeploy, runWranglerDev } from './wrangler-cli'
25
-
26
- const DEFAULT_PORT = 8787
18
+ import {
19
+ bulkPutSecrets,
20
+ buildCloudflareWorker,
21
+ buildFrontend,
22
+ logTo,
23
+ prepare,
24
+ runWranglerDeploy,
25
+ runWranglerDev,
26
+ runWranglerTypes,
27
+ watchFrontend,
28
+ } from './build/index.js'
27
29
 
28
30
  export function cloudflare(
29
31
  envs: Record<string, CloudflareParams>,
32
+ ): DomainAdapter<CloudflareParams> {
33
+ return cloudflareWithDependencies(envs, {
34
+ bulkPutSecrets,
35
+ buildCloudflareWorker,
36
+ buildFrontend,
37
+ logTo,
38
+ prepare,
39
+ runWranglerDeploy,
40
+ runWranglerDev,
41
+ runWranglerTypes,
42
+ watchFrontend,
43
+ waitUntilLive,
44
+ })
45
+ }
46
+
47
+ interface CloudflareAdapterDependencies {
48
+ readonly bulkPutSecrets: typeof bulkPutSecrets
49
+ readonly buildCloudflareWorker: typeof buildCloudflareWorker
50
+ readonly buildFrontend: typeof buildFrontend
51
+ readonly logTo: typeof logTo
52
+ readonly prepare: typeof prepare
53
+ readonly runWranglerDeploy: typeof runWranglerDeploy
54
+ readonly runWranglerDev: typeof runWranglerDev
55
+ readonly runWranglerTypes: typeof runWranglerTypes
56
+ readonly watchFrontend: typeof watchFrontend
57
+ readonly waitUntilLive: typeof waitUntilLive
58
+ }
59
+
60
+ /** Exact orchestration seam used by adapter-level deployment qualification. */
61
+ export function cloudflareWithDependencies(
62
+ envs: Record<string, CloudflareParams>,
63
+ dependencies: CloudflareAdapterDependencies,
30
64
  ): DomainAdapter<CloudflareParams> {
31
65
  return defineAdapter<CloudflareParams>({
32
66
  name: 'cloudflare',
33
67
  envs,
34
68
 
69
+ async build(params, ctx) {
70
+ await dependencies.buildCloudflareWorker({
71
+ params,
72
+ context: ctx,
73
+ onLog: dependencies.logTo(),
74
+ })
75
+ },
76
+
35
77
  async watch(params, ctx) {
36
- const clientDir = resolveCloudflareClientDir(params, ctx)
37
- const { configPath, port } = await prepare(params, ctx, 'dev', {
38
- servesClient: Boolean(clientDir),
39
- bundleAssets: Boolean(clientDir),
78
+ const onLog = dependencies.logTo()
79
+ const frontend = await dependencies.buildFrontend({
80
+ definition: ctx.definition,
81
+ projectDir: ctx.projectDir,
82
+ onLog,
40
83
  })
41
- if (clientDir) await buildClient(clientDir, ctx.projectDir, logTo())
42
- const handle = await runWranglerDev({
84
+ const { configPath, port } = await dependencies.prepare(params, ctx, 'dev', frontend)
85
+ const handle = await dependencies.runWranglerDev({
43
86
  projectDir: ctx.projectDir,
44
87
  configPath,
45
88
  port,
@@ -47,58 +90,104 @@ export function cloudflare(
47
90
  autoPickPort: params.port === undefined,
48
91
  ...(params.host ? { ip: '0.0.0.0' } : {}),
49
92
  onReload: ctx.onReload,
50
- onLog: logTo(),
93
+ onLog,
51
94
  })
95
+ let frontendWatch: ReturnType<typeof watchFrontend>
96
+ try {
97
+ frontendWatch = dependencies.watchFrontend({
98
+ definition: ctx.definition,
99
+ projectDir: ctx.projectDir,
100
+ onLog,
101
+ // Routes and source recipes are immutable for this watch generation.
102
+ // Wrangler already observes the atomically swapped asset stage, so a
103
+ // source rebuild needs no config regeneration.
104
+ onRebuild: ctx.onReload,
105
+ })
106
+ } catch (error) {
107
+ try {
108
+ await handle.stop()
109
+ } catch (cleanupError) {
110
+ throw new AggregateError(
111
+ [error, cleanupError],
112
+ 'cloudflare frontend watcher startup failed and Wrangler cleanup also failed',
113
+ )
114
+ }
115
+ throw error
116
+ }
52
117
  // With --host the actionable URL is the PUBLIC one (the tunnel/proxy
53
118
  // front), not the local bind — print and spec-stamp that.
54
- return { url: params.host ?? handle.url, stop: handle.stop }
119
+ return {
120
+ url: params.host ?? handle.url,
121
+ async stop() {
122
+ const errors: unknown[] = []
123
+ try {
124
+ await frontendWatch?.stop()
125
+ } catch (error) {
126
+ errors.push(error)
127
+ }
128
+ try {
129
+ await handle.stop()
130
+ } catch (error) {
131
+ errors.push(error)
132
+ }
133
+ if (errors.length === 1) throw errors[0]
134
+ if (errors.length > 1) {
135
+ throw new AggregateError(errors, 'cloudflare development cleanup failed')
136
+ }
137
+ },
138
+ }
55
139
  },
56
140
 
57
141
  // Config hot-regen: rewrite the generated files only — the running
58
142
  // `wrangler dev` watches its `--config` (and the worker entry) and reloads
59
143
  // them itself, so a `astrale.config.ts` edit lands without a restart.
60
- // Mirrors `watch`'s prep exactly (codegen + client build), minus the spawn.
144
+ // Mirrors `watch`'s prep exactly (codegen + frontend build), minus the spawn.
61
145
  async regenerate(params, ctx) {
62
- const clientDir = resolveCloudflareClientDir(params, ctx)
63
- await prepare(params, ctx, 'dev', {
64
- servesClient: Boolean(clientDir),
65
- bundleAssets: Boolean(clientDir),
146
+ const frontend = await dependencies.buildFrontend({
147
+ definition: ctx.definition,
148
+ projectDir: ctx.projectDir,
149
+ onLog: dependencies.logTo(),
150
+ })
151
+ const { configPath } = await dependencies.prepare(params, ctx, 'dev', frontend)
152
+ await dependencies.runWranglerTypes({
153
+ projectDir: ctx.projectDir,
154
+ configPath,
155
+ onLog: dependencies.logTo(),
66
156
  })
67
- if (clientDir) await buildClient(clientDir, ctx.projectDir, logTo())
68
157
  },
69
158
 
70
159
  async deploy(params, ctx) {
71
- const clientDir = resolveCloudflareClientDir(params, ctx)
160
+ const frontend = await dependencies.buildFrontend({
161
+ definition: ctx.definition,
162
+ projectDir: ctx.projectDir,
163
+ onLog: dependencies.logTo(),
164
+ })
72
165
  const {
73
166
  configPath,
74
167
  fallbackConfigPath,
75
168
  workerName: name,
76
- } = await prepare(params, ctx, 'deploy', {
77
- servesClient: Boolean(clientDir),
78
- bundleAssets: Boolean(clientDir),
79
- })
80
- if (clientDir) await buildClient(clientDir, ctx.projectDir, logTo())
81
- const { url } = await runWranglerDeploy({
169
+ } = await dependencies.prepare(params, ctx, 'deploy', frontend)
170
+ const { url, deploymentVersion } = await dependencies.runWranglerDeploy({
82
171
  projectDir: ctx.projectDir,
83
172
  configPath,
84
173
  workerName: name,
85
174
  ...(fallbackConfigPath ? { fallbackConfigPath } : {}),
86
175
  ...(params.route ? { route: params.route } : {}),
87
- onLog: logTo(),
176
+ onLog: dependencies.logTo(),
88
177
  })
89
178
  if (Object.keys(ctx.secrets).length > 0) {
90
- await bulkPutSecrets({
179
+ await dependencies.bulkPutSecrets({
91
180
  projectDir: ctx.projectDir,
92
181
  configPath,
93
182
  secrets: ctx.secrets,
94
- onLog: logTo(),
183
+ onLog: dependencies.logTo(),
95
184
  })
96
185
  }
97
186
  // A first deploy on a fresh `*.workers.dev` host can take ~30-60s to
98
187
  // propagate; an `astrale domain install <url>` issued right away would
99
188
  // 404 (Cloudflare 1042). Block until the URL actually serves so the
100
189
  // install line we print is immediately actionable.
101
- await waitUntilLive(url, logTo())
190
+ await dependencies.waitUntilLive(url, dependencies.logTo(), deploymentVersion)
102
191
  return { url }
103
192
  },
104
193
 
@@ -108,160 +197,50 @@ export function cloudflare(
108
197
  })
109
198
  }
110
199
 
111
- function resolveCloudflareClientDir(
112
- params: CloudflareParams,
113
- ctx: WatchCtx | DeployCtx,
114
- ): string | undefined {
115
- return resolveClientDir({
116
- adapterName: 'cloudflare',
117
- env: ctx.env,
118
- projectDir: ctx.projectDir,
119
- domain: ctx.domain,
120
- ...(params.client ? { client: params.client } : {}),
121
- })
122
- }
123
-
124
- // ── codegen orchestration ──────────────────────────────────────────────────
125
-
126
- export async function prepare(
127
- params: CloudflareParams,
128
- ctx: WatchCtx | DeployCtx,
129
- mode: 'dev' | 'deploy',
130
- clientAssets: { servesClient: boolean; bundleAssets: boolean } = {
131
- servesClient: false,
132
- bundleAssets: false,
133
- },
134
- ): Promise<{ configPath: string; fallbackConfigPath?: string; port: number; workerName: string }> {
135
- const astraleDir = join(ctx.projectDir, '.astrale')
136
- await mkdir(astraleDir, { recursive: true })
137
- await ensureIdentity(astraleDir, ctx.domain.origin)
138
-
139
- const name = workerName(params, ctx.domain.origin)
140
- // Instance-router service binding (on by default → admin-router): lets the
141
- // worker reach other instances' hosts internally, so a cross-instance JWKS
142
- // fetch doesn't 522. Resolved once, fed to BOTH codegens below.
143
- const router = resolveRouter(params.router)
144
- // Two distinct client signals: the WORKER's `/ui` hook and Wrangler's local
145
- // assets binding. Direct Cloudflare deploys use both; managed deploys ship
146
- // assets separately but still need the worker hook.
147
- const servesClient = clientAssets.servesClient
148
- const bundleAssets = clientAssets.bundleAssets
149
-
150
- await writeFileAtomic(
151
- join(astraleDir, 'worker.gen.ts'),
152
- // Everything else — schema/methods/deps/views/functions/requires/postInstall —
153
- // rides the `...domain` spread the generated entry imports from `../domain`;
154
- // the only remaining build-time signal is whether to wire the /ui asset hook.
155
- generateWorkerEntry({
156
- origin: ctx.domain.origin,
157
- hasClient: servesClient,
158
- ...(router
159
- ? {
160
- router: {
161
- binding: router.binding,
162
- hostSuffix: router.hostSuffix,
163
- minLabels: router.minLabels,
164
- },
165
- }
166
- : {}),
167
- }),
168
- )
169
-
170
- // Pin the worker's canonical serving URL (its `iss` identity) for routed
171
- // deploys: a routed worker may ALSO be reachable via `*.workers.dev`, so
172
- // resolving the URL from the per-request Host would let `iss` drift between
173
- // hostnames. Dev + workers.dev-only deploys are single-host, so the worker
174
- // falls back to the per-request Host (always the canonical URL there).
175
- // An explicit `vars.WORKER_URL` (e.g. a tunnel/proxy front) wins.
176
- // Dev behind a tunnel/proxy (`--host`) pins WORKER_URL the same way a routed
177
- // deploy does — the per-request Host there is the proxy's INTERNAL hostname
178
- // and would corrupt the worker's `iss`.
179
- const workerUrl =
180
- mode === 'deploy' && params.route
181
- ? `https://${params.route}`
182
- : mode === 'dev' && params.host
183
- ? params.host
184
- : undefined
185
-
186
- // Dev injects secrets as local vars (the gitignored .astrale config never
187
- // ships); deploy keeps secrets out of the config and pushes them encrypted.
188
- const vars = {
189
- ...(workerUrl ? { WORKER_URL: workerUrl } : {}),
190
- ...(mode === 'dev' ? { ...params.vars, ...ctx.secrets } : { ...params.vars }),
191
- }
192
-
193
- const baseConfig = {
194
- workerName: name,
195
- ...(mode === 'deploy' && params.route ? { route: params.route } : {}),
196
- hasClient: bundleAssets,
197
- vars,
198
- // Router service binding flows into BOTH the SELF and SELF-less fallback
199
- // configs — the first-deploy two-pass must keep it. `admin-router` already
200
- // exists, so unlike SELF it always resolves (no first-deploy chicken-egg).
201
- ...(router ? { router: { binding: router.binding, service: router.service } } : {}),
202
- // Escape hatch: extra bindings (KV/R2/D1/queues/…) deep-merged on top. Same
203
- // overlay in dev and deploy — it's infra, not env-specific plumbing — and it
204
- // flows into both the SELF and the SELF-less fallback config below.
205
- ...(params.wrangler ? { wrangler: params.wrangler } : {}),
206
- }
207
-
208
- // Always self-bind: it enables autobinding (a handler calling its own domain).
209
- // Locally the dev registry resolves it; on deploy the first deploy of a fresh
210
- // worker can't (the script doesn't exist yet), so we also emit a SELF-less
211
- // fallback config that `runWranglerDeploy` uses for a one-time two-pass deploy.
212
- const configPath = join(astraleDir, 'wrangler.gen.jsonc')
213
- await writeFileAtomic(configPath, generateWranglerConfig({ ...baseConfig, selfBinding: true }))
214
-
215
- let fallbackConfigPath: string | undefined
216
- if (mode === 'deploy') {
217
- fallbackConfigPath = join(astraleDir, 'wrangler.no-self.gen.jsonc')
218
- await writeFileAtomic(
219
- fallbackConfigPath,
220
- generateWranglerConfig({ ...baseConfig, selfBinding: false }),
221
- )
222
- }
223
-
224
- return {
225
- configPath,
226
- ...(fallbackConfigPath ? { fallbackConfigPath } : {}),
227
- port: params.port ?? DEFAULT_PORT,
228
- workerName: name,
229
- }
230
- }
231
-
232
- /**
233
- * Write-then-rename: hot-regen rewrites these files while a live `wrangler dev`
234
- * watches them — a plain `writeFile` can be read truncated mid-write.
235
- */
236
- async function writeFileAtomic(path: string, content: string): Promise<void> {
237
- const tmp = `${path}.tmp`
238
- await writeFile(tmp, content)
239
- await rename(tmp, path)
240
- }
241
-
242
200
  /**
243
- * Poll the deployed URL until the worker answers (anything but the Cloudflare
244
- * 1042 "no script on this host" 404), or give up after `timeoutMs`. Resolves
245
- * silently on the fast path; logs only when propagation actually makes us wait.
201
+ * Poll until the deployed Worker health endpoint returns exactly 200.
246
202
  */
247
203
  async function waitUntilLive(
248
204
  url: string,
249
205
  onLog: (line: string) => void,
206
+ expectedDeploymentVersion?: string,
250
207
  timeoutMs = 90_000,
251
208
  ): Promise<void> {
252
209
  const probe = new URL('/health', url)
210
+ const metadata = new URL('/meta', url)
253
211
  const deadline = Date.now() + timeoutMs
254
212
  let waited = false
213
+ let lastStatus: number | undefined
214
+ let lastDeploymentVersion: string | undefined
255
215
  for (;;) {
256
216
  try {
257
217
  const res = await fetch(probe, { redirect: 'manual' })
258
- if (res.status !== 404) return
218
+ lastStatus = res.status
219
+ await res.body?.cancel()
220
+ if (res.status === 200) {
221
+ if (expectedDeploymentVersion === undefined) return
222
+ const meta = await fetch(metadata, { redirect: 'manual' })
223
+ if (meta.status === 200) {
224
+ const body = (await meta.json()) as { deploymentVersion?: unknown }
225
+ lastDeploymentVersion =
226
+ typeof body.deploymentVersion === 'string' ? body.deploymentVersion : undefined
227
+ if (lastDeploymentVersion === expectedDeploymentVersion) return
228
+ } else {
229
+ await meta.body?.cancel()
230
+ }
231
+ }
259
232
  } catch {
260
233
  // Network hiccup — treat like "not live yet" and keep polling.
261
234
  }
262
235
  if (Date.now() >= deadline) {
263
- onLog(`URL not reachable yet after ${Math.round(timeoutMs / 1000)}s — it may need a moment.`)
264
- return
236
+ throw new Error(
237
+ `deployed Worker health did not reach HTTP 200 within ${Math.round(timeoutMs / 1000)}s` +
238
+ (lastStatus === undefined ? '' : ` (last status ${lastStatus})`) +
239
+ (expectedDeploymentVersion === undefined
240
+ ? ''
241
+ : `; deployment version ${JSON.stringify(lastDeploymentVersion)} ` +
242
+ `did not reach ${JSON.stringify(expectedDeploymentVersion)}`),
243
+ )
265
244
  }
266
245
  if (!waited) {
267
246
  waited = true
@@ -270,52 +249,3 @@ async function waitUntilLive(
270
249
  await new Promise((r) => setTimeout(r, 3000))
271
250
  }
272
251
  }
273
-
274
- const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]*$/
275
-
276
- export function workerName(params: CloudflareParams, origin: string): string {
277
- // An explicit name is the deployed worker's identity (and the SELF service
278
- // target) — validate and reject an invalid one rather than silently rewriting
279
- // it, so the dev fixes it instead of deploying under a surprising name.
280
- if (params.workerName !== undefined) {
281
- if (!WORKER_NAME_RE.test(params.workerName) || params.workerName.length > 63) {
282
- throw new Error(
283
- `cloudflare adapter: invalid \`workerName\` "${params.workerName}". A Cloudflare worker ` +
284
- 'name must be lowercase, start with a letter or digit, contain only letters, digits and ' +
285
- 'dashes, and be at most 63 chars.',
286
- )
287
- }
288
- return params.workerName
289
- }
290
- // Derive from the origin: lowercase, collapse non-alphanumerics to a dash, then
291
- // SLICE before trimming dashes so a cut landing mid-dash can't leave a trailing
292
- // "-" (which wrangler rejects).
293
- return origin
294
- .toLowerCase()
295
- .replace(/[^a-z0-9]+/g, '-')
296
- .slice(0, 54)
297
- .replace(/^-+|-+$/g, '')
298
- }
299
-
300
- /**
301
- * Resolve the instance-router config. ON by default (targets `admin-router`) so
302
- * every adapter-deployed worker can reach other instances' hosts internally and
303
- * verify cross-instance credentials without a 522; `false` disables it. The
304
- * returned shape carries every value the worker- and wrangler-codegen need.
305
- */
306
- export function resolveRouter(
307
- ir: CloudflareParams['router'],
308
- ): { binding: string; service: string; hostSuffix: string; minLabels: number } | undefined {
309
- if (ir === false) return undefined
310
- const cfg = ir ?? {}
311
- return {
312
- binding: cfg.binding ?? 'ROUTER',
313
- service: cfg.service ?? 'admin-router',
314
- hostSuffix: cfg.hostSuffix ?? '.astrale.ai',
315
- minLabels: cfg.minLabels ?? 4,
316
- }
317
- }
318
-
319
- export function logTo(): (line: string) => void {
320
- return (line: string) => process.stderr.write(`\x1b[2m ${line}\x1b[0m\n`)
321
- }
package/src/index.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  * import { cloudflare } from '@astrale-os/adapter-cloudflare'
6
6
  *
7
7
  * adapter: cloudflare({
8
- * dev: { client: { dir: 'client' }, secrets: '.env.dev' },
9
- * prod: { client: { dir: 'client' }, route: 'crm.acme.dev', secrets: '.env.prod' },
8
+ * dev: { secrets: '.env.dev' },
9
+ * prod: { route: 'crm.acme.dev', secrets: '.env.prod' },
10
10
  * })
11
11
  *
12
12
  * `dev` runs `wrangler dev` locally; an env with no `route` ships to
@@ -17,5 +17,5 @@
17
17
  * `/build` toolkit and ships it through the Astrale platform.
18
18
  */
19
19
 
20
- export { cloudflare } from './cloudflare'
21
- export type { CloudflareParams } from './params'
20
+ export { cloudflare } from './cloudflare.js'
21
+ export type { CloudflareParams } from './params.js'
package/src/params.ts CHANGED
@@ -1,5 +1,3 @@
1
- import type { ClientConfig } from './client'
2
-
3
1
  /**
4
2
  * `CloudflareParams` — the provider-typed config for the Cloudflare adapter.
5
3
  *
@@ -13,18 +11,16 @@ import type { ClientConfig } from './client'
13
11
  * • remote (custom) — `deploy()` with `route`; URL = https://<route>
14
12
  */
15
13
  export interface CloudflareParams {
16
- /** Frontend assets to build and serve for this env's mounted `/ui` views. */
17
- client?: ClientConfig
18
14
  /**
19
15
  * Custom route / hostname for a routed deploy, e.g. `'crm.acme.dev'`. Omit to
20
16
  * deploy to `*.workers.dev`. Ignored by local `wrangler dev` (URL is
21
17
  * localhost) but still used as the placeholder base domain there.
22
18
  */
23
- route?: string
19
+ readonly route?: string
24
20
  /** Gitignored secrets file (whole contents = secrets), e.g. `'.env.dev'`. */
25
- secrets?: string
21
+ readonly secrets?: string
26
22
  /** Local dev port for `wrangler dev`. Default 8787. */
27
- port?: number
23
+ readonly port?: number
28
24
  /**
29
25
  * Public URL the dev server is reached through (tunnel / sandbox preview /
30
26
  * reverse proxy), e.g. `'https://my-box.preview.dev'`. Sets BOTH effects an
@@ -33,13 +29,13 @@ export interface CloudflareParams {
33
29
  * identity (`iss`) doesn't drift to the proxy's internal hostname.
34
30
  * Usually injected by `pnpm dev --host <url>` rather than written here.
35
31
  */
36
- host?: string
32
+ readonly host?: string
37
33
  /** Override the Worker name. Default: derived from the domain origin. */
38
- workerName?: string
34
+ readonly workerName?: string
39
35
  /** Run `wrangler dev --remote` (edge isolate) instead of local workerd. */
40
- remote?: boolean
36
+ readonly remote?: boolean
41
37
  /** Extra plain (non-secret) vars to inject as Worker vars. */
42
- vars?: Record<string, string>
38
+ readonly vars?: Record<string, string>
43
39
  /**
44
40
  * Escape hatch: raw wrangler config deep-merged over the generated base. Use
45
41
  * it to declare extra bindings the adapter has no typed field for — KV, R2,
@@ -58,7 +54,7 @@ export interface CloudflareParams {
58
54
  * the reserved binding names `SELF` and `ASSETS`. Durable Objects need extra
59
55
  * worker-entry codegen and aren't supported through this field yet.
60
56
  */
61
- wrangler?: Record<string, unknown>
57
+ readonly wrangler?: Record<string, unknown>
62
58
  /**
63
59
  * Route subrequests to platform INSTANCE hostnames through a service binding
64
60
  * instead of the public edge. A Cloudflare Worker can't fetch a same-zone
@@ -81,16 +77,16 @@ export interface CloudflareParams {
81
77
  * router: false // opt out
82
78
  * router: { service: 'my-router' } // override target
83
79
  */
84
- router?:
80
+ readonly router?:
85
81
  | false
86
82
  | {
87
83
  /** Router Worker the binding targets. Default `'admin-router'`. */
88
- service?: string
84
+ readonly service?: string
89
85
  /** Service-binding name the worker reads. Default `'ROUTER'`. */
90
- binding?: string
86
+ readonly binding?: string
91
87
  /** Host suffix that marks an instance host. Default `'.astrale.ai'`. */
92
- hostSuffix?: string
88
+ readonly hostSuffix?: string
93
89
  /** Min dot-separated label count for an instance host. Default `4` (`<slug>.<region>.astrale.ai`). */
94
- minLabels?: number
90
+ readonly minLabels?: number
95
91
  }
96
92
  }