@astrale-os/adapter-cloudflare 0.5.0-beta.13 → 0.5.0-beta.131

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 (196) hide show
  1. package/CHANGELOG.md +1515 -0
  2. package/README.md +46 -2
  3. package/dist/adapter.d.ts +14 -0
  4. package/dist/adapter.js +82 -0
  5. package/dist/build/artifact.d.ts +7 -0
  6. package/dist/build/artifact.js +165 -0
  7. package/dist/build/bundle/admit.d.ts +1 -0
  8. package/dist/build/bundle/admit.js +9 -0
  9. package/dist/build/closure.d.ts +2 -0
  10. package/dist/build/closure.js +22 -0
  11. package/dist/build/codegen/execution.d.ts +3 -0
  12. package/dist/build/codegen/execution.js +2 -0
  13. package/dist/build/codegen/merge.d.ts +0 -1
  14. package/dist/build/codegen/merge.js +0 -1
  15. package/dist/build/codegen/wrangler.d.ts +12 -3
  16. package/dist/build/codegen/wrangler.js +26 -18
  17. package/dist/build/configuration/addressing.d.ts +2 -0
  18. package/dist/build/configuration/addressing.js +11 -0
  19. package/dist/build/configuration/invocation-timeout.d.ts +2 -0
  20. package/dist/build/configuration/invocation-timeout.js +9 -0
  21. package/dist/build/configuration/request-limit.d.ts +2 -0
  22. package/dist/build/configuration/request-limit.js +9 -0
  23. package/dist/build/configuration/router.d.ts +7 -0
  24. package/dist/build/configuration/router.js +11 -0
  25. package/dist/build/configuration/worker-name.d.ts +2 -0
  26. package/dist/build/configuration/worker-name.js +18 -0
  27. package/dist/build/configuration.d.ts +5 -0
  28. package/dist/build/configuration.js +5 -0
  29. package/dist/build/direct.d.ts +4 -0
  30. package/dist/build/direct.js +90 -0
  31. package/dist/build/frontend/files.d.ts +9 -0
  32. package/dist/build/frontend/files.js +33 -0
  33. package/dist/build/frontend/readiness.d.ts +10 -0
  34. package/dist/build/frontend/readiness.js +21 -0
  35. package/dist/build/frontend/revision.d.ts +5 -0
  36. package/dist/build/frontend/revision.js +14 -0
  37. package/dist/build/frontend/vite.d.ts +8 -0
  38. package/dist/build/frontend/vite.js +32 -0
  39. package/dist/build/frontend/vite.process.d.ts +7 -0
  40. package/dist/build/frontend/vite.process.js +65 -0
  41. package/dist/build/frontend/vite.runner.d.ts +1 -0
  42. package/dist/build/frontend/vite.runner.js +120 -0
  43. package/dist/build/index.d.ts +9 -16
  44. package/dist/build/index.js +8 -16
  45. package/dist/build/materialize.d.ts +4 -0
  46. package/dist/build/materialize.js +10 -0
  47. package/dist/build/metafile.d.ts +5 -0
  48. package/dist/build/metafile.js +14 -0
  49. package/dist/build/qualification.d.ts +5 -0
  50. package/dist/build/qualification.js +7 -0
  51. package/dist/build/roots.d.ts +2 -0
  52. package/dist/build/roots.js +34 -0
  53. package/dist/build/worker/artifact.d.ts +40 -0
  54. package/dist/build/worker/artifact.js +1 -0
  55. package/dist/build/worker/artifact.load.d.ts +3 -0
  56. package/dist/build/worker/artifact.load.js +308 -0
  57. package/dist/build/worker/artifact.manifest.d.ts +48 -0
  58. package/dist/build/worker/artifact.manifest.js +87 -0
  59. package/dist/build/worker/artifact.paths.d.ts +4 -0
  60. package/dist/build/worker/artifact.paths.js +25 -0
  61. package/dist/build/worker/codegen/entry.d.ts +2 -0
  62. package/dist/build/worker/codegen/entry.js +28 -0
  63. package/dist/build/worker/codegen/service.d.ts +19 -0
  64. package/dist/build/worker/codegen/service.js +53 -0
  65. package/dist/build/worker/execution.prepare.d.ts +19 -0
  66. package/dist/build/worker/execution.prepare.js +115 -0
  67. package/dist/build/worker/index.d.ts +2 -0
  68. package/dist/build/worker/index.js +1 -0
  69. package/dist/build/worker/modules.d.ts +11 -0
  70. package/dist/build/worker/modules.js +26 -0
  71. package/dist/build/worker/runtime.d.ts +10 -0
  72. package/dist/build/worker/runtime.js +35 -0
  73. package/dist/build/worker/specifier.d.ts +1 -0
  74. package/dist/build/worker/specifier.js +5 -0
  75. package/dist/cloudflare.d.ts +1 -45
  76. package/dist/cloudflare.js +1 -202
  77. package/dist/deploy/deploy.d.ts +17 -0
  78. package/dist/deploy/deploy.js +221 -0
  79. package/dist/deploy/index.d.ts +1 -0
  80. package/dist/deploy/index.js +1 -0
  81. package/dist/deploy/output.d.ts +2 -0
  82. package/dist/deploy/output.js +10 -0
  83. package/dist/deploy/readiness.d.ts +5 -0
  84. package/dist/deploy/readiness.js +18 -0
  85. package/dist/identity/binding.d.ts +2 -0
  86. package/dist/identity/binding.js +2 -0
  87. package/dist/identity/index.d.ts +3 -0
  88. package/dist/identity/index.js +3 -0
  89. package/dist/identity/material.d.ts +7 -0
  90. package/dist/identity/material.js +42 -0
  91. package/dist/identity/secret.d.ts +3 -0
  92. package/dist/identity/secret.js +7 -0
  93. package/dist/identity/source.d.ts +26 -0
  94. package/dist/identity/source.js +127 -0
  95. package/dist/index.d.ts +10 -9
  96. package/dist/index.js +10 -9
  97. package/dist/params.d.ts +40 -36
  98. package/dist/params.js +0 -1
  99. package/dist/release/deployment.d.ts +44 -0
  100. package/dist/release/deployment.js +141 -0
  101. package/dist/release/index.d.ts +2 -0
  102. package/dist/release/index.js +2 -0
  103. package/dist/release/staged-worker-name.d.ts +3 -0
  104. package/dist/release/staged-worker-name.js +10 -0
  105. package/dist/worker/entry.d.ts +48 -0
  106. package/dist/worker/entry.js +157 -0
  107. package/dist/worker/fetch.d.ts +0 -1
  108. package/dist/worker/fetch.js +0 -1
  109. package/dist/worker/frontend.d.ts +4 -0
  110. package/dist/worker/frontend.js +46 -0
  111. package/dist/worker/identity.d.ts +7 -0
  112. package/dist/worker/identity.js +23 -0
  113. package/dist/worker/index.d.ts +2 -61
  114. package/dist/worker/index.js +2 -218
  115. package/dist/wrangler/bundle.d.ts +10 -0
  116. package/dist/wrangler/bundle.js +28 -0
  117. package/dist/wrangler/deploy.d.ts +24 -0
  118. package/dist/wrangler/deploy.js +44 -0
  119. package/dist/wrangler/index.d.ts +5 -0
  120. package/dist/wrangler/index.js +5 -0
  121. package/dist/wrangler/process.d.ts +15 -0
  122. package/dist/wrangler/process.js +102 -0
  123. package/dist/wrangler/rollout.d.ts +27 -0
  124. package/dist/wrangler/rollout.js +75 -0
  125. package/dist/wrangler/secrets.d.ts +2 -0
  126. package/dist/wrangler/secrets.js +24 -0
  127. package/dist/wrangler/status.d.ts +18 -0
  128. package/dist/wrangler/status.js +75 -0
  129. package/dist/wrangler/triggers.d.ts +5 -0
  130. package/dist/wrangler/triggers.js +12 -0
  131. package/dist/wrangler/type-generation.d.ts +6 -0
  132. package/dist/wrangler/type-generation.js +19 -0
  133. package/dist/wrangler/versions.d.ts +13 -0
  134. package/dist/wrangler/versions.js +38 -0
  135. package/package.json +34 -19
  136. package/template/astrale.config.ts +30 -31
  137. package/dist/build/codegen/identity.d.ts +0 -10
  138. package/dist/build/codegen/identity.d.ts.map +0 -1
  139. package/dist/build/codegen/identity.js +0 -32
  140. package/dist/build/codegen/identity.js.map +0 -1
  141. package/dist/build/codegen/merge.d.ts.map +0 -1
  142. package/dist/build/codegen/merge.js.map +0 -1
  143. package/dist/build/codegen/worker.d.ts +0 -12
  144. package/dist/build/codegen/worker.d.ts.map +0 -1
  145. package/dist/build/codegen/worker.js +0 -40
  146. package/dist/build/codegen/worker.js.map +0 -1
  147. package/dist/build/codegen/wrangler.d.ts.map +0 -1
  148. package/dist/build/codegen/wrangler.js.map +0 -1
  149. package/dist/build/frontend.d.ts +0 -27
  150. package/dist/build/frontend.d.ts.map +0 -1
  151. package/dist/build/frontend.js +0 -252
  152. package/dist/build/frontend.js.map +0 -1
  153. package/dist/build/index.d.ts.map +0 -1
  154. package/dist/build/index.js.map +0 -1
  155. package/dist/build/parse-output.d.ts +0 -27
  156. package/dist/build/parse-output.d.ts.map +0 -1
  157. package/dist/build/parse-output.js +0 -58
  158. package/dist/build/parse-output.js.map +0 -1
  159. package/dist/build/prepare.d.ts +0 -18
  160. package/dist/build/prepare.d.ts.map +0 -1
  161. package/dist/build/prepare.js +0 -138
  162. package/dist/build/prepare.js.map +0 -1
  163. package/dist/build/worker.d.ts +0 -15
  164. package/dist/build/worker.d.ts.map +0 -1
  165. package/dist/build/worker.js +0 -21
  166. package/dist/build/worker.js.map +0 -1
  167. package/dist/build/wrangler-cli.d.ts +0 -88
  168. package/dist/build/wrangler-cli.d.ts.map +0 -1
  169. package/dist/build/wrangler-cli.js +0 -283
  170. package/dist/build/wrangler-cli.js.map +0 -1
  171. package/dist/cloudflare.d.ts.map +0 -1
  172. package/dist/cloudflare.js.map +0 -1
  173. package/dist/index.d.ts.map +0 -1
  174. package/dist/index.js.map +0 -1
  175. package/dist/params.d.ts.map +0 -1
  176. package/dist/params.js.map +0 -1
  177. package/dist/worker/fetch.d.ts.map +0 -1
  178. package/dist/worker/fetch.js.map +0 -1
  179. package/dist/worker/index.d.ts.map +0 -1
  180. package/dist/worker/index.js.map +0 -1
  181. package/src/build/codegen/identity.ts +0 -40
  182. package/src/build/codegen/merge.ts +0 -92
  183. package/src/build/codegen/worker.ts +0 -53
  184. package/src/build/codegen/wrangler.ts +0 -128
  185. package/src/build/frontend.ts +0 -298
  186. package/src/build/index.ts +0 -29
  187. package/src/build/parse-output.ts +0 -60
  188. package/src/build/prepare.ts +0 -189
  189. package/src/build/worker.ts +0 -37
  190. package/src/build/wrangler-cli.ts +0 -372
  191. package/src/cloudflare.ts +0 -275
  192. package/src/index.ts +0 -21
  193. package/src/params.ts +0 -92
  194. package/src/worker/fetch.ts +0 -46
  195. package/src/worker/index.ts +0 -332
  196. package/src/worker/tsconfig.json +0 -10
