@astrale-os/adapter-cloudflare 0.5.0-beta.2 → 0.5.0-beta.21
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.
- package/README.md +11 -0
- package/dist/adapter.d.ts +17 -0
- package/dist/adapter.js +140 -0
- package/dist/build/artifact.d.ts +8 -0
- package/dist/build/artifact.js +179 -0
- package/dist/build/closure.d.ts +2 -0
- package/dist/build/closure.js +22 -0
- package/dist/build/codegen/execution.d.ts +13 -0
- package/dist/build/codegen/execution.js +44 -0
- package/dist/build/codegen/gateway.d.ts +1 -0
- package/dist/build/codegen/gateway.js +7 -0
- package/dist/build/codegen/merge.d.ts +0 -1
- package/dist/build/codegen/merge.js +0 -1
- package/dist/build/codegen/wrangler.d.ts +10 -1
- package/dist/build/codegen/wrangler.js +22 -16
- package/dist/build/configuration.d.ts +9 -0
- package/dist/build/configuration.js +40 -0
- package/dist/build/frontend-v1.d.ts +16 -0
- package/dist/build/frontend-v1.js +133 -0
- package/dist/build/index.d.ts +8 -16
- package/dist/build/index.js +8 -16
- package/dist/build/metafile.d.ts +5 -0
- package/dist/build/metafile.js +14 -0
- package/dist/build/parse-output.d.ts +0 -1
- package/dist/build/parse-output.js +0 -1
- package/dist/build/qualification.d.ts +5 -0
- package/dist/build/qualification.js +7 -0
- package/dist/build/roots.d.ts +2 -0
- package/dist/build/roots.js +34 -0
- package/dist/build/wrangler-cli.d.ts +53 -34
- package/dist/build/wrangler-cli.js +284 -111
- package/dist/cloudflare.d.ts +1 -34
- package/dist/cloudflare.js +1 -205
- package/dist/deploy/deploy.d.ts +17 -0
- package/dist/deploy/deploy.js +207 -0
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- package/dist/identity/binding.d.ts +2 -0
- package/dist/identity/binding.js +2 -0
- package/dist/identity/material.d.ts +7 -0
- package/dist/identity/material.js +42 -0
- package/dist/identity/secret.d.ts +3 -0
- package/dist/identity/secret.js +7 -0
- package/dist/identity/source.d.ts +26 -0
- package/dist/identity/source.js +98 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -4
- package/dist/params.d.ts +18 -5
- package/dist/params.js +0 -1
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.js +2 -0
- package/dist/release/namespace.d.ts +2 -0
- package/dist/release/namespace.js +6 -0
- package/dist/release/target.d.ts +2 -0
- package/dist/release/target.js +9 -0
- package/dist/worker/entry.d.ts +34 -0
- package/dist/worker/entry.js +157 -0
- package/dist/worker/fetch.d.ts +11 -0
- package/dist/worker/fetch.js +31 -0
- package/dist/worker/gateway.d.ts +15 -0
- package/dist/worker/gateway.js +26 -0
- package/dist/worker/identity.d.ts +7 -0
- package/dist/worker/identity.js +23 -0
- package/dist/worker/index.d.ts +3 -48
- package/dist/worker/index.js +3 -186
- package/package.json +21 -18
- package/template/astrale.config.ts +19 -11
- package/dist/build/codegen/identity.d.ts +0 -10
- package/dist/build/codegen/identity.d.ts.map +0 -1
- package/dist/build/codegen/identity.js +0 -32
- package/dist/build/codegen/identity.js.map +0 -1
- package/dist/build/codegen/merge.d.ts.map +0 -1
- package/dist/build/codegen/merge.js.map +0 -1
- package/dist/build/codegen/worker.d.ts +0 -12
- package/dist/build/codegen/worker.d.ts.map +0 -1
- package/dist/build/codegen/worker.js +0 -40
- package/dist/build/codegen/worker.js.map +0 -1
- package/dist/build/codegen/wrangler.d.ts.map +0 -1
- package/dist/build/codegen/wrangler.js.map +0 -1
- package/dist/build/frontend.d.ts +0 -26
- package/dist/build/frontend.d.ts.map +0 -1
- package/dist/build/frontend.js +0 -263
- package/dist/build/frontend.js.map +0 -1
- package/dist/build/index.d.ts.map +0 -1
- package/dist/build/index.js.map +0 -1
- package/dist/build/parse-output.d.ts.map +0 -1
- package/dist/build/parse-output.js.map +0 -1
- package/dist/build/prepare.d.ts +0 -18
- package/dist/build/prepare.d.ts.map +0 -1
- package/dist/build/prepare.js +0 -117
- package/dist/build/prepare.js.map +0 -1
- package/dist/build/worker.d.ts +0 -15
- package/dist/build/worker.d.ts.map +0 -1
- package/dist/build/worker.js +0 -21
- package/dist/build/worker.js.map +0 -1
- package/dist/build/wrangler-cli.d.ts.map +0 -1
- package/dist/build/wrangler-cli.js.map +0 -1
- package/dist/cloudflare.d.ts.map +0 -1
- package/dist/cloudflare.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/params.d.ts.map +0 -1
- package/dist/params.js.map +0 -1
- package/dist/worker/index.d.ts.map +0 -1
- package/dist/worker/index.js.map +0 -1
- package/src/build/codegen/identity.ts +0 -40
- package/src/build/codegen/merge.ts +0 -92
- package/src/build/codegen/worker.ts +0 -53
- package/src/build/codegen/wrangler.ts +0 -127
- package/src/build/frontend.ts +0 -324
- package/src/build/index.ts +0 -29
- package/src/build/parse-output.ts +0 -60
- package/src/build/prepare.ts +0 -161
- package/src/build/worker.ts +0 -37
- package/src/build/wrangler-cli.ts +0 -362
- package/src/cloudflare.ts +0 -251
- package/src/index.ts +0 -21
- package/src/params.ts +0 -92
- package/src/worker/index.ts +0 -276
- package/src/worker/tsconfig.json +0 -10
|
@@ -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
|
-
}
|
package/src/cloudflare.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `cloudflare(envs)` — the Cloudflare deployment adapter.
|
|
3
|
-
*
|
|
4
|
-
* Owns everything target-specific: it codegens the Worker entry + wrangler
|
|
5
|
-
* config (the dev never sees a `wrangler.jsonc`), materializes the optional frontend
|
|
6
|
-
* SPA, and shells out to `wrangler`. `watch` → `wrangler dev` (local URL);
|
|
7
|
-
* `deploy` → `wrangler deploy` (workers.dev or a custom-domain URL); secrets →
|
|
8
|
-
* `wrangler secret bulk`. Both `watch` and `deploy` return the URL the CLI
|
|
9
|
-
* prints and `astrale domain install <url>` consumes.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { DomainAdapter } from '@astrale-os/sdk'
|
|
13
|
-
|
|
14
|
-
import { defineAdapter } from '@astrale-os/sdk'
|
|
15
|
-
|
|
16
|
-
import type { CloudflareParams } from './params.js'
|
|
17
|
-
|
|
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'
|
|
29
|
-
|
|
30
|
-
export function cloudflare(
|
|
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,
|
|
64
|
-
): DomainAdapter<CloudflareParams> {
|
|
65
|
-
return defineAdapter<CloudflareParams>({
|
|
66
|
-
name: 'cloudflare',
|
|
67
|
-
envs,
|
|
68
|
-
|
|
69
|
-
async build(params, ctx) {
|
|
70
|
-
await dependencies.buildCloudflareWorker({
|
|
71
|
-
params,
|
|
72
|
-
context: ctx,
|
|
73
|
-
onLog: dependencies.logTo(),
|
|
74
|
-
})
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
async watch(params, ctx) {
|
|
78
|
-
const onLog = dependencies.logTo()
|
|
79
|
-
const frontend = await dependencies.buildFrontend({
|
|
80
|
-
definition: ctx.definition,
|
|
81
|
-
projectDir: ctx.projectDir,
|
|
82
|
-
onLog,
|
|
83
|
-
})
|
|
84
|
-
const { configPath, port } = await dependencies.prepare(params, ctx, 'dev', frontend)
|
|
85
|
-
const handle = await dependencies.runWranglerDev({
|
|
86
|
-
projectDir: ctx.projectDir,
|
|
87
|
-
configPath,
|
|
88
|
-
port,
|
|
89
|
-
remote: Boolean(params.remote),
|
|
90
|
-
autoPickPort: params.port === undefined,
|
|
91
|
-
...(params.host ? { ip: '0.0.0.0' } : {}),
|
|
92
|
-
onReload: ctx.onReload,
|
|
93
|
-
onLog,
|
|
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
|
-
}
|
|
117
|
-
// With --host the actionable URL is the PUBLIC one (the tunnel/proxy
|
|
118
|
-
// front), not the local bind — print and spec-stamp that.
|
|
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
|
-
}
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
// Config hot-regen: rewrite the generated files only — the running
|
|
142
|
-
// `wrangler dev` watches its `--config` (and the worker entry) and reloads
|
|
143
|
-
// them itself, so a `astrale.config.ts` edit lands without a restart.
|
|
144
|
-
// Mirrors `watch`'s prep exactly (codegen + frontend build), minus the spawn.
|
|
145
|
-
async regenerate(params, ctx) {
|
|
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(),
|
|
156
|
-
})
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
async deploy(params, ctx) {
|
|
160
|
-
const frontend = await dependencies.buildFrontend({
|
|
161
|
-
definition: ctx.definition,
|
|
162
|
-
projectDir: ctx.projectDir,
|
|
163
|
-
onLog: dependencies.logTo(),
|
|
164
|
-
})
|
|
165
|
-
const {
|
|
166
|
-
configPath,
|
|
167
|
-
fallbackConfigPath,
|
|
168
|
-
workerName: name,
|
|
169
|
-
} = await dependencies.prepare(params, ctx, 'deploy', frontend)
|
|
170
|
-
const { url, deploymentVersion } = await dependencies.runWranglerDeploy({
|
|
171
|
-
projectDir: ctx.projectDir,
|
|
172
|
-
configPath,
|
|
173
|
-
workerName: name,
|
|
174
|
-
...(fallbackConfigPath ? { fallbackConfigPath } : {}),
|
|
175
|
-
...(params.route ? { route: params.route } : {}),
|
|
176
|
-
onLog: dependencies.logTo(),
|
|
177
|
-
})
|
|
178
|
-
if (Object.keys(ctx.secrets).length > 0) {
|
|
179
|
-
await dependencies.bulkPutSecrets({
|
|
180
|
-
projectDir: ctx.projectDir,
|
|
181
|
-
configPath,
|
|
182
|
-
secrets: ctx.secrets,
|
|
183
|
-
onLog: dependencies.logTo(),
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
// A first deploy on a fresh `*.workers.dev` host can take ~30-60s to
|
|
187
|
-
// propagate; an `astrale domain install <url>` issued right away would
|
|
188
|
-
// 404 (Cloudflare 1042). Block until the URL actually serves so the
|
|
189
|
-
// install line we print is immediately actionable.
|
|
190
|
-
await dependencies.waitUntilLive(url, dependencies.logTo(), deploymentVersion)
|
|
191
|
-
return { url }
|
|
192
|
-
},
|
|
193
|
-
|
|
194
|
-
secretsFile(params) {
|
|
195
|
-
return params.secrets
|
|
196
|
-
},
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Poll until the deployed Worker health endpoint returns exactly 200.
|
|
202
|
-
*/
|
|
203
|
-
async function waitUntilLive(
|
|
204
|
-
url: string,
|
|
205
|
-
onLog: (line: string) => void,
|
|
206
|
-
expectedDeploymentVersion?: string,
|
|
207
|
-
timeoutMs = 90_000,
|
|
208
|
-
): Promise<void> {
|
|
209
|
-
const probe = new URL('/health', url)
|
|
210
|
-
const metadata = new URL('/meta', url)
|
|
211
|
-
const deadline = Date.now() + timeoutMs
|
|
212
|
-
let waited = false
|
|
213
|
-
let lastStatus: number | undefined
|
|
214
|
-
let lastDeploymentVersion: string | undefined
|
|
215
|
-
for (;;) {
|
|
216
|
-
try {
|
|
217
|
-
const res = await fetch(probe, { redirect: 'manual' })
|
|
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
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
// Network hiccup — treat like "not live yet" and keep polling.
|
|
234
|
-
}
|
|
235
|
-
if (Date.now() >= deadline) {
|
|
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
|
-
)
|
|
244
|
-
}
|
|
245
|
-
if (!waited) {
|
|
246
|
-
waited = true
|
|
247
|
-
onLog('waiting for the URL to go live (first deploys can take ~30-60s)…')
|
|
248
|
-
}
|
|
249
|
-
await new Promise((r) => setTimeout(r, 3000))
|
|
250
|
-
}
|
|
251
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@astrale-os/adapter-cloudflare` — deploy an Astrale domain as a standalone
|
|
3
|
-
* Cloudflare Worker on YOUR OWN Cloudflare account.
|
|
4
|
-
*
|
|
5
|
-
* import { cloudflare } from '@astrale-os/adapter-cloudflare'
|
|
6
|
-
*
|
|
7
|
-
* adapter: cloudflare({
|
|
8
|
-
* dev: { secrets: '.env.dev' },
|
|
9
|
-
* prod: { route: 'crm.acme.dev', secrets: '.env.prod' },
|
|
10
|
-
* })
|
|
11
|
-
*
|
|
12
|
-
* `dev` runs `wrangler dev` locally; an env with no `route` ships to
|
|
13
|
-
* `*.workers.dev`; an env with a `route` ships to that custom domain.
|
|
14
|
-
*
|
|
15
|
-
* Prefer a managed deploy (no Cloudflare account)? Use
|
|
16
|
-
* `@astrale-os/adapter-astrale`, which builds the same bundle via this package's
|
|
17
|
-
* `/build` toolkit and ships it through the Astrale platform.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export { cloudflare } from './cloudflare.js'
|
|
21
|
-
export type { CloudflareParams } from './params.js'
|