@astrale-os/adapter-cloudflare 0.5.0-beta.7 → 0.5.0-beta.70

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