@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.
Files changed (120) hide show
  1. package/README.md +11 -0
  2. package/dist/adapter.d.ts +17 -0
  3. package/dist/adapter.js +140 -0
  4. package/dist/build/artifact.d.ts +8 -0
  5. package/dist/build/artifact.js +179 -0
  6. package/dist/build/closure.d.ts +2 -0
  7. package/dist/build/closure.js +22 -0
  8. package/dist/build/codegen/execution.d.ts +13 -0
  9. package/dist/build/codegen/execution.js +44 -0
  10. package/dist/build/codegen/gateway.d.ts +1 -0
  11. package/dist/build/codegen/gateway.js +7 -0
  12. package/dist/build/codegen/merge.d.ts +0 -1
  13. package/dist/build/codegen/merge.js +0 -1
  14. package/dist/build/codegen/wrangler.d.ts +10 -1
  15. package/dist/build/codegen/wrangler.js +22 -16
  16. package/dist/build/configuration.d.ts +9 -0
  17. package/dist/build/configuration.js +40 -0
  18. package/dist/build/frontend-v1.d.ts +16 -0
  19. package/dist/build/frontend-v1.js +133 -0
  20. package/dist/build/index.d.ts +8 -16
  21. package/dist/build/index.js +8 -16
  22. package/dist/build/metafile.d.ts +5 -0
  23. package/dist/build/metafile.js +14 -0
  24. package/dist/build/parse-output.d.ts +0 -1
  25. package/dist/build/parse-output.js +0 -1
  26. package/dist/build/qualification.d.ts +5 -0
  27. package/dist/build/qualification.js +7 -0
  28. package/dist/build/roots.d.ts +2 -0
  29. package/dist/build/roots.js +34 -0
  30. package/dist/build/wrangler-cli.d.ts +53 -34
  31. package/dist/build/wrangler-cli.js +284 -111
  32. package/dist/cloudflare.d.ts +1 -34
  33. package/dist/cloudflare.js +1 -205
  34. package/dist/deploy/deploy.d.ts +17 -0
  35. package/dist/deploy/deploy.js +207 -0
  36. package/dist/deploy/index.d.ts +1 -0
  37. package/dist/deploy/index.js +1 -0
  38. package/dist/identity/binding.d.ts +2 -0
  39. package/dist/identity/binding.js +2 -0
  40. package/dist/identity/material.d.ts +7 -0
  41. package/dist/identity/material.js +42 -0
  42. package/dist/identity/secret.d.ts +3 -0
  43. package/dist/identity/secret.js +7 -0
  44. package/dist/identity/source.d.ts +26 -0
  45. package/dist/identity/source.js +98 -0
  46. package/dist/index.d.ts +3 -4
  47. package/dist/index.js +3 -4
  48. package/dist/params.d.ts +18 -5
  49. package/dist/params.js +0 -1
  50. package/dist/release/index.d.ts +2 -0
  51. package/dist/release/index.js +2 -0
  52. package/dist/release/namespace.d.ts +2 -0
  53. package/dist/release/namespace.js +6 -0
  54. package/dist/release/target.d.ts +2 -0
  55. package/dist/release/target.js +9 -0
  56. package/dist/worker/entry.d.ts +34 -0
  57. package/dist/worker/entry.js +157 -0
  58. package/dist/worker/fetch.d.ts +11 -0
  59. package/dist/worker/fetch.js +31 -0
  60. package/dist/worker/gateway.d.ts +15 -0
  61. package/dist/worker/gateway.js +26 -0
  62. package/dist/worker/identity.d.ts +7 -0
  63. package/dist/worker/identity.js +23 -0
  64. package/dist/worker/index.d.ts +3 -48
  65. package/dist/worker/index.js +3 -186
  66. package/package.json +21 -18
  67. package/template/astrale.config.ts +19 -11
  68. package/dist/build/codegen/identity.d.ts +0 -10
  69. package/dist/build/codegen/identity.d.ts.map +0 -1
  70. package/dist/build/codegen/identity.js +0 -32
  71. package/dist/build/codegen/identity.js.map +0 -1
  72. package/dist/build/codegen/merge.d.ts.map +0 -1
  73. package/dist/build/codegen/merge.js.map +0 -1
  74. package/dist/build/codegen/worker.d.ts +0 -12
  75. package/dist/build/codegen/worker.d.ts.map +0 -1
  76. package/dist/build/codegen/worker.js +0 -40
  77. package/dist/build/codegen/worker.js.map +0 -1
  78. package/dist/build/codegen/wrangler.d.ts.map +0 -1
  79. package/dist/build/codegen/wrangler.js.map +0 -1
  80. package/dist/build/frontend.d.ts +0 -26
  81. package/dist/build/frontend.d.ts.map +0 -1
  82. package/dist/build/frontend.js +0 -263
  83. package/dist/build/frontend.js.map +0 -1
  84. package/dist/build/index.d.ts.map +0 -1
  85. package/dist/build/index.js.map +0 -1
  86. package/dist/build/parse-output.d.ts.map +0 -1
  87. package/dist/build/parse-output.js.map +0 -1
  88. package/dist/build/prepare.d.ts +0 -18
  89. package/dist/build/prepare.d.ts.map +0 -1
  90. package/dist/build/prepare.js +0 -117
  91. package/dist/build/prepare.js.map +0 -1
  92. package/dist/build/worker.d.ts +0 -15
  93. package/dist/build/worker.d.ts.map +0 -1
  94. package/dist/build/worker.js +0 -21
  95. package/dist/build/worker.js.map +0 -1
  96. package/dist/build/wrangler-cli.d.ts.map +0 -1
  97. package/dist/build/wrangler-cli.js.map +0 -1
  98. package/dist/cloudflare.d.ts.map +0 -1
  99. package/dist/cloudflare.js.map +0 -1
  100. package/dist/index.d.ts.map +0 -1
  101. package/dist/index.js.map +0 -1
  102. package/dist/params.d.ts.map +0 -1
  103. package/dist/params.js.map +0 -1
  104. package/dist/worker/index.d.ts.map +0 -1
  105. package/dist/worker/index.js.map +0 -1
  106. package/src/build/codegen/identity.ts +0 -40
  107. package/src/build/codegen/merge.ts +0 -92
  108. package/src/build/codegen/worker.ts +0 -53
  109. package/src/build/codegen/wrangler.ts +0 -127
  110. package/src/build/frontend.ts +0 -324
  111. package/src/build/index.ts +0 -29
  112. package/src/build/parse-output.ts +0 -60
  113. package/src/build/prepare.ts +0 -161
  114. package/src/build/worker.ts +0 -37
  115. package/src/build/wrangler-cli.ts +0 -362
  116. package/src/cloudflare.ts +0 -251
  117. package/src/index.ts +0 -21
  118. package/src/params.ts +0 -92
  119. package/src/worker/index.ts +0 -276
  120. package/src/worker/tsconfig.json +0 -10
