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