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

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 (134) hide show
  1. package/README.md +11 -0
  2. package/dist/build/codegen/identity.d.ts.map +1 -0
  3. package/dist/build/codegen/identity.js.map +1 -0
  4. package/dist/build/codegen/merge.d.ts.map +1 -0
  5. package/dist/build/codegen/merge.js.map +1 -0
  6. package/dist/build/codegen/worker.d.ts +12 -0
  7. package/dist/build/codegen/worker.d.ts.map +1 -0
  8. package/dist/build/codegen/worker.js +40 -0
  9. package/dist/build/codegen/worker.js.map +1 -0
  10. package/dist/{codegen → build/codegen}/wrangler.d.ts +6 -3
  11. package/dist/build/codegen/wrangler.d.ts.map +1 -0
  12. package/dist/{codegen → build/codegen}/wrangler.js +29 -10
  13. package/dist/build/codegen/wrangler.js.map +1 -0
  14. package/dist/build/frontend.d.ts +26 -0
  15. package/dist/build/frontend.d.ts.map +1 -0
  16. package/dist/build/frontend.js +263 -0
  17. package/dist/build/frontend.js.map +1 -0
  18. package/dist/{build.js → build/index.d.ts} +5 -4
  19. package/dist/build/index.d.ts.map +1 -0
  20. package/dist/{build.d.ts → build/index.js} +5 -5
  21. package/dist/build/index.js.map +1 -0
  22. package/dist/{parse-output.d.ts → build/parse-output.d.ts} +2 -0
  23. package/dist/build/parse-output.d.ts.map +1 -0
  24. package/dist/{parse-output.js → build/parse-output.js} +5 -0
  25. package/dist/build/parse-output.js.map +1 -0
  26. package/dist/build/prepare.d.ts +18 -0
  27. package/dist/build/prepare.d.ts.map +1 -0
  28. package/dist/build/prepare.js +117 -0
  29. package/dist/build/prepare.js.map +1 -0
  30. package/dist/build/worker.d.ts +15 -0
  31. package/dist/build/worker.d.ts.map +1 -0
  32. package/dist/build/worker.js +21 -0
  33. package/dist/build/worker.js.map +1 -0
  34. package/dist/{wrangler-cli.d.ts → build/wrangler-cli.d.ts} +10 -0
  35. package/dist/build/wrangler-cli.d.ts.map +1 -0
  36. package/dist/{wrangler-cli.js → build/wrangler-cli.js} +38 -6
  37. package/dist/build/wrangler-cli.js.map +1 -0
  38. package/dist/cloudflare.d.ts +21 -24
  39. package/dist/cloudflare.d.ts.map +1 -1
  40. package/dist/cloudflare.js +125 -191
  41. package/dist/cloudflare.js.map +1 -1
  42. package/dist/index.d.ts +4 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +3 -3
  45. package/dist/index.js.map +1 -1
  46. package/dist/params.d.ts +13 -16
  47. package/dist/params.d.ts.map +1 -1
  48. package/dist/worker/fetch.d.ts +12 -0
  49. package/dist/worker/fetch.d.ts.map +1 -0
  50. package/dist/worker/fetch.js +32 -0
  51. package/dist/worker/fetch.js.map +1 -0
  52. package/dist/worker/index.d.ts +61 -0
  53. package/dist/worker/index.d.ts.map +1 -0
  54. package/dist/worker/index.js +212 -0
  55. package/dist/worker/index.js.map +1 -0
  56. package/package.json +13 -5
  57. package/src/build/codegen/worker.ts +53 -0
  58. package/src/{codegen → build/codegen}/wrangler.ts +34 -12
  59. package/src/build/frontend.ts +324 -0
  60. package/src/{build.ts → build/index.ts} +17 -4
  61. package/src/{parse-output.ts → build/parse-output.ts} +6 -0
  62. package/src/build/prepare.ts +161 -0
  63. package/src/build/worker.ts +37 -0
  64. package/src/{wrangler-cli.ts → build/wrangler-cli.ts} +56 -8
  65. package/src/cloudflare.ts +153 -223
  66. package/src/index.ts +4 -4
  67. package/src/params.ts +13 -17
  68. package/src/worker/fetch.ts +46 -0
  69. package/src/worker/index.ts +325 -0
  70. package/src/worker/tsconfig.json +10 -0
  71. package/template/astrale.config.ts +7 -7
  72. package/dist/assets-pack.d.ts +0 -8
  73. package/dist/assets-pack.d.ts.map +0 -1
  74. package/dist/assets-pack.js +0 -30
  75. package/dist/assets-pack.js.map +0 -1
  76. package/dist/build.d.ts.map +0 -1
  77. package/dist/build.js.map +0 -1
  78. package/dist/client.d.ts +0 -17
  79. package/dist/client.d.ts.map +0 -1
  80. package/dist/client.js +0 -59
  81. package/dist/client.js.map +0 -1
  82. package/dist/codegen/identity.d.ts.map +0 -1
  83. package/dist/codegen/identity.js.map +0 -1
  84. package/dist/codegen/merge.d.ts.map +0 -1
  85. package/dist/codegen/merge.js.map +0 -1
  86. package/dist/codegen/worker.d.ts +0 -51
  87. package/dist/codegen/worker.d.ts.map +0 -1
  88. package/dist/codegen/worker.js +0 -120
  89. package/dist/codegen/worker.js.map +0 -1
  90. package/dist/codegen/wrangler.d.ts.map +0 -1
  91. package/dist/codegen/wrangler.js.map +0 -1
  92. package/dist/parse-output.d.ts.map +0 -1
  93. package/dist/parse-output.js.map +0 -1
  94. package/dist/wrangler-cli.d.ts.map +0 -1
  95. package/dist/wrangler-cli.js.map +0 -1
  96. package/src/assets-pack.ts +0 -32
  97. package/src/client.ts +0 -89
  98. package/src/codegen/worker.ts +0 -136
  99. package/template/.env.example +0 -9
  100. package/template/.gitignore.template +0 -16
  101. package/template/CLAUDE.md +0 -13
  102. package/template/README.md +0 -47
  103. package/template/client/.gitignore.template +0 -4
  104. package/template/client/__tests__/app.test.tsx +0 -15
  105. package/template/client/components.json +0 -25
  106. package/template/client/index.html +0 -12
  107. package/template/client/package.json +0 -35
  108. package/template/client/src/app.tsx +0 -52
  109. package/template/client/src/components/ui/cn.ts +0 -6
  110. package/template/client/src/main.tsx +0 -9
  111. package/template/client/src/styles.css +0 -78
  112. package/template/client/tsconfig.json +0 -26
  113. package/template/client/vite.config.ts +0 -22
  114. package/template/client/vitest.config.ts +0 -13
  115. package/template/core/README.md +0 -7
  116. package/template/deps.ts +0 -9
  117. package/template/domain.ts +0 -11
  118. package/template/env.ts +0 -8
  119. package/template/icons.ts +0 -4
  120. package/template/integrations/README.md +0 -7
  121. package/template/oxlint.config.ts +0 -1
  122. package/template/package.json +0 -58
  123. package/template/pnpm-workspace.yaml +0 -31
  124. package/template/runtime/index.ts +0 -8
  125. package/template/schema/index.ts +0 -10
  126. package/template/simulation/README.md +0 -12
  127. package/template/tsconfig.json +0 -28
  128. package/template/vitest.config.ts +0 -7
  129. /package/dist/{codegen → build/codegen}/identity.d.ts +0 -0
  130. /package/dist/{codegen → build/codegen}/identity.js +0 -0
  131. /package/dist/{codegen → build/codegen}/merge.d.ts +0 -0
  132. /package/dist/{codegen → build/codegen}/merge.js +0 -0
  133. /package/src/{codegen → build/codegen}/identity.ts +0 -0
  134. /package/src/{codegen → build/codegen}/merge.ts +0 -0