@@ -1,189 +0,0 @@
1
- import type {
2
- BuildCtx,
3
- CompiledFrontendArtifact,
4
- DeployCtx,
5
- FrontendRoute,
6
- WatchCtx,
7
- } from '@astrale-os/sdk'
8
-
9
- import { mkdir, rename, writeFile } from 'node:fs/promises'
10
- import { join } from 'node:path'
11
-
12
- import type { CloudflareParams } from '../index.js'
13
-
14
- import { ensureIdentity } from './codegen/identity.js'
15
- import { generateWorkerEntry } from './codegen/worker.js'
16
- import { generateWranglerConfig } from './codegen/wrangler.js'
17
-
18
- const DEFAULT_PORT = 8787
19
- const DEPLOYMENT_VERSION_BINDING = 'ASTRALE_DEPLOYMENT_VERSION'
20
- const FRONTEND_DEV_BINDING = 'FRONTEND_DEV_URL'
21
- const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]*$/
22
-
23
- /** Prepare immutable generated Worker sources and Wrangler configuration. */
24
- export async function prepare(
25
- params: CloudflareParams,
26
- ctx: WatchCtx | DeployCtx | BuildCtx,
27
- mode: 'build' | 'dev' | 'deploy',
28
- frontend?: CompiledFrontendArtifact,
29
- frontendDevUrl?: string,
30
- ): Promise<{ configPath: string; fallbackConfigPath?: string; port: number; workerName: string }> {
31
- if (Object.hasOwn(params.vars ?? {}, DEPLOYMENT_VERSION_BINDING)) {
32
- throw new Error(
33
- `cloudflare adapter: \`${DEPLOYMENT_VERSION_BINDING}\` is adapter-managed and cannot be set in \`vars\`.`,
34
- )
35
- }
36
- if (Object.hasOwn(params.vars ?? {}, FRONTEND_DEV_BINDING)) {
37
- throw new Error(
38
- `cloudflare adapter: \`${FRONTEND_DEV_BINDING}\` is adapter-managed and cannot be set in \`vars\`.`,
39
- )
40
- }
41
- if (frontendDevUrl !== undefined && (mode !== 'dev' || !localFrontendDefinition(ctx))) {
42
- throw new TypeError(
43
- 'cloudflare adapter: frontendDevUrl requires a local Vite frontend in dev mode.',
44
- )
45
- }
46
- const astraleDir = join(ctx.projectDir, '.astrale')
47
- await mkdir(astraleDir, { recursive: true })
48
- await ensureIdentity(astraleDir, ctx.definition.schema.origin)
49
-
50
- const name = workerName(params, ctx.definition.schema.origin)
51
- const router = resolveRouter(params.router)
52
- const localFrontend =
53
- ctx.definition.frontend !== undefined && ctx.definition.frontend.source.kind !== 'external'
54
- const developmentFrontend = mode === 'dev' && frontendDevUrl !== undefined
55
- if (
56
- !developmentFrontend &&
57
- localFrontend !== (frontend !== undefined && frontend.artifact.source.kind !== 'external')
58
- ) {
59
- throw new TypeError('cloudflare adapter: materialized frontend must match definition.frontend.')
60
- }
61
- if (developmentFrontend) await mkdir(join(astraleDir, 'frontend'), { recursive: true })
62
-
63
- await writeFileAtomic(
64
- join(astraleDir, 'worker.gen.ts'),
65
- generateWorkerEntry({
66
- origin: ctx.definition.schema.origin,
67
- ...(router
68
- ? {
69
- router: {
70
- binding: router.binding,
71
- hostSuffix: router.hostSuffix,
72
- minLabels: router.minLabels,
73
- },
74
- }
75
- : {}),
76
- }),
77
- )
78
-
79
- const workerUrl =
80
- mode === 'deploy' && params.route
81
- ? `https://${params.route}`
82
- : mode === 'dev' && params.host
83
- ? params.host
84
- : undefined
85
- const vars = {
86
- ...(workerUrl ? { WORKER_URL: workerUrl } : {}),
87
- ...(frontendDevUrl === undefined ? {} : { FRONTEND_DEV_URL: frontendDevUrl }),
88
- ...(mode === 'dev'
89
- ? { ...params.vars, ...('secrets' in ctx ? ctx.secrets : {}) }
90
- : { ...params.vars }),
91
- }
92
- const baseConfig = {
93
- workerName: name,
94
- ...(mode === 'deploy' && params.route ? { route: params.route } : {}),
95
- ...(localFrontend
96
- ? {
97
- frontend: {
98
- directory: './frontend',
99
- routes: localFrontendRoutes(ctx.definition),
100
- dev: frontendDevUrl !== undefined,
101
- },
102
- }
103
- : {}),
104
- vars,
105
- ...(router ? { router: { binding: router.binding, service: router.service } } : {}),
106
- ...(params.wrangler ? { wrangler: params.wrangler } : {}),
107
- }
108
-
109
- const configPath = join(astraleDir, 'wrangler.gen.jsonc')
110
- await writeFileAtomic(configPath, generateWranglerConfig({ ...baseConfig, selfBinding: true }))
111
-
112
- let fallbackConfigPath: string | undefined
113
- if (mode === 'deploy') {
114
- fallbackConfigPath = join(astraleDir, 'wrangler.no-self.gen.jsonc')
115
- await writeFileAtomic(
116
- fallbackConfigPath,
117
- generateWranglerConfig({ ...baseConfig, selfBinding: false }),
118
- )
119
- }
120
-
121
- return {
122
- configPath,
123
- ...(fallbackConfigPath ? { fallbackConfigPath } : {}),
124
- port: params.port ?? DEFAULT_PORT,
125
- workerName: name,
126
- }
127
- }
128
-
129
- function localFrontendRoutes(
130
- definition: BuildCtx['definition'] | DeployCtx['definition'] | WatchCtx['definition'],
131
- ): readonly string[] {
132
- const frontend = definition.frontend
133
- if (frontend === undefined || frontend.source.kind === 'external') return Object.freeze([])
134
- const routes = Object.values(frontend.routes as Readonly<Record<string, FrontendRoute>>)
135
- .flatMap((route) =>
136
- frontend.source.kind === 'vite' && frontend.source.spa && route.path !== '/'
137
- ? [route.path, `${route.path}/*`]
138
- : route.path === '/'
139
- ? ['/*']
140
- : [route.path],
141
- )
142
- .sort()
143
- return Object.freeze(routes)
144
- }
145
-
146
- function localFrontendDefinition(ctx: WatchCtx | DeployCtx | BuildCtx): boolean {
147
- return ctx.definition.frontend?.source.kind === 'vite'
148
- }
149
-
150
- async function writeFileAtomic(path: string, content: string): Promise<void> {
151
- const tmp = `${path}.tmp`
152
- await writeFile(tmp, content)
153
- await rename(tmp, path)
154
- }
155
-
156
- export function workerName(params: CloudflareParams, origin: string): string {
157
- if (params.workerName !== undefined) {
158
- if (!WORKER_NAME_RE.test(params.workerName) || params.workerName.length > 63) {
159
- throw new Error(
160
- `cloudflare adapter: invalid \`workerName\` "${params.workerName}". A Cloudflare worker ` +
161
- 'name must be lowercase, start with a letter or digit, contain only letters, digits and ' +
162
- 'dashes, and be at most 63 chars.',
163
- )
164
- }
165
- return params.workerName
166
- }
167
- return origin
168
- .toLowerCase()
169
- .replace(/[^a-z0-9]+/g, '-')
170
- .slice(0, 54)
171
- .replace(/^-+|-+$/g, '')
172
- }
173
-
174
- export function resolveRouter(
175
- ir: CloudflareParams['router'],
176
- ): { binding: string; service: string; hostSuffix: string; minLabels: number } | undefined {
177
- if (ir === false) return undefined
178
- const cfg = ir ?? {}
179
- return {
180
- binding: cfg.binding ?? 'ROUTER',
181
- service: cfg.service ?? 'admin-router',
182
- hostSuffix: cfg.hostSuffix ?? '.astrale.ai',
183
- minLabels: cfg.minLabels ?? 4,
184
- }
185
- }
186
-
187
- export function logTo(): (line: string) => void {
188
- return (line: string) => process.stderr.write(`\x1b[2m ${line}\x1b[0m\n`)
189
- }
@@ -1,37 +0,0 @@
1
- import type { BuildCtx, CompiledFrontendArtifact } from '@astrale-os/sdk'
2
-
3
- import { join } from 'node:path'
4
-
5
- import type { CloudflareParams } from '../params.js'
6
-
7
- import { buildFrontend } from './frontend.js'
8
- import { prepare } from './prepare.js'
9
- import { runWranglerBundle } from './wrangler-cli.js'
10
-
11
- export interface CloudflareWorkerBuild {
12
- readonly frontend: CompiledFrontendArtifact | undefined
13
- readonly configPath: string
14
- readonly bundlePath: string
15
- }
16
-
17
- /** One shared frontend/config/Worker qualification lifecycle for Cloudflare-based adapters. */
18
- export async function buildCloudflareWorker(input: {
19
- readonly params: CloudflareParams
20
- readonly context: BuildCtx
21
- readonly outDir?: string
22
- readonly onLog?: (line: string) => void
23
- }): Promise<CloudflareWorkerBuild> {
24
- const frontend = await buildFrontend({
25
- definition: input.context.definition,
26
- projectDir: input.context.projectDir,
27
- onLog: input.onLog,
28
- })
29
- const { configPath } = await prepare(input.params, input.context, 'build', frontend)
30
- const bundlePath = await runWranglerBundle({
31
- projectDir: input.context.projectDir,
32
- configPath,
33
- outDir: input.outDir ?? join(input.context.projectDir, '.astrale', 'dist-build'),
34
- onLog: input.onLog,
35
- })
36
- return Object.freeze({ frontend, configPath, bundlePath })
37
- }
@@ -1,372 +0,0 @@
1
- /**
2
- * Thin wrapper around the `wrangler` binary — the only place the adapter shells
3
- * out. `watch` runs `wrangler dev` (local workerd) and resolves once the worker
4
- * reports its local URL; `deploy` runs `wrangler deploy` and parses the
5
- * authoritative public URL from the output; secrets go through `wrangler secret
6
- * bulk`. The wrangler binary is resolved from the project's `node_modules/.bin`,
7
- * falling back to `npx wrangler`.
8
- */
9
-
10
- import type { ChildProcess } from 'node:child_process'
11
-
12
- import { spawn } from 'node:child_process'
13
- import { existsSync, readdirSync } from 'node:fs'
14
- import { mkdtemp, rm, writeFile } from 'node:fs/promises'
15
- import { createServer } from 'node:net'
16
- import { tmpdir } from 'node:os'
17
- import { dirname, join, resolve } from 'node:path'
18
-
19
- import { parseDeployUrl, parseDeployVersionId, parseDevReadyUrl } from './parse-output.js'
20
-
21
- function wranglerCommand(projectDir: string): { cmd: string; prefix: string[] } {
22
- const local = join(projectDir, 'node_modules', '.bin', 'wrangler')
23
- if (existsSync(local)) return { cmd: local, prefix: [] }
24
- return { cmd: 'npx', prefix: ['--yes', 'wrangler'] }
25
- }
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
-
37
- export interface DevHandle {
38
- url: string
39
- stop(): Promise<void>
40
- }
41
-
42
- /**
43
- * True when `port` is free on `host`. A taken port previously made `wrangler
44
- * dev` either die ("Address already in use") or — worse — a FIRST probe hit a
45
- * DIFFERENT project's dev server on the same port (observed in agent runs:
46
- * every reader of the docs converges on 8787/8899).
47
- */
48
- function portFree(port: number, host: string): Promise<boolean> {
49
- return new Promise((resolve) => {
50
- const srv = createServer()
51
- srv.once('error', () => resolve(false))
52
- srv.once('listening', () => srv.close(() => resolve(true)))
53
- srv.listen(port, host)
54
- })
55
- }
56
-
57
- /** An OS-assigned free port. */
58
- function ephemeralPort(host: string): Promise<number> {
59
- return new Promise((resolve, reject) => {
60
- const srv = createServer()
61
- srv.once('error', reject)
62
- srv.listen(0, host, () => {
63
- const addr = srv.address()
64
- const port = typeof addr === 'object' && addr ? addr.port : 0
65
- srv.close(() => resolve(port))
66
- })
67
- })
68
- }
69
-
70
- export async function runWranglerDev(args: {
71
- projectDir: string
72
- configPath: string
73
- port: number
74
- remote: boolean
75
- /** Bind address — '0.0.0.0' for tunneled/proxied dev (`--host`). Default loopback. */
76
- ip?: string
77
- /** When the port was NOT explicitly chosen, dodge a taken one automatically. */
78
- autoPickPort?: boolean
79
- onReload: () => void
80
- onLog?: (line: string) => void
81
- }): Promise<DevHandle> {
82
- await runWranglerTypes(args)
83
- const { cmd, prefix } = wranglerCommand(args.projectDir)
84
- const bindHost = args.ip ?? '127.0.0.1'
85
- if (args.autoPickPort && !(await portFree(args.port, bindHost))) {
86
- const picked = await ephemeralPort(bindHost)
87
- args.onLog?.(`port ${args.port} is taken — using ${picked} instead (pass --port to pin one)`)
88
- args = { ...args, port: picked }
89
- }
90
- const wranglerArgs = [
91
- ...prefix,
92
- 'dev',
93
- '--config',
94
- args.configPath,
95
- '--port',
96
- String(args.port),
97
- '--local-protocol',
98
- 'http',
99
- '--ip',
100
- args.ip ?? '127.0.0.1',
101
- ...(args.remote ? ['--remote'] : []),
102
- ]
103
-
104
- const child = spawn(cmd, wranglerArgs, {
105
- cwd: args.projectDir,
106
- stdio: ['ignore', 'pipe', 'pipe'],
107
- env: wranglerProcessEnv(args.projectDir),
108
- })
109
-
110
- const fallbackUrl = `http://localhost:${args.port}`
111
- // (args.port may have been re-picked above — everything below reads args.port)
112
- let resolved = false
113
- // Wrangler's watch mode STAYS ALIVE on a build failure (listener up, worker
114
- // never answers) — the classic silent hang. Track build errors so the
115
- // safety net can fail LOUD instead of resolving a dead URL.
116
- let buildErrorTail = ''
117
- let outputTail = ''
118
-
119
- const url = await new Promise<string>((resolve, reject) => {
120
- const onData = (buf: Buffer) => {
121
- const text = buf.toString()
122
- outputTail = (outputTail + text).slice(-3000)
123
- for (const line of text.split('\n')) {
124
- if (line.trim()) args.onLog?.(line)
125
- }
126
- if (/✘ \[ERROR\]|Build failed|Could not resolve "/.test(text)) {
127
- buildErrorTail = (buildErrorTail + text).slice(-2000)
128
- }
129
- if (!resolved) {
130
- const ready = parseDevReadyUrl(text, args.port)
131
- if (ready) {
132
- resolved = true
133
- resolve(ready)
134
- }
135
- } else if (/Reloading|Detected changes|Updated/i.test(text)) {
136
- args.onReload()
137
- }
138
- }
139
- child.stdout?.on('data', onData)
140
- child.stderr?.on('data', onData)
141
- child.on('exit', (code) => {
142
- if (!resolved) {
143
- resolved = true
144
- reject(
145
- new Error(
146
- `wrangler dev exited before becoming ready (code ${code ?? 'null'}).\n${outputTail}`,
147
- ),
148
- )
149
- }
150
- })
151
- child.on('error', (err) => {
152
- if (!resolved) {
153
- resolved = true
154
- reject(err)
155
- }
156
- })
157
- // Safety net: if we never matched a URL but the process is alive, assume the
158
- // conventional localhost URL after a grace period — UNLESS the build
159
- // failed, in which case a listener may be up but the worker will never
160
- // answer: fail loud with the build output instead of handing back a dead
161
- // URL.
162
- setTimeout(() => {
163
- if (!resolved && child.exitCode === null) {
164
- resolved = true
165
- if (buildErrorTail) {
166
- void stopChild(child)
167
- reject(
168
- new Error(
169
- `worker build failed — the dev server would hang silently.\n${buildErrorTail}`,
170
- ),
171
- )
172
- } else {
173
- resolve(fallbackUrl)
174
- }
175
- }
176
- }, 15_000).unref?.()
177
- })
178
-
179
- return { url, stop: () => stopChild(child) }
180
- }
181
-
182
- export interface DeployArgs {
183
- configPath: string
184
- /** SELF-less config for the first-deploy two-pass (see `deployWithSelfFallback`). */
185
- fallbackConfigPath?: string
186
- /** The worker's own name, to scope the self-binding-missing detection. */
187
- workerName?: string
188
- route?: string
189
- onLog?: (line: string) => void
190
- }
191
-
192
- export async function runWranglerDeploy(
193
- args: DeployArgs & { projectDir: string },
194
- ): Promise<{ url: string; raw: string; deploymentVersion?: string }> {
195
- await runWranglerTypes(args)
196
- const { cmd, prefix } = wranglerCommand(args.projectDir)
197
- const deploy = (configPath: string) =>
198
- runCapture(cmd, [...prefix, 'deploy', '--config', configPath], args.projectDir, args.onLog)
199
- return deployWithSelfFallback(deploy, args)
200
- }
201
-
202
- /**
203
- * Deploy with a one-time SELF-binding first-deploy recovery. A fresh worker that
204
- * declares a `services: SELF` binding can't deploy (its own script doesn't exist
205
- * yet); on THAT specific failure we deploy the SELF-less fallback to create the
206
- * script, then redeploy WITH SELF so autobinding works. Steady state (every
207
- * deploy after the first) is a single pass. The `deploy` runner is injected so
208
- * the orchestration is unit-testable without shelling out.
209
- */
210
- export async function deployWithSelfFallback(
211
- deploy: (configPath: string) => Promise<{ code: number; out: string }>,
212
- args: DeployArgs,
213
- ): Promise<{ url: string; raw: string; deploymentVersion?: string }> {
214
- let { code, out } = await deploy(args.configPath)
215
-
216
- if (code !== 0 && args.fallbackConfigPath && isSelfBindingMissing(out, args.workerName)) {
217
- args.onLog?.(
218
- 'SELF service binding not resolvable yet (first deploy) — creating the worker, then re-binding SELF',
219
- )
220
- const first = await deploy(args.fallbackConfigPath)
221
- if (first.code !== 0) {
222
- throw new Error(
223
- `wrangler deploy (no-self pass) failed (code ${first.code}):\n${first.out.slice(-2000)}`,
224
- )
225
- }
226
- ;({ code, out } = await deploy(args.configPath))
227
- }
228
-
229
- if (code !== 0) {
230
- throw new Error(`wrangler deploy failed (code ${code}):\n${out.slice(-2000)}`)
231
- }
232
- const url = parseDeployUrl(out, args.route)
233
- if (!url) {
234
- throw new Error(`could not determine deployed URL from wrangler output:\n${out.slice(-2000)}`)
235
- }
236
- const deploymentVersion = parseDeployVersionId(out)
237
- return { url, raw: out, ...(deploymentVersion === undefined ? {} : { deploymentVersion }) }
238
- }
239
-
240
- /**
241
- * True when wrangler failed specifically because the worker's OWN `SELF` service
242
- * binding can't resolve yet (first deploy of a fresh script). Requires the
243
- * worker name so an unrelated missing-service error doesn't trigger the two-pass.
244
- */
245
- export function isSelfBindingMissing(out: string, workerName?: string): boolean {
246
- const bindingError =
247
- /could not resolve binding/i.test(out) ||
248
- /service\b[^\n]*\bnot found/i.test(out) ||
249
- /script\b[^\n]*\bnot found/i.test(out)
250
- if (!bindingError || !/\bSELF\b/.test(out)) return false
251
- return workerName ? out.includes(workerName) : true
252
- }
253
-
254
- export async function bulkPutSecrets(args: {
255
- projectDir: string
256
- configPath: string
257
- secrets: Record<string, string>
258
- onLog?: (line: string) => void
259
- }): Promise<void> {
260
- const names = Object.keys(args.secrets)
261
- if (names.length === 0) return
262
- const { cmd, prefix } = wranglerCommand(args.projectDir)
263
- const dir = await mkdtemp(join(tmpdir(), 'astrale-secrets-'))
264
- const file = join(dir, 'secrets.json')
265
- try {
266
- await writeFile(file, JSON.stringify(args.secrets))
267
- const { code, out } = await runCapture(
268
- cmd,
269
- [...prefix, 'secret', 'bulk', file, '--config', args.configPath],
270
- args.projectDir,
271
- args.onLog,
272
- )
273
- if (code !== 0) {
274
- throw new Error(`wrangler secret bulk failed (code ${code}):\n${out.slice(-1500)}`)
275
- }
276
- } finally {
277
- await rm(dir, { recursive: true, force: true })
278
- }
279
- }
280
-
281
- // ── internals ──────────────────────────────────────────────────────────────
282
-
283
- function runCapture(
284
- cmd: string,
285
- args: string[],
286
- cwd: string,
287
- onLog?: (line: string) => void,
288
- ): Promise<{ code: number; out: string }> {
289
- return new Promise((resolve, reject) => {
290
- const child = spawn(cmd, args, {
291
- cwd,
292
- stdio: ['ignore', 'pipe', 'pipe'],
293
- env: wranglerProcessEnv(cwd),
294
- })
295
- let out = ''
296
- const onData = (buf: Buffer) => {
297
- const text = buf.toString()
298
- out += text
299
- for (const line of text.split('\n')) if (line.trim()) onLog?.(line)
300
- }
301
- child.stdout?.on('data', onData)
302
- child.stderr?.on('data', onData)
303
- child.on('exit', (code) => resolve({ code: code ?? 1, out }))
304
- child.on('error', reject)
305
- })
306
- }
307
-
308
- async function stopChild(child: ChildProcess): Promise<void> {
309
- if (child.exitCode !== null) return
310
- await new Promise<void>((resolve) => {
311
- child.on('exit', () => resolve())
312
- child.kill('SIGTERM')
313
- setTimeout(() => {
314
- if (child.exitCode === null) child.kill('SIGKILL')
315
- resolve()
316
- }, 4000).unref?.()
317
- })
318
- }
319
-
320
- /**
321
- * Build the single-module worker bundle WITHOUT deploying — `wrangler deploy
322
- * --dry-run --outdir` (no Cloudflare auth required). Returns the path of the
323
- * bundled .js module (the managed adapter publishes its bytes).
324
- */
325
- export async function runWranglerBundle(args: {
326
- projectDir: string
327
- configPath: string
328
- outDir: string
329
- onLog?: (line: string) => void
330
- }): Promise<string> {
331
- await runWranglerTypes(args)
332
- const { cmd, prefix } = wranglerCommand(args.projectDir)
333
- const { code, out } = await runCapture(
334
- cmd,
335
- [...prefix, 'deploy', '--dry-run', '--outdir', args.outDir, '--config', args.configPath],
336
- args.projectDir,
337
- args.onLog,
338
- )
339
- if (code !== 0) throw new Error(`wrangler bundle failed (exit ${code}): ${out.slice(-400)}`)
340
- const js = readdirSync(args.outDir).filter((f) => f.endsWith('.js'))
341
- if (js.length === 0) throw new Error(`wrangler bundle produced no .js in ${args.outDir}`)
342
- const preferred = js.find((f) => f.startsWith('worker')) ?? js[0]!
343
- return join(args.outDir, preferred)
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
- }