@astrale-os/adapter-cloudflare 0.5.0-beta.12 → 0.5.0-beta.13
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/dist/build/codegen/wrangler.d.ts +1 -0
- package/dist/build/codegen/wrangler.d.ts.map +1 -1
- package/dist/build/codegen/wrangler.js +13 -11
- package/dist/build/codegen/wrangler.js.map +1 -1
- package/dist/build/frontend.d.ts +13 -12
- package/dist/build/frontend.d.ts.map +1 -1
- package/dist/build/frontend.js +120 -131
- package/dist/build/frontend.js.map +1 -1
- package/dist/build/index.d.ts +1 -1
- package/dist/build/index.d.ts.map +1 -1
- package/dist/build/index.js +1 -1
- package/dist/build/index.js.map +1 -1
- package/dist/build/prepare.d.ts +1 -1
- package/dist/build/prepare.d.ts.map +1 -1
- package/dist/build/prepare.js +26 -5
- package/dist/build/prepare.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -2
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +31 -37
- package/dist/cloudflare.js.map +1 -1
- package/dist/worker/index.js +21 -9
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/src/build/codegen/wrangler.ts +14 -11
- package/src/build/frontend.ts +147 -173
- package/src/build/index.ts +4 -4
- package/src/build/prepare.ts +34 -6
- package/src/cloudflare.ts +42 -38
- package/src/worker/index.ts +24 -9
package/src/build/frontend.ts
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import type { CompiledFrontendArtifact, DomainDefinition, FrontendFile } from '@astrale-os/sdk'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ChildProcess } from 'node:child_process'
|
|
3
3
|
|
|
4
4
|
import { compileFrontend } from '@astrale-os/sdk'
|
|
5
|
-
import { compileReactEntrypoint } from '@astrale-os/sdk/react'
|
|
6
5
|
import { spawn } from 'node:child_process'
|
|
7
|
-
import {
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
8
7
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { createServer } from 'node:net'
|
|
9
9
|
import { dirname, join, resolve, sep } from 'node:path'
|
|
10
10
|
|
|
11
11
|
export const FRONTEND_STAGE_DIR = '.astrale/frontend'
|
|
12
|
-
const
|
|
12
|
+
const FRONTEND_BUILD_DIR = '.astrale/frontend-build'
|
|
13
13
|
let stageGeneration = 0
|
|
14
14
|
const buildQueues = new Map<string, Promise<void>>()
|
|
15
15
|
|
|
16
|
+
export interface ViteBuildInput {
|
|
17
|
+
readonly projectDir: string
|
|
18
|
+
readonly output: string
|
|
19
|
+
readonly config?: string
|
|
20
|
+
readonly onLog?: (line: string) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ViteBuild = (input: ViteBuildInput) => Promise<void>
|
|
24
|
+
|
|
16
25
|
/** Materialize one Domain-owned frontend recipe into an immutable adapter artifact. */
|
|
17
26
|
export async function buildFrontend(input: {
|
|
18
27
|
readonly definition: DomainDefinition
|
|
19
28
|
readonly projectDir: string
|
|
20
29
|
readonly onLog?: (line: string) => void
|
|
30
|
+
readonly buildVite?: ViteBuild
|
|
21
31
|
}): Promise<CompiledFrontendArtifact | undefined> {
|
|
22
32
|
const key = resolve(input.projectDir)
|
|
23
33
|
const previous = buildQueues.get(key) ?? Promise.resolve()
|
|
@@ -40,6 +50,7 @@ async function buildFrontendUnlocked(input: {
|
|
|
40
50
|
readonly definition: DomainDefinition
|
|
41
51
|
readonly projectDir: string
|
|
42
52
|
readonly onLog?: (line: string) => void
|
|
53
|
+
readonly buildVite?: ViteBuild
|
|
43
54
|
}): Promise<CompiledFrontendArtifact | undefined> {
|
|
44
55
|
const artifact = input.definition.frontend
|
|
45
56
|
if (artifact === undefined) return undefined
|
|
@@ -50,44 +61,15 @@ async function buildFrontendUnlocked(input: {
|
|
|
50
61
|
provenance: { adapter: 'cloudflare', source: 'external' },
|
|
51
62
|
})
|
|
52
63
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
}
|
|
64
|
+
const output = contained(input.projectDir, FRONTEND_BUILD_DIR, 'Vite frontend staging output')
|
|
65
|
+
await rm(output, { recursive: true, force: true })
|
|
66
|
+
await (input.buildVite ?? runViteBuild)({
|
|
67
|
+
projectDir: input.projectDir,
|
|
68
|
+
output,
|
|
69
|
+
...(artifact.source.config === undefined ? {} : { config: artifact.source.config }),
|
|
70
|
+
...(input.onLog === undefined ? {} : { onLog: input.onLog }),
|
|
71
|
+
})
|
|
72
|
+
const files = await readFiles(output)
|
|
91
73
|
const compiled = await compileFrontend({
|
|
92
74
|
artifact,
|
|
93
75
|
baseHref: 'https://frontend.invalid/',
|
|
@@ -98,109 +80,53 @@ async function buildFrontendUnlocked(input: {
|
|
|
98
80
|
return compiled
|
|
99
81
|
}
|
|
100
82
|
|
|
101
|
-
export interface
|
|
83
|
+
export interface FrontendDevHandle {
|
|
84
|
+
readonly url: string
|
|
102
85
|
stop(): Promise<void>
|
|
103
86
|
}
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
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: {
|
|
88
|
+
/** Start the Domain project's own Vite server; the Worker remains the browser-facing origin. */
|
|
89
|
+
export async function startFrontendDev(input: {
|
|
117
90
|
readonly definition: DomainDefinition
|
|
118
91
|
readonly projectDir: string
|
|
119
92
|
readonly onLog?: (line: string) => void
|
|
120
|
-
|
|
121
|
-
readonly delayMs?: number
|
|
122
|
-
readonly build?: typeof buildFrontend
|
|
123
|
-
readonly subscribe?: FrontendWatchSubscribe
|
|
124
|
-
}): FrontendWatchHandle | undefined {
|
|
93
|
+
}): Promise<FrontendDevHandle | undefined> {
|
|
125
94
|
const artifact = input.definition.frontend
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
95
|
+
if (artifact === undefined || artifact.source.kind !== 'vite') return undefined
|
|
96
|
+
const command = viteCommand(input.projectDir)
|
|
97
|
+
const port = await ephemeralPort('127.0.0.1')
|
|
98
|
+
const args = [
|
|
99
|
+
...command.prefix,
|
|
100
|
+
'--host',
|
|
101
|
+
'127.0.0.1',
|
|
102
|
+
'--port',
|
|
103
|
+
String(port),
|
|
104
|
+
'--strictPort',
|
|
105
|
+
...(artifact.source.config === undefined ? [] : ['--config', artifact.source.config]),
|
|
106
|
+
]
|
|
107
|
+
const child = spawn(command.command, args, {
|
|
108
|
+
cwd: input.projectDir,
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
})
|
|
111
|
+
const output: string[] = []
|
|
112
|
+
const collect = (buffer: Buffer) => {
|
|
113
|
+
for (const line of buffer.toString().split('\n')) {
|
|
114
|
+
if (line.trim() === '') continue
|
|
115
|
+
output.push(line)
|
|
116
|
+
if (output.length > 40) output.shift()
|
|
117
|
+
input.onLog?.(line)
|
|
170
118
|
}
|
|
171
119
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
120
|
+
child.stdout?.on('data', collect)
|
|
121
|
+
child.stderr?.on('data', collect)
|
|
122
|
+
const url = `http://127.0.0.1:${port}`
|
|
123
|
+
try {
|
|
124
|
+
await waitUntilReady(url, child, output)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await stopChild(child)
|
|
127
|
+
throw error
|
|
180
128
|
}
|
|
181
|
-
|
|
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
|
-
)
|
|
129
|
+
return Object.freeze({ url, stop: () => stopChild(child) })
|
|
204
130
|
}
|
|
205
131
|
|
|
206
132
|
async function readFiles(root: string, current = ''): Promise<readonly FrontendFile[]> {
|
|
@@ -249,42 +175,6 @@ async function replaceStage(projectDir: string, files: readonly FrontendFile[]):
|
|
|
249
175
|
}
|
|
250
176
|
}
|
|
251
177
|
|
|
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
178
|
function isMissing(error: unknown): boolean {
|
|
289
179
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
290
180
|
}
|
|
@@ -298,7 +188,34 @@ function contained(root: string, input: string, label: string): string {
|
|
|
298
188
|
return target
|
|
299
189
|
}
|
|
300
190
|
|
|
301
|
-
function
|
|
191
|
+
function viteCommand(projectDir: string): { command: string; prefix: readonly string[] } {
|
|
192
|
+
const command = join(projectDir, 'node_modules', '.bin', 'vite')
|
|
193
|
+
if (!existsSync(command)) {
|
|
194
|
+
throw new TypeError(
|
|
195
|
+
'Vite frontend requires the Domain project to install vite as a development dependency.',
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
return { command, prefix: [] }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function runViteBuild(input: ViteBuildInput): Promise<void> {
|
|
202
|
+
const command = viteCommand(input.projectDir)
|
|
203
|
+
await runProcess(
|
|
204
|
+
command.command,
|
|
205
|
+
[
|
|
206
|
+
...command.prefix,
|
|
207
|
+
'build',
|
|
208
|
+
'--outDir',
|
|
209
|
+
input.output,
|
|
210
|
+
'--emptyOutDir',
|
|
211
|
+
...(input.config === undefined ? [] : ['--config', input.config]),
|
|
212
|
+
],
|
|
213
|
+
input.projectDir,
|
|
214
|
+
input.onLog,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runProcess(
|
|
302
219
|
command: string,
|
|
303
220
|
args: readonly string[],
|
|
304
221
|
cwd: string,
|
|
@@ -318,7 +235,64 @@ function runBuild(
|
|
|
318
235
|
child.on('exit', (code) =>
|
|
319
236
|
code === 0
|
|
320
237
|
? resolveBuild()
|
|
321
|
-
: reject(
|
|
238
|
+
: reject(
|
|
239
|
+
new Error(`Vite frontend build failed (code ${code ?? 1}):\n${output.slice(-1500)}`),
|
|
240
|
+
),
|
|
322
241
|
)
|
|
323
242
|
})
|
|
324
243
|
}
|
|
244
|
+
|
|
245
|
+
function ephemeralPort(host: string): Promise<number> {
|
|
246
|
+
return new Promise((resolvePort, reject) => {
|
|
247
|
+
const server = createServer()
|
|
248
|
+
server.once('error', reject)
|
|
249
|
+
server.listen(0, host, () => {
|
|
250
|
+
const address = server.address()
|
|
251
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0
|
|
252
|
+
server.close((error) => (error === undefined ? resolvePort(port) : reject(error)))
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function waitUntilReady(
|
|
258
|
+
url: string,
|
|
259
|
+
child: ChildProcess,
|
|
260
|
+
output: readonly string[],
|
|
261
|
+
): Promise<void> {
|
|
262
|
+
const deadline = Date.now() + 15_000
|
|
263
|
+
while (Date.now() < deadline) {
|
|
264
|
+
if (child.exitCode !== null) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Vite frontend dev server exited before becoming ready (code ${child.exitCode}).\n${output.join('\n')}`,
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(500) })
|
|
271
|
+
await response.body?.cancel()
|
|
272
|
+
return
|
|
273
|
+
} catch {
|
|
274
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 50))
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
throw new Error(`Vite frontend dev server did not become ready at ${url}.\n${output.join('\n')}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stopChild(child: ChildProcess): Promise<void> {
|
|
281
|
+
if (child.exitCode !== null || child.killed) return Promise.resolve()
|
|
282
|
+
return new Promise((resolveStop) => {
|
|
283
|
+
let settled = false
|
|
284
|
+
const finish = () => {
|
|
285
|
+
if (settled) return
|
|
286
|
+
settled = true
|
|
287
|
+
clearTimeout(timer)
|
|
288
|
+
resolveStop()
|
|
289
|
+
}
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
if (child.exitCode === null) child.kill('SIGKILL')
|
|
292
|
+
finish()
|
|
293
|
+
}, 2_000)
|
|
294
|
+
timer.unref?.()
|
|
295
|
+
child.once('exit', finish)
|
|
296
|
+
child.kill('SIGTERM')
|
|
297
|
+
})
|
|
298
|
+
}
|
package/src/build/index.ts
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
export {
|
|
14
14
|
buildFrontend,
|
|
15
15
|
FRONTEND_STAGE_DIR,
|
|
16
|
-
|
|
17
|
-
type
|
|
18
|
-
type
|
|
19
|
-
type
|
|
16
|
+
startFrontendDev,
|
|
17
|
+
type FrontendDevHandle,
|
|
18
|
+
type ViteBuild,
|
|
19
|
+
type ViteBuildInput,
|
|
20
20
|
} from './frontend.js'
|
|
21
21
|
export { logTo, prepare } from './prepare.js'
|
|
22
22
|
export { buildCloudflareWorker, type CloudflareWorkerBuild } from './worker.js'
|
package/src/build/prepare.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { generateWranglerConfig } from './codegen/wrangler.js'
|
|
|
17
17
|
|
|
18
18
|
const DEFAULT_PORT = 8787
|
|
19
19
|
const DEPLOYMENT_VERSION_BINDING = 'ASTRALE_DEPLOYMENT_VERSION'
|
|
20
|
+
const FRONTEND_DEV_BINDING = 'FRONTEND_DEV_URL'
|
|
20
21
|
const WORKER_NAME_RE = /^[a-z0-9][a-z0-9-]*$/
|
|
21
22
|
|
|
22
23
|
/** Prepare immutable generated Worker sources and Wrangler configuration. */
|
|
@@ -25,12 +26,23 @@ export async function prepare(
|
|
|
25
26
|
ctx: WatchCtx | DeployCtx | BuildCtx,
|
|
26
27
|
mode: 'build' | 'dev' | 'deploy',
|
|
27
28
|
frontend?: CompiledFrontendArtifact,
|
|
29
|
+
frontendDevUrl?: string,
|
|
28
30
|
): Promise<{ configPath: string; fallbackConfigPath?: string; port: number; workerName: string }> {
|
|
29
31
|
if (Object.hasOwn(params.vars ?? {}, DEPLOYMENT_VERSION_BINDING)) {
|
|
30
32
|
throw new Error(
|
|
31
33
|
`cloudflare adapter: \`${DEPLOYMENT_VERSION_BINDING}\` is adapter-managed and cannot be set in \`vars\`.`,
|
|
32
34
|
)
|
|
33
35
|
}
|
|
36
|
+
if (Object.hasOwn(params.vars ?? {}, FRONTEND_DEV_BINDING)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`cloudflare adapter: \`${FRONTEND_DEV_BINDING}\` is adapter-managed and cannot be set in \`vars\`.`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (frontendDevUrl !== undefined && (mode !== 'dev' || !localFrontendDefinition(ctx))) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
'cloudflare adapter: frontendDevUrl requires a local Vite frontend in dev mode.',
|
|
44
|
+
)
|
|
45
|
+
}
|
|
34
46
|
const astraleDir = join(ctx.projectDir, '.astrale')
|
|
35
47
|
await mkdir(astraleDir, { recursive: true })
|
|
36
48
|
await ensureIdentity(astraleDir, ctx.definition.schema.origin)
|
|
@@ -39,9 +51,14 @@ export async function prepare(
|
|
|
39
51
|
const router = resolveRouter(params.router)
|
|
40
52
|
const localFrontend =
|
|
41
53
|
ctx.definition.frontend !== undefined && ctx.definition.frontend.source.kind !== 'external'
|
|
42
|
-
|
|
54
|
+
const developmentFrontend = mode === 'dev' && frontendDevUrl !== undefined
|
|
55
|
+
if (
|
|
56
|
+
!developmentFrontend &&
|
|
57
|
+
localFrontend !== (frontend !== undefined && frontend.artifact.source.kind !== 'external')
|
|
58
|
+
) {
|
|
43
59
|
throw new TypeError('cloudflare adapter: materialized frontend must match definition.frontend.')
|
|
44
60
|
}
|
|
61
|
+
if (developmentFrontend) await mkdir(join(astraleDir, 'frontend'), { recursive: true })
|
|
45
62
|
|
|
46
63
|
await writeFileAtomic(
|
|
47
64
|
join(astraleDir, 'worker.gen.ts'),
|
|
@@ -67,6 +84,7 @@ export async function prepare(
|
|
|
67
84
|
: undefined
|
|
68
85
|
const vars = {
|
|
69
86
|
...(workerUrl ? { WORKER_URL: workerUrl } : {}),
|
|
87
|
+
...(frontendDevUrl === undefined ? {} : { FRONTEND_DEV_URL: frontendDevUrl }),
|
|
70
88
|
...(mode === 'dev'
|
|
71
89
|
? { ...params.vars, ...('secrets' in ctx ? ctx.secrets : {}) }
|
|
72
90
|
: { ...params.vars }),
|
|
@@ -79,6 +97,7 @@ export async function prepare(
|
|
|
79
97
|
frontend: {
|
|
80
98
|
directory: './frontend',
|
|
81
99
|
routes: localFrontendRoutes(ctx.definition),
|
|
100
|
+
dev: frontendDevUrl !== undefined,
|
|
82
101
|
},
|
|
83
102
|
}
|
|
84
103
|
: {}),
|
|
@@ -112,11 +131,20 @@ function localFrontendRoutes(
|
|
|
112
131
|
): readonly string[] {
|
|
113
132
|
const frontend = definition.frontend
|
|
114
133
|
if (frontend === undefined || frontend.source.kind === 'external') return Object.freeze([])
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
const routes = Object.values(frontend.routes as Readonly<Record<string, FrontendRoute>>)
|
|
135
|
+
.flatMap((route) =>
|
|
136
|
+
frontend.source.kind === 'vite' && frontend.source.spa && route.path !== '/'
|
|
137
|
+
? [route.path, `${route.path}/*`]
|
|
138
|
+
: route.path === '/'
|
|
139
|
+
? ['/*']
|
|
140
|
+
: [route.path],
|
|
141
|
+
)
|
|
142
|
+
.sort()
|
|
143
|
+
return Object.freeze(routes)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function localFrontendDefinition(ctx: WatchCtx | DeployCtx | BuildCtx): boolean {
|
|
147
|
+
return ctx.definition.frontend?.source.kind === 'vite'
|
|
120
148
|
}
|
|
121
149
|
|
|
122
150
|
async function writeFileAtomic(path: string, content: string): Promise<void> {
|
package/src/cloudflare.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { DeployFetch } from '@astrale-os/sdk/deployment'
|
|
|
14
14
|
|
|
15
15
|
import { defineAdapter } from '@astrale-os/sdk'
|
|
16
16
|
import { deployCheck } from '@astrale-os/sdk/deployment'
|
|
17
|
+
import { resolve } from 'node:path'
|
|
17
18
|
|
|
18
19
|
import type { CloudflareParams } from './params.js'
|
|
19
20
|
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
runWranglerDeploy,
|
|
27
28
|
runWranglerDev,
|
|
28
29
|
runWranglerTypes,
|
|
29
|
-
|
|
30
|
+
startFrontendDev,
|
|
30
31
|
} from './build/index.js'
|
|
31
32
|
|
|
32
33
|
export function cloudflare(
|
|
@@ -41,7 +42,7 @@ export function cloudflare(
|
|
|
41
42
|
runWranglerDeploy,
|
|
42
43
|
runWranglerDev,
|
|
43
44
|
runWranglerTypes,
|
|
44
|
-
|
|
45
|
+
startFrontendDev,
|
|
45
46
|
waitUntilLive,
|
|
46
47
|
})
|
|
47
48
|
}
|
|
@@ -55,7 +56,7 @@ interface CloudflareAdapterDependencies {
|
|
|
55
56
|
readonly runWranglerDeploy: typeof runWranglerDeploy
|
|
56
57
|
readonly runWranglerDev: typeof runWranglerDev
|
|
57
58
|
readonly runWranglerTypes: typeof runWranglerTypes
|
|
58
|
-
readonly
|
|
59
|
+
readonly startFrontendDev: typeof startFrontendDev
|
|
59
60
|
readonly waitUntilLive: typeof waitUntilLive
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -64,6 +65,7 @@ export function cloudflareWithDependencies(
|
|
|
64
65
|
envs: Record<string, CloudflareParams>,
|
|
65
66
|
dependencies: CloudflareAdapterDependencies,
|
|
66
67
|
): DomainAdapter<CloudflareParams> {
|
|
68
|
+
const frontendDevUrls = new Map<string, string>()
|
|
67
69
|
return defineAdapter<CloudflareParams>({
|
|
68
70
|
name: 'cloudflare',
|
|
69
71
|
envs,
|
|
@@ -78,44 +80,46 @@ export function cloudflareWithDependencies(
|
|
|
78
80
|
|
|
79
81
|
async watch(params, ctx) {
|
|
80
82
|
const onLog = dependencies.logTo()
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
onLog,
|
|
85
|
-
})
|
|
86
|
-
const { configPath, port } = await dependencies.prepare(params, ctx, 'dev', frontend)
|
|
87
|
-
const handle = await dependencies.runWranglerDev({
|
|
88
|
-
projectDir: ctx.projectDir,
|
|
89
|
-
configPath,
|
|
90
|
-
port,
|
|
91
|
-
remote: Boolean(params.remote),
|
|
92
|
-
autoPickPort: params.port === undefined,
|
|
93
|
-
...(params.host ? { ip: '0.0.0.0' } : {}),
|
|
94
|
-
onReload: ctx.onReload,
|
|
95
|
-
onLog,
|
|
96
|
-
})
|
|
97
|
-
let frontendWatch: ReturnType<typeof watchFrontend>
|
|
83
|
+
const projectKey = resolve(ctx.projectDir)
|
|
84
|
+
let frontendDev: Awaited<ReturnType<typeof startFrontendDev>>
|
|
85
|
+
let handle: Awaited<ReturnType<typeof runWranglerDev>> | undefined
|
|
98
86
|
try {
|
|
99
|
-
|
|
87
|
+
frontendDev = await dependencies.startFrontendDev({
|
|
100
88
|
definition: ctx.definition,
|
|
101
89
|
projectDir: ctx.projectDir,
|
|
102
90
|
onLog,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
})
|
|
92
|
+
if (frontendDev !== undefined) frontendDevUrls.set(projectKey, frontendDev.url)
|
|
93
|
+
const { configPath, port } = await dependencies.prepare(
|
|
94
|
+
params,
|
|
95
|
+
ctx,
|
|
96
|
+
'dev',
|
|
97
|
+
undefined,
|
|
98
|
+
frontendDev?.url,
|
|
99
|
+
)
|
|
100
|
+
handle = await dependencies.runWranglerDev({
|
|
101
|
+
projectDir: ctx.projectDir,
|
|
102
|
+
configPath,
|
|
103
|
+
port,
|
|
104
|
+
remote: Boolean(params.remote),
|
|
105
|
+
autoPickPort: params.port === undefined,
|
|
106
|
+
...(params.host ? { ip: '0.0.0.0' } : {}),
|
|
107
|
+
onReload: ctx.onReload,
|
|
108
|
+
onLog,
|
|
107
109
|
})
|
|
108
110
|
} catch (error) {
|
|
111
|
+
frontendDevUrls.delete(projectKey)
|
|
109
112
|
try {
|
|
110
|
-
await
|
|
113
|
+
await frontendDev?.stop()
|
|
111
114
|
} catch (cleanupError) {
|
|
112
115
|
throw new AggregateError(
|
|
113
116
|
[error, cleanupError],
|
|
114
|
-
'cloudflare
|
|
117
|
+
'cloudflare development startup failed and Vite cleanup also failed',
|
|
115
118
|
)
|
|
116
119
|
}
|
|
117
120
|
throw error
|
|
118
121
|
}
|
|
122
|
+
if (handle === undefined) throw new Error('cloudflare local Worker failed to start')
|
|
119
123
|
// With --host the actionable URL is the PUBLIC one (the tunnel/proxy
|
|
120
124
|
// front), not the local bind — print and spec-stamp that.
|
|
121
125
|
return {
|
|
@@ -123,8 +127,9 @@ export function cloudflareWithDependencies(
|
|
|
123
127
|
localUrl: handle.url,
|
|
124
128
|
async stop() {
|
|
125
129
|
const errors: unknown[] = []
|
|
130
|
+
frontendDevUrls.delete(projectKey)
|
|
126
131
|
try {
|
|
127
|
-
await
|
|
132
|
+
await frontendDev?.stop()
|
|
128
133
|
} catch (error) {
|
|
129
134
|
errors.push(error)
|
|
130
135
|
}
|
|
@@ -141,17 +146,16 @@ export function cloudflareWithDependencies(
|
|
|
141
146
|
}
|
|
142
147
|
},
|
|
143
148
|
|
|
144
|
-
// Config hot-regen
|
|
145
|
-
//
|
|
146
|
-
// them itself, so a `astrale.config.ts` edit lands without a restart.
|
|
147
|
-
// Mirrors `watch`'s prep exactly (codegen + frontend build), minus the spawn.
|
|
149
|
+
// Config hot-regen rewrites generated files only. Wrangler reloads its config and entry;
|
|
150
|
+
// the author-owned Vite process keeps its own source/config watch lifecycle.
|
|
148
151
|
async regenerate(params, ctx) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const { configPath } = await dependencies.prepare(
|
|
153
|
+
params,
|
|
154
|
+
ctx,
|
|
155
|
+
'dev',
|
|
156
|
+
undefined,
|
|
157
|
+
frontendDevUrls.get(resolve(ctx.projectDir)),
|
|
158
|
+
)
|
|
155
159
|
await dependencies.runWranglerTypes({
|
|
156
160
|
projectDir: ctx.projectDir,
|
|
157
161
|
configPath,
|