@@ -14,9 +14,9 @@ import { existsSync, readdirSync } from 'node:fs'
14
14
  import { mkdtemp, rm, writeFile } from 'node:fs/promises'
15
15
  import { createServer } from 'node:net'
16
16
  import { tmpdir } from 'node:os'
17
- import { join } from 'node:path'
17
+ import { dirname, join, resolve } from 'node:path'
18
18
 
19
- import { parseDeployUrl, parseDevReadyUrl } from './parse-output'
19
+ import { parseDeployUrl, parseDeployVersionId, parseDevReadyUrl } from './parse-output.js'
20
20
 
21
21
  function wranglerCommand(projectDir: string): { cmd: string; prefix: string[] } {
22
22
  const local = join(projectDir, 'node_modules', '.bin', 'wrangler')
@@ -24,6 +24,16 @@ function wranglerCommand(projectDir: string): { cmd: string; prefix: string[] }
24
24
  return { cmd: 'npx', prefix: ['--yes', 'wrangler'] }
25
25
  }
26
26
 
27
+ /** Keep Wrangler CLI diagnostic logs inside the generated project state. */
28
+ export function wranglerProcessEnv(projectDir: string): NodeJS.ProcessEnv {
29
+ return {
30
+ ...process.env,
31
+ WRANGLER_SEND_METRICS: 'false',
32
+ WRANGLER_LOG_PATH:
33
+ process.env.WRANGLER_LOG_PATH || resolve(projectDir, '.astrale', 'logs', 'wrangler'),
34
+ }
35
+ }
36
+
27
37
  export interface DevHandle {
28
38
  url: string
29
39
  stop(): Promise<void>
@@ -69,6 +79,7 @@ export async function runWranglerDev(args: {
69
79
  onReload: () => void
70
80
  onLog?: (line: string) => void
71
81
  }): Promise<DevHandle> {
82
+ await runWranglerTypes(args)
72
83
  const { cmd, prefix } = wranglerCommand(args.projectDir)
73
84
  const bindHost = args.ip ?? '127.0.0.1'
74
85
  if (args.autoPickPort && !(await portFree(args.port, bindHost))) {
@@ -93,7 +104,7 @@ export async function runWranglerDev(args: {
93
104
  const child = spawn(cmd, wranglerArgs, {
94
105
  cwd: args.projectDir,
95
106
  stdio: ['ignore', 'pipe', 'pipe'],
96
- env: { ...process.env, WRANGLER_SEND_METRICS: 'false' },
107
+ env: wranglerProcessEnv(args.projectDir),
97
108
  })
98
109
 
99
110
  const fallbackUrl = `http://localhost:${args.port}`
@@ -103,10 +114,12 @@ export async function runWranglerDev(args: {
103
114
  // never answers) — the classic silent hang. Track build errors so the
104
115
  // safety net can fail LOUD instead of resolving a dead URL.
105
116
  let buildErrorTail = ''
117
+ let outputTail = ''
106
118
 
107
119
  const url = await new Promise<string>((resolve, reject) => {
108
120
  const onData = (buf: Buffer) => {
109
121
  const text = buf.toString()
122
+ outputTail = (outputTail + text).slice(-3000)
110
123
  for (const line of text.split('\n')) {
111
124
  if (line.trim()) args.onLog?.(line)
112
125
  }
@@ -128,7 +141,11 @@ export async function runWranglerDev(args: {
128
141
  child.on('exit', (code) => {
129
142
  if (!resolved) {
130
143
  resolved = true
131
- reject(new Error(`wrangler dev exited before becoming ready (code ${code ?? 'null'}).`))
144
+ reject(
145
+ new Error(
146
+ `wrangler dev exited before becoming ready (code ${code ?? 'null'}).\n${outputTail}`,
147
+ ),
148
+ )
132
149
  }
133
150
  })
134
151
  child.on('error', (err) => {
@@ -174,7 +191,8 @@ export interface DeployArgs {
174
191
 
175
192
  export async function runWranglerDeploy(
176
193
  args: DeployArgs & { projectDir: string },
177
- ): Promise<{ url: string; raw: string }> {
194
+ ): Promise<{ url: string; raw: string; deploymentVersion?: string }> {
195
+ await runWranglerTypes(args)
178
196
  const { cmd, prefix } = wranglerCommand(args.projectDir)
179
197
  const deploy = (configPath: string) =>
180
198
  runCapture(cmd, [...prefix, 'deploy', '--config', configPath], args.projectDir, args.onLog)
@@ -192,7 +210,7 @@ export async function runWranglerDeploy(
192
210
  export async function deployWithSelfFallback(
193
211
  deploy: (configPath: string) => Promise<{ code: number; out: string }>,
194
212
  args: DeployArgs,
195
- ): Promise<{ url: string; raw: string }> {
213
+ ): Promise<{ url: string; raw: string; deploymentVersion?: string }> {
196
214
  let { code, out } = await deploy(args.configPath)
197
215
 
198
216
  if (code !== 0 && args.fallbackConfigPath && isSelfBindingMissing(out, args.workerName)) {
@@ -215,7 +233,8 @@ export async function deployWithSelfFallback(
215
233
  if (!url) {
216
234
  throw new Error(`could not determine deployed URL from wrangler output:\n${out.slice(-2000)}`)
217
235
  }
218
- return { url, raw: out }
236
+ const deploymentVersion = parseDeployVersionId(out)
237
+ return { url, raw: out, ...(deploymentVersion === undefined ? {} : { deploymentVersion }) }
219
238
  }
220
239
 
221
240
  /**
@@ -271,7 +290,7 @@ function runCapture(
271
290
  const child = spawn(cmd, args, {
272
291
  cwd,
273
292
  stdio: ['ignore', 'pipe', 'pipe'],
274
- env: { ...process.env, WRANGLER_SEND_METRICS: 'false' },
293
+ env: wranglerProcessEnv(cwd),
275
294
  })
276
295
  let out = ''
277
296
  const onData = (buf: Buffer) => {
@@ -309,6 +328,7 @@ export async function runWranglerBundle(args: {
309
328
  outDir: string
310
329
  onLog?: (line: string) => void
311
330
  }): Promise<string> {
331
+ await runWranglerTypes(args)
312
332
  const { cmd, prefix } = wranglerCommand(args.projectDir)
313
333
  const { code, out } = await runCapture(
314
334
  cmd,
@@ -322,3 +342,31 @@ export async function runWranglerBundle(args: {
322
342
  const preferred = js.find((f) => f.startsWith('worker')) ?? js[0]!
323
343
  return join(args.outDir, preferred)
324
344
  }
345
+
346
+ /** Generate the exact binding and runtime types selected by the generated config. */
347
+ export async function runWranglerTypes(args: {
348
+ readonly projectDir: string
349
+ readonly configPath: string
350
+ readonly onLog?: (line: string) => void
351
+ }): Promise<string> {
352
+ const { cmd, prefix } = wranglerCommand(args.projectDir)
353
+ const outputPath = join(dirname(args.configPath), 'worker-configuration.d.ts')
354
+ const { code, out } = await runCapture(
355
+ cmd,
356
+ [
357
+ ...prefix,
358
+ 'types',
359
+ outputPath,
360
+ '--env-interface',
361
+ 'CloudflareEnv',
362
+ '--config',
363
+ args.configPath,
364
+ ],
365
+ args.projectDir,
366
+ args.onLog,
367
+ )
368
+ if (code !== 0) {
369
+ throw new Error(`wrangler types failed (code ${code}):\n${out.slice(-2000)}`)
370
+ }
371
+ return outputPath
372
+ }
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'