@@ -1,324 +0,0 @@
1
- import type { CompiledFrontendArtifact, DomainDefinition, FrontendFile } from '@astrale-os/sdk'
2
- import type { ReactFrontendArtifact } from '@astrale-os/sdk/react'
3
-
4
- import { compileFrontend } from '@astrale-os/sdk'
5
- import { compileReactEntrypoint } from '@astrale-os/sdk/react'
6
- import { spawn } from 'node:child_process'
7
- import { watch } from 'node:fs'
8
- import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
9
- import { dirname, join, resolve, sep } from 'node:path'
10
-
11
- export const FRONTEND_STAGE_DIR = '.astrale/frontend'
12
- const FRONTEND_REBUILD_DELAY_MS = 75
13
- let stageGeneration = 0
14
- const buildQueues = new Map<string, Promise<void>>()
15
-
16
- /** Materialize one Domain-owned frontend recipe into an immutable adapter artifact. */
17
- export async function buildFrontend(input: {
18
- readonly definition: DomainDefinition
19
- readonly projectDir: string
20
- readonly onLog?: (line: string) => void
21
- }): Promise<CompiledFrontendArtifact | undefined> {
22
- const key = resolve(input.projectDir)
23
- const previous = buildQueues.get(key) ?? Promise.resolve()
24
- let release = (): void => {}
25
- const current = new Promise<void>((resolveBuild) => {
26
- release = resolveBuild
27
- })
28
- const tail = previous.then(() => current)
29
- buildQueues.set(key, tail)
30
- await previous
31
- try {
32
- return await buildFrontendUnlocked(input)
33
- } finally {
34
- release()
35
- if (buildQueues.get(key) === tail) buildQueues.delete(key)
36
- }
37
- }
38
-
39
- async function buildFrontendUnlocked(input: {
40
- readonly definition: DomainDefinition
41
- readonly projectDir: string
42
- readonly onLog?: (line: string) => void
43
- }): Promise<CompiledFrontendArtifact | undefined> {
44
- const artifact = input.definition.frontend
45
- if (artifact === undefined) return undefined
46
- if (artifact.source.kind === 'external') {
47
- return compileFrontend({
48
- artifact,
49
- files: [],
50
- provenance: { adapter: 'cloudflare', source: 'external' },
51
- })
52
- }
53
-
54
- let files: readonly FrontendFile[]
55
- if (artifact.source.kind === 'generated') {
56
- files = artifact.source.files.map((file) => ({
57
- path: file.path,
58
- content:
59
- typeof file.content === 'string' ? new TextEncoder().encode(file.content) : file.content,
60
- ...(file.mediaType === undefined ? {} : { mediaType: file.mediaType }),
61
- }))
62
- } else if (artifact.source.kind === 'prebuilt') {
63
- files = await readFiles(
64
- contained(input.projectDir, artifact.source.directory, 'prebuilt frontend'),
65
- )
66
- } else {
67
- if (isReactFrontend(artifact)) {
68
- const root = contained(input.projectDir, artifact.source.root, 'React frontend root')
69
- for (const file of compileReactEntrypoint(artifact)) {
70
- const path = contained(root, file.path, 'generated React entrypoint')
71
- await mkdir(dirname(path), { recursive: true })
72
- await writeFile(path, file.content)
73
- }
74
- }
75
- const output = contained(input.projectDir, artifact.source.outDir, 'Vite frontend output')
76
- await rm(output, { recursive: true, force: true })
77
- await runBuild(
78
- artifact.source.command[0],
79
- [
80
- ...artifact.source.command.slice(1),
81
- artifact.source.root,
82
- '--outDir',
83
- output,
84
- '--emptyOutDir',
85
- ],
86
- input.projectDir,
87
- input.onLog,
88
- )
89
- files = await readFiles(output)
90
- }
91
- const compiled = await compileFrontend({
92
- artifact,
93
- baseHref: 'https://frontend.invalid/',
94
- files,
95
- provenance: { adapter: 'cloudflare', source: artifact.source.kind },
96
- })
97
- await replaceStage(input.projectDir, compiled.files)
98
- return compiled
99
- }
100
-
101
- export interface FrontendWatchHandle {
102
- stop(): Promise<void>
103
- }
104
-
105
- export interface FrontendWatchSubscription {
106
- close(): void
107
- }
108
-
109
- export type FrontendWatchSubscribe = (
110
- root: string,
111
- onChange: (path?: string) => void,
112
- onError: (error: Error) => void,
113
- ) => FrontendWatchSubscription
114
-
115
- /** Keep Wrangler's materialized frontend stage current without owning config regeneration. */
116
- export function watchFrontend(input: {
117
- readonly definition: DomainDefinition
118
- readonly projectDir: string
119
- readonly onLog?: (line: string) => void
120
- readonly onRebuild?: () => void
121
- readonly delayMs?: number
122
- readonly build?: typeof buildFrontend
123
- readonly subscribe?: FrontendWatchSubscribe
124
- }): FrontendWatchHandle | undefined {
125
- const artifact = input.definition.frontend
126
- if (
127
- artifact === undefined ||
128
- artifact.source.kind === 'external' ||
129
- artifact.source.kind === 'generated'
130
- )
131
- return undefined
132
-
133
- const source = artifact.source
134
- const root =
135
- source.kind === 'prebuilt'
136
- ? contained(input.projectDir, source.directory, 'prebuilt frontend watch root')
137
- : resolve(input.projectDir)
138
- const build = input.build ?? buildFrontend
139
- const subscribe = input.subscribe ?? subscribeToFrontend
140
- const delayMs = input.delayMs ?? FRONTEND_REBUILD_DELAY_MS
141
- let stopped = false
142
- let pending = false
143
- let timer: ReturnType<typeof setTimeout> | undefined
144
- let running: Promise<void> | undefined
145
-
146
- const drain = async (): Promise<void> => {
147
- if (stopped || running !== undefined || !pending) return
148
- pending = false
149
- running = (async () => {
150
- try {
151
- await build({
152
- definition: input.definition,
153
- projectDir: input.projectDir,
154
- onLog: input.onLog,
155
- })
156
- if (!stopped) input.onRebuild?.()
157
- } catch (error) {
158
- input.onLog?.(
159
- `frontend rebuild failed; keeping the last good stage: ${error instanceof Error ? error.message : String(error)}`,
160
- )
161
- }
162
- })()
163
- await running
164
- running = undefined
165
- if (!stopped && pending) {
166
- timer = setTimeout(() => {
167
- timer = undefined
168
- void drain()
169
- }, delayMs)
170
- }
171
- }
172
- const schedule = (path?: string) => {
173
- if (stopped || !rebuildsFor(source, path)) return
174
- pending = true
175
- if (timer !== undefined) clearTimeout(timer)
176
- timer = setTimeout(() => {
177
- timer = undefined
178
- void drain()
179
- }, delayMs)
180
- }
181
- const subscription = subscribe(root, schedule, (error) => {
182
- input.onLog?.(`frontend watcher failed: ${error.message}`)
183
- })
184
-
185
- return Object.freeze({
186
- async stop() {
187
- if (stopped) return
188
- stopped = true
189
- pending = false
190
- if (timer !== undefined) clearTimeout(timer)
191
- timer = undefined
192
- subscription.close()
193
- await running
194
- },
195
- })
196
- }
197
-
198
- function isReactFrontend(
199
- artifact: DomainDefinition['frontend'],
200
- ): artifact is ReactFrontendArtifact {
201
- return (
202
- artifact !== undefined && (artifact as { readonly framework?: unknown }).framework === 'react'
203
- )
204
- }
205
-
206
- async function readFiles(root: string, current = ''): Promise<readonly FrontendFile[]> {
207
- const directory = join(root, current)
208
- const entries = await readdir(directory, { withFileTypes: true })
209
- const files: FrontendFile[] = []
210
- for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
211
- const path = current === '' ? entry.name : `${current}/${entry.name}`
212
- if (entry.isDirectory()) files.push(...(await readFiles(root, path)))
213
- else if (entry.isFile())
214
- files.push({ path, content: new Uint8Array(await readFile(join(root, path))) })
215
- }
216
- return files
217
- }
218
-
219
- async function replaceStage(projectDir: string, files: readonly FrontendFile[]): Promise<void> {
220
- const stage = contained(projectDir, FRONTEND_STAGE_DIR, 'frontend stage')
221
- const generation = `${process.pid}-${++stageGeneration}`
222
- const next = `${stage}.next-${generation}`
223
- const previous = `${stage}.previous-${generation}`
224
- await rm(next, { recursive: true, force: true })
225
- await mkdir(next, { recursive: true })
226
- try {
227
- for (const file of files) {
228
- const path = contained(next, file.path, 'staged frontend file')
229
- await mkdir(dirname(path), { recursive: true })
230
- await writeFile(path, file.content)
231
- }
232
- await rm(previous, { recursive: true, force: true })
233
- let retainedPrevious = false
234
- try {
235
- await rename(stage, previous)
236
- retainedPrevious = true
237
- } catch (error) {
238
- if (!isMissing(error)) throw error
239
- }
240
- try {
241
- await rename(next, stage)
242
- } catch (error) {
243
- if (retainedPrevious) await rename(previous, stage)
244
- throw error
245
- }
246
- if (retainedPrevious) await rm(previous, { recursive: true, force: true })
247
- } finally {
248
- await rm(next, { recursive: true, force: true })
249
- }
250
- }
251
-
252
- function subscribeToFrontend(
253
- root: string,
254
- onChange: (path?: string) => void,
255
- onError: (error: Error) => void,
256
- ): FrontendWatchSubscription {
257
- const watcher = watch(root, { recursive: true }, (_event, path) => {
258
- onChange(path === null ? undefined : String(path))
259
- })
260
- watcher.on('error', onError)
261
- return watcher
262
- }
263
-
264
- function rebuildsFor(
265
- source: Exclude<
266
- NonNullable<DomainDefinition['frontend']>['source'],
267
- { readonly kind: 'external' | 'generated' }
268
- >,
269
- path: string | undefined,
270
- ): boolean {
271
- if (path === undefined || source.kind === 'prebuilt') return true
272
- const normalized = path.replaceAll('\\', '/')
273
- if (
274
- normalized === '.astrale' ||
275
- normalized.startsWith('.astrale/') ||
276
- normalized === 'node_modules' ||
277
- normalized.startsWith('node_modules/') ||
278
- normalized === '.git' ||
279
- normalized.startsWith('.git/')
280
- )
281
- return false
282
- const outDir = source.outDir.replace(/^\.\//u, '')
283
- if (normalized === outDir || normalized.startsWith(`${outDir}/`)) return false
284
- const root = source.root.replace(/^\.\//u, '')
285
- return normalized !== `${root}/astrale.generated.tsx` && normalized !== `${root}/index.html`
286
- }
287
-
288
- function isMissing(error: unknown): boolean {
289
- return error instanceof Error && 'code' in error && error.code === 'ENOENT'
290
- }
291
-
292
- function contained(root: string, input: string, label: string): string {
293
- const base = resolve(root)
294
- const target = resolve(base, input)
295
- if (target !== base && !target.startsWith(`${base}${sep}`)) {
296
- throw new TypeError(`${label} must remain within the Domain project root.`)
297
- }
298
- return target
299
- }
300
-
301
- function runBuild(
302
- command: string,
303
- args: readonly string[],
304
- cwd: string,
305
- onLog?: (line: string) => void,
306
- ): Promise<void> {
307
- return new Promise((resolveBuild, reject) => {
308
- const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] })
309
- let output = ''
310
- const collect = (buffer: Buffer) => {
311
- const value = buffer.toString()
312
- output += value
313
- for (const line of value.split('\n')) if (line.trim() !== '') onLog?.(line)
314
- }
315
- child.stdout?.on('data', collect)
316
- child.stderr?.on('data', collect)
317
- child.on('error', reject)
318
- child.on('exit', (code) =>
319
- code === 0
320
- ? resolveBuild()
321
- : reject(new Error(`frontend build failed (code ${code ?? 1}):\n${output.slice(-1500)}`)),
322
- )
323
- })
324
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * `@astrale-os/adapter-cloudflare/build` — the shared Cloudflare-Workers build
3
- * toolkit, exposed for sibling deploy adapters (e.g.
4
- * `@astrale-os/adapter-astrale`).
5
- *
6
- * This is the "compile a `defineDomain` into a Cloudflare Workers bundle + run
7
- * local dev" surface that every Cloudflare-Workers deploy strategy shares,
8
- * regardless of WHERE the bundle ultimately ships. It is NOT part of the public
9
- * adapter API (`@astrale-os/adapter-cloudflare`) — it's the seam two adapters
10
- * build on so the codegen/bundling/dev logic lives in exactly one place.
11
- */
12
-
13
- export {
14
- buildFrontend,
15
- FRONTEND_STAGE_DIR,
16
- watchFrontend,
17
- type FrontendWatchHandle,
18
- type FrontendWatchSubscribe,
19
- type FrontendWatchSubscription,
20
- } from './frontend.js'
21
- export { logTo, prepare } from './prepare.js'
22
- export { buildCloudflareWorker, type CloudflareWorkerBuild } from './worker.js'
23
- export {
24
- bulkPutSecrets,
25
- runWranglerBundle,
26
- runWranglerDeploy,
27
- runWranglerDev,
28
- runWranglerTypes,
29
- } from './wrangler-cli.js'
@@ -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
- }