@springbrand/space 0.1.0-alpha.1 → 0.1.0-alpha.2
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/package.json +1 -1
- package/src/space/deploy-engine.ts +246 -105
- package/src/space/durable-object.ts +44 -6
- package/src/space/workspace-port.ts +1 -1
package/package.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { Git } from "@cloudflare/shell/git"
|
|
2
2
|
import type { FileSystem } from "@cloudflare/shell"
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
createApp,
|
|
5
|
+
createWorker,
|
|
6
|
+
inferContentType,
|
|
7
|
+
isTextContentType,
|
|
8
|
+
type AssetConfig,
|
|
9
|
+
type Modules,
|
|
10
|
+
} from "@cloudflare/worker-bundler"
|
|
5
11
|
import { globInfos } from "./fileinfo"
|
|
12
|
+
import {
|
|
13
|
+
parseWranglerConfig,
|
|
14
|
+
type ParsedWranglerConfig,
|
|
15
|
+
WranglerConfigError,
|
|
16
|
+
} from "./wrangler-config"
|
|
6
17
|
|
|
7
18
|
// ─── Deploy Engine ──────────────────────────────────────────────────────────
|
|
8
19
|
|
|
@@ -25,11 +36,25 @@ export interface BranchDeploymentBundle {
|
|
|
25
36
|
commitHash: string
|
|
26
37
|
mainModule: string
|
|
27
38
|
modules: Record<string, string | Record<string, unknown>>
|
|
28
|
-
assets: Record<string,
|
|
39
|
+
assets: Record<string, SerializedAsset>
|
|
29
40
|
assetConfig: AssetConfig | undefined
|
|
30
41
|
compatibilityDate: string
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
export type SerializedAsset = string | { base64: string }
|
|
45
|
+
type WebsiteAssets = Record<string, string | ArrayBuffer>
|
|
46
|
+
|
|
47
|
+
const DEFAULT_COMPATIBILITY_DATE = "2025-04-01"
|
|
48
|
+
const STATIC_APP_MAIN = ".springbrand/static-app.ts"
|
|
49
|
+
const STATIC_APP_SOURCE = `import { DurableObject } from "cloudflare:workers"
|
|
50
|
+
|
|
51
|
+
export class App extends DurableObject {
|
|
52
|
+
fetch() {
|
|
53
|
+
return new Response("Not Found", { status: 404 })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`
|
|
57
|
+
|
|
33
58
|
export async function handleDeployCommand(
|
|
34
59
|
ctx: DeployContext,
|
|
35
60
|
cmd: string,
|
|
@@ -74,45 +99,56 @@ export function normalizeAppRoot(appRoot?: string | null): string {
|
|
|
74
99
|
async function readBranchFiles(
|
|
75
100
|
ctx: DeployContext,
|
|
76
101
|
branch: string,
|
|
77
|
-
appRoot = ""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
appRoot = "",
|
|
103
|
+
revision?: string,
|
|
104
|
+
): Promise<{
|
|
105
|
+
commitHash: string
|
|
106
|
+
files: Record<string, string>
|
|
107
|
+
assets: WebsiteAssets
|
|
108
|
+
}> {
|
|
109
|
+
const log = await ctx.git.log({ ref: branch, depth: revision ? 1000 : 1 })
|
|
81
110
|
if (log.length === 0) {
|
|
82
111
|
throw new Error(`No commits found on branch "${branch}"`)
|
|
83
112
|
}
|
|
84
|
-
const commitHash =
|
|
113
|
+
const commitHash = revision
|
|
114
|
+
? log.find((entry) => entry.oid === revision)?.oid
|
|
115
|
+
: log[0].oid
|
|
116
|
+
if (!commitHash) {
|
|
117
|
+
throw new Error(`Revision "${revision}" was not found on branch "${branch}"`)
|
|
118
|
+
}
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
await ctx.git.checkout({ ref: branch, force: true })
|
|
120
|
+
await ctx.git.checkout({ ref: revision ? commitHash : branch, force: true })
|
|
88
121
|
|
|
89
|
-
// Read all files recursively (readDir is non-recursive, glob is)
|
|
90
|
-
const allFiles = await globInfos(ctx.fs, "**/*")
|
|
91
122
|
const files: Record<string, string> = {}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
const assets: WebsiteAssets = {}
|
|
124
|
+
try {
|
|
125
|
+
for (const fileInfo of await globInfos(ctx.fs, "**/*")) {
|
|
126
|
+
if (fileInfo.type !== "file") continue
|
|
127
|
+
// Skip git's object store and the ArtifactsFileSystem bookkeeping dir —
|
|
128
|
+
// neither is part of the app and must never ship in a deploy bundle.
|
|
129
|
+
if (fileInfo.path.startsWith("/.git/") || fileInfo.path === "/.git") continue
|
|
130
|
+
if (fileInfo.path.startsWith("/.afs/") || fileInfo.path === "/.afs") continue
|
|
131
|
+
|
|
132
|
+
let bytes: Uint8Array
|
|
133
|
+
try {
|
|
134
|
+
bytes = await ctx.fs.readFileBytes(fileInfo.path)
|
|
135
|
+
} catch {
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const path = fileInfo.path.startsWith("/") ? fileInfo.path.slice(1) : fileInfo.path
|
|
139
|
+
if (appRoot) {
|
|
140
|
+
if (path !== appRoot && !path.startsWith(`${appRoot}/`)) continue
|
|
141
|
+
const relativePath = path.slice(appRoot.length + 1)
|
|
142
|
+
addWebsiteFile(files, assets, relativePath, bytes)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
addWebsiteFile(files, assets, path, bytes)
|
|
111
146
|
}
|
|
112
|
-
|
|
147
|
+
} finally {
|
|
148
|
+
if (revision) await ctx.git.checkout({ ref: branch, force: true })
|
|
113
149
|
}
|
|
114
150
|
|
|
115
|
-
return { commitHash, files }
|
|
151
|
+
return { commitHash, files, assets }
|
|
116
152
|
}
|
|
117
153
|
|
|
118
154
|
// ─── Deploy a branch ────────────────────────────────────────────────────────
|
|
@@ -121,7 +157,11 @@ async function deployBranch(
|
|
|
121
157
|
ctx: DeployContext,
|
|
122
158
|
request: Request
|
|
123
159
|
): Promise<Response> {
|
|
124
|
-
const body = (await request.json()) as {
|
|
160
|
+
const body = (await request.json()) as {
|
|
161
|
+
branch: string
|
|
162
|
+
appRoot?: string
|
|
163
|
+
revision?: string
|
|
164
|
+
}
|
|
125
165
|
const branch = body.branch
|
|
126
166
|
if (!branch) {
|
|
127
167
|
return jsonResponse({ error: "branch is required" }, 400)
|
|
@@ -129,7 +169,7 @@ async function deployBranch(
|
|
|
129
169
|
|
|
130
170
|
let bundle: BranchDeploymentBundle
|
|
131
171
|
try {
|
|
132
|
-
bundle = await buildBranchDeployment(ctx, branch, body.appRoot)
|
|
172
|
+
bundle = await buildBranchDeployment(ctx, branch, body.appRoot, body.revision)
|
|
133
173
|
} catch (e) {
|
|
134
174
|
const message = e instanceof Error ? e.message : String(e)
|
|
135
175
|
const separator = message.indexOf(": ")
|
|
@@ -140,6 +180,7 @@ async function deployBranch(
|
|
|
140
180
|
}
|
|
141
181
|
|
|
142
182
|
const {
|
|
183
|
+
branch: deploymentId,
|
|
143
184
|
commitHash,
|
|
144
185
|
mainModule,
|
|
145
186
|
modules: serializedModules,
|
|
@@ -153,7 +194,7 @@ async function deployBranch(
|
|
|
153
194
|
`INSERT OR REPLACE INTO deployments
|
|
154
195
|
(branch, commit_hash, main_module, modules, assets, asset_config, compatibility_date, deployed_at)
|
|
155
196
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
156
|
-
|
|
197
|
+
deploymentId,
|
|
157
198
|
commitHash,
|
|
158
199
|
mainModule,
|
|
159
200
|
JSON.stringify(serializedModules),
|
|
@@ -164,7 +205,7 @@ async function deployBranch(
|
|
|
164
205
|
)
|
|
165
206
|
|
|
166
207
|
return jsonResponse({
|
|
167
|
-
branch,
|
|
208
|
+
branch: deploymentId,
|
|
168
209
|
commit_hash: commitHash,
|
|
169
210
|
main_module: mainModule,
|
|
170
211
|
has_assets: Object.keys(serializedAssets).length > 0,
|
|
@@ -176,10 +217,11 @@ async function deployBranch(
|
|
|
176
217
|
export async function buildBranchDeployment(
|
|
177
218
|
ctx: DeployContext,
|
|
178
219
|
branch: string,
|
|
179
|
-
appRoot?: string | null
|
|
220
|
+
appRoot?: string | null,
|
|
221
|
+
revision?: string,
|
|
180
222
|
): Promise<BranchDeploymentBundle> {
|
|
181
223
|
const root = normalizeAppRoot(appRoot)
|
|
182
|
-
const { commitHash, files } = await readBranchFiles(ctx, branch, root)
|
|
224
|
+
const { commitHash, files, assets } = await readBranchFiles(ctx, branch, root, revision)
|
|
183
225
|
if (Object.keys(files).length === 0) {
|
|
184
226
|
throw new Error(
|
|
185
227
|
root
|
|
@@ -188,77 +230,156 @@ export async function buildBranchDeployment(
|
|
|
188
230
|
)
|
|
189
231
|
}
|
|
190
232
|
|
|
191
|
-
let wranglerCfg
|
|
192
233
|
try {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
234
|
+
const result = await buildWebsite(files, assets)
|
|
235
|
+
return {
|
|
236
|
+
branch: revision ?? branch,
|
|
237
|
+
commitHash,
|
|
238
|
+
mainModule: result.mainModule,
|
|
239
|
+
modules: result.modules,
|
|
240
|
+
assets: serializeAssets(result.assets),
|
|
241
|
+
assetConfig: result.assetConfig,
|
|
242
|
+
compatibilityDate: result.compatibilityDate,
|
|
197
243
|
}
|
|
198
|
-
|
|
244
|
+
} catch (e) {
|
|
245
|
+
throw new Error(`Build failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
199
246
|
}
|
|
247
|
+
}
|
|
200
248
|
|
|
201
|
-
|
|
249
|
+
async function buildWebsite(
|
|
250
|
+
sourceFiles: Record<string, string>,
|
|
251
|
+
sourceAssets: WebsiteAssets,
|
|
252
|
+
) {
|
|
253
|
+
const { files, config } = prepareWebsite(sourceFiles, sourceAssets)
|
|
254
|
+
if (config.durableObjects?.length) {
|
|
202
255
|
throw new Error(
|
|
203
256
|
"Durable Object bindings are not allowed in wrangler.json: The platform runs your app as a single Durable Object (`export class App extends DurableObject` from your main module). Do not declare `durable_objects.bindings`."
|
|
204
257
|
)
|
|
205
258
|
}
|
|
206
259
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}),
|
|
221
|
-
...(wranglerCfg.assets?.htmlHandling && {
|
|
222
|
-
html_handling: wranglerCfg.assets.htmlHandling,
|
|
223
|
-
}),
|
|
224
|
-
}
|
|
225
|
-
: undefined
|
|
226
|
-
|
|
227
|
-
if (Object.keys(collectedAssets).length) {
|
|
228
|
-
const result = await createApp({
|
|
229
|
-
files,
|
|
230
|
-
assets: collectedAssets,
|
|
231
|
-
assetConfig,
|
|
232
|
-
server: wranglerCfg.main,
|
|
233
|
-
})
|
|
234
|
-
return {
|
|
235
|
-
branch,
|
|
236
|
-
commitHash,
|
|
237
|
-
mainModule: result.mainModule,
|
|
238
|
-
modules: serializeModules(result.modules),
|
|
239
|
-
assets: serializeAssets(result.assets),
|
|
240
|
-
assetConfig: result.assetConfig,
|
|
241
|
-
compatibilityDate: wranglerCfg.compatibilityDate ?? "2025-04-01",
|
|
260
|
+
const assetsDirectory = normalizeAssetsDirectory(config.assets?.directory)
|
|
261
|
+
const assets = assetsDirectory === null
|
|
262
|
+
? {}
|
|
263
|
+
: collectAssets(sourceAssets, assetsDirectory)
|
|
264
|
+
const assetConfig: AssetConfig | undefined = assetsDirectory === null
|
|
265
|
+
? undefined
|
|
266
|
+
: {
|
|
267
|
+
...(config.assets?.notFoundHandling && {
|
|
268
|
+
not_found_handling: config.assets.notFoundHandling,
|
|
269
|
+
}),
|
|
270
|
+
...(config.assets?.htmlHandling && {
|
|
271
|
+
html_handling: config.assets.htmlHandling,
|
|
272
|
+
}),
|
|
242
273
|
}
|
|
243
|
-
}
|
|
244
274
|
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
275
|
+
if (Object.keys(assets).length > 0) {
|
|
276
|
+
const result = await createApp({
|
|
277
|
+
files,
|
|
278
|
+
assets,
|
|
279
|
+
assetConfig,
|
|
280
|
+
server: config.main,
|
|
281
|
+
})
|
|
248
282
|
return {
|
|
249
|
-
branch,
|
|
250
|
-
commitHash,
|
|
251
283
|
mainModule: result.mainModule,
|
|
252
|
-
modules,
|
|
253
|
-
assets:
|
|
254
|
-
assetConfig,
|
|
255
|
-
compatibilityDate:
|
|
284
|
+
modules: serializeModules(result.modules),
|
|
285
|
+
assets: result.assets,
|
|
286
|
+
assetConfig: result.assetConfig,
|
|
287
|
+
compatibilityDate: config.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
|
|
256
288
|
}
|
|
257
|
-
}
|
|
258
|
-
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = await createWorker({ files, entryPoint: config.main })
|
|
292
|
+
const modules = serializeModules(result.modules)
|
|
293
|
+
modules["__STATIC_CONTENT_MANIFEST"] ??= { text: "{}" }
|
|
294
|
+
return {
|
|
295
|
+
mainModule: result.mainModule,
|
|
296
|
+
modules,
|
|
297
|
+
assets: {},
|
|
298
|
+
assetConfig,
|
|
299
|
+
compatibilityDate: config.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
|
|
259
300
|
}
|
|
260
301
|
}
|
|
261
302
|
|
|
303
|
+
function prepareWebsite(
|
|
304
|
+
files: Record<string, string>,
|
|
305
|
+
assets: WebsiteAssets,
|
|
306
|
+
): { files: Record<string, string>; config: ParsedWranglerConfig } {
|
|
307
|
+
let config: ParsedWranglerConfig
|
|
308
|
+
try {
|
|
309
|
+
config = parseWranglerConfig(files)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error instanceof WranglerConfigError) {
|
|
312
|
+
throw new Error(`Invalid wrangler.json: ${error.message}`)
|
|
313
|
+
}
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
if (hasWrangler(files) || config.main || config.assets?.directory) return { files, config }
|
|
317
|
+
|
|
318
|
+
const directory = assets["index.html"] !== undefined
|
|
319
|
+
? "."
|
|
320
|
+
: assets["public/index.html"] !== undefined
|
|
321
|
+
? "public"
|
|
322
|
+
: null
|
|
323
|
+
if (directory === null) return { files, config }
|
|
324
|
+
|
|
325
|
+
const generatedAssets = {
|
|
326
|
+
directory,
|
|
327
|
+
htmlHandling: "auto-trailing-slash" as const,
|
|
328
|
+
notFoundHandling: "404-page" as const,
|
|
329
|
+
}
|
|
330
|
+
config = {
|
|
331
|
+
main: STATIC_APP_MAIN,
|
|
332
|
+
compatibilityDate: DEFAULT_COMPATIBILITY_DATE,
|
|
333
|
+
assets: generatedAssets,
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
files: {
|
|
337
|
+
...files,
|
|
338
|
+
"wrangler.json": JSON.stringify({
|
|
339
|
+
main: config.main,
|
|
340
|
+
compatibility_date: config.compatibilityDate,
|
|
341
|
+
assets: {
|
|
342
|
+
directory: generatedAssets.directory,
|
|
343
|
+
html_handling: generatedAssets.htmlHandling,
|
|
344
|
+
not_found_handling: generatedAssets.notFoundHandling,
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
[STATIC_APP_MAIN]: STATIC_APP_SOURCE,
|
|
348
|
+
},
|
|
349
|
+
config,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function hasWrangler(files: Record<string, string>): boolean {
|
|
354
|
+
return ["wrangler.json", "wrangler.jsonc", "wrangler.toml"]
|
|
355
|
+
.some((path) => files[path] !== undefined)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function normalizeAssetsDirectory(directory?: string): string | null {
|
|
359
|
+
if (directory === undefined) return null
|
|
360
|
+
const normalized = directory.replaceAll("\\", "/").replace(/^\.\//u, "").replace(/\/$/u, "")
|
|
361
|
+
return normalized === "." ? "" : normalized
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function collectAssets(assets: WebsiteAssets, directory: string): WebsiteAssets {
|
|
365
|
+
return Object.fromEntries(
|
|
366
|
+
Object.entries(assets)
|
|
367
|
+
.filter(([path]) => !directory || path === directory || path.startsWith(`${directory}/`))
|
|
368
|
+
.map(([path, content]) => [
|
|
369
|
+
directory ? `/${path.slice(directory.length + 1)}` : `/${path}`,
|
|
370
|
+
content,
|
|
371
|
+
]),
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function serializeModules(modules: Modules): Record<string, string | Record<string, unknown>> {
|
|
376
|
+
const out: Record<string, string | Record<string, unknown>> = {}
|
|
377
|
+
for (const [name, value] of Object.entries(modules)) {
|
|
378
|
+
out[name] = typeof value === "string" ? value : (value as Record<string, unknown>)
|
|
379
|
+
}
|
|
380
|
+
return out
|
|
381
|
+
}
|
|
382
|
+
|
|
262
383
|
// ─── Get a deployment ───────────────────────────────────────────────────────
|
|
263
384
|
|
|
264
385
|
async function getDeployment(
|
|
@@ -341,23 +462,43 @@ async function undeployBranch(
|
|
|
341
462
|
|
|
342
463
|
// ─── Serialization helpers ───────────────────────────────────────────────────
|
|
343
464
|
|
|
344
|
-
function serializeModules(modules: Modules): Record<string, string | Record<string, unknown>> {
|
|
345
|
-
const out: Record<string, string | Record<string, unknown>> = {}
|
|
346
|
-
for (const [name, value] of Object.entries(modules)) {
|
|
347
|
-
out[name] = typeof value === "string" ? value : (value as Record<string, unknown>)
|
|
348
|
-
}
|
|
349
|
-
return out
|
|
350
|
-
}
|
|
351
|
-
|
|
352
465
|
function serializeAssets(
|
|
353
466
|
assets: Record<string, string | ArrayBuffer>
|
|
354
|
-
): Record<string,
|
|
355
|
-
const out: Record<string,
|
|
467
|
+
): Record<string, SerializedAsset> {
|
|
468
|
+
const out: Record<string, SerializedAsset> = {}
|
|
356
469
|
for (const [path, content] of Object.entries(assets)) {
|
|
357
|
-
// Store text as-is; encode binary as base64
|
|
358
470
|
out[path] = typeof content === "string"
|
|
359
471
|
? content
|
|
360
|
-
:
|
|
472
|
+
: { base64: bytesToBase64(new Uint8Array(content)) }
|
|
361
473
|
}
|
|
362
474
|
return out
|
|
363
475
|
}
|
|
476
|
+
|
|
477
|
+
function addWebsiteFile(
|
|
478
|
+
files: Record<string, string>,
|
|
479
|
+
assets: WebsiteAssets,
|
|
480
|
+
path: string,
|
|
481
|
+
bytes: Uint8Array,
|
|
482
|
+
): void {
|
|
483
|
+
const contentType = inferContentType(path)
|
|
484
|
+
const binary = contentType !== undefined && !isTextContentType(contentType)
|
|
485
|
+
if (binary) {
|
|
486
|
+
assets[path] = exactBuffer(bytes)
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
const text = new TextDecoder().decode(bytes)
|
|
490
|
+
files[path] = text
|
|
491
|
+
assets[path] = text
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function exactBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
495
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
499
|
+
let binary = ""
|
|
500
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
501
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000))
|
|
502
|
+
}
|
|
503
|
+
return btoa(binary)
|
|
504
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
handleDeployCommand,
|
|
8
8
|
type BranchDeploymentBundle,
|
|
9
9
|
type DeployContext,
|
|
10
|
+
type SerializedAsset,
|
|
10
11
|
} from "./deploy-engine"
|
|
11
12
|
import { globInfos, readDirInfos, toFileInfo } from "./fileinfo"
|
|
12
13
|
import { handleAssetRequest, buildAssetManifest, createMemoryStorage, type AssetConfig } from "@cloudflare/worker-bundler"
|
|
@@ -93,11 +94,37 @@ interface DeploymentRow {
|
|
|
93
94
|
commitHash: string
|
|
94
95
|
mainModule: string
|
|
95
96
|
modules: Record<string, string | Record<string, unknown>>
|
|
96
|
-
assets: Record<string, string>
|
|
97
|
+
assets: Record<string, string | ArrayBuffer>
|
|
97
98
|
assetConfig: AssetConfig
|
|
98
99
|
compatibilityDate: string
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
function deserializeAssets(
|
|
103
|
+
assets: Record<string, SerializedAsset>,
|
|
104
|
+
): Record<string, string | ArrayBuffer> {
|
|
105
|
+
return Object.fromEntries(
|
|
106
|
+
Object.entries(assets).map(([path, content]) => [
|
|
107
|
+
path,
|
|
108
|
+
typeof content === "string"
|
|
109
|
+
? content
|
|
110
|
+
: exactArrayBuffer(base64ToBytes(content.base64)),
|
|
111
|
+
]),
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function exactArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
116
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function base64ToBytes(value: string): Uint8Array {
|
|
120
|
+
const binary = atob(value)
|
|
121
|
+
const bytes = new Uint8Array(binary.length)
|
|
122
|
+
for (let index = 0; index < binary.length; index++) {
|
|
123
|
+
bytes[index] = binary.charCodeAt(index)
|
|
124
|
+
}
|
|
125
|
+
return bytes
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
// Overlay-only paths that must never leak into a deploy, rollback tree, or any
|
|
102
129
|
// file listing: git's object store and the ArtifactsFileSystem bookkeeping dir.
|
|
103
130
|
const isReservedPath = isReservedSpacePath
|
|
@@ -1059,7 +1086,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1059
1086
|
* analysis Space with a spreadsheet and a script deploys nothing until
|
|
1060
1087
|
* someone names the directory that holds a site.
|
|
1061
1088
|
*/
|
|
1062
|
-
async deploy(branch: string, appRoot?: string): Promise<unknown> {
|
|
1089
|
+
async deploy(branch: string, appRoot?: string, revision?: string): Promise<unknown> {
|
|
1063
1090
|
await this.ensureInit()
|
|
1064
1091
|
return this.lane(async () => {
|
|
1065
1092
|
// The deploy engine reads the full branch tree, so ensure the Artifacts
|
|
@@ -1069,7 +1096,11 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1069
1096
|
const fakeRequest = new Request("http://internal/?cmd=deploy", {
|
|
1070
1097
|
method: "POST",
|
|
1071
1098
|
headers: { "Content-Type": "application/json" },
|
|
1072
|
-
body: JSON.stringify({
|
|
1099
|
+
body: JSON.stringify({
|
|
1100
|
+
branch,
|
|
1101
|
+
...(appRoot ? { appRoot } : {}),
|
|
1102
|
+
...(revision ? { revision } : {}),
|
|
1103
|
+
}),
|
|
1073
1104
|
})
|
|
1074
1105
|
const ctx: DeployContext = {
|
|
1075
1106
|
sql: this.ctx.storage.sql,
|
|
@@ -1078,9 +1109,12 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1078
1109
|
}
|
|
1079
1110
|
const res = await handleDeployCommand(ctx, "deploy", fakeRequest)
|
|
1080
1111
|
const data = await res.json() as Record<string, unknown>
|
|
1081
|
-
data.
|
|
1112
|
+
if (typeof data.error === "string") throw new Error(data.error)
|
|
1113
|
+
const deploymentId = data.branch as string
|
|
1114
|
+
data.preview_url =
|
|
1115
|
+
`/space/${this.spaceName}/preview/${encodeURIComponent(deploymentId)}/`
|
|
1082
1116
|
|
|
1083
|
-
if (!
|
|
1117
|
+
if (!revision) await this.backend.push(branch)
|
|
1084
1118
|
|
|
1085
1119
|
return data
|
|
1086
1120
|
})
|
|
@@ -1089,6 +1123,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1089
1123
|
async getDeploymentBundle(
|
|
1090
1124
|
branch: string,
|
|
1091
1125
|
appRoot?: string,
|
|
1126
|
+
revision?: string,
|
|
1092
1127
|
): Promise<BranchDeploymentBundle> {
|
|
1093
1128
|
await this.ensureInit()
|
|
1094
1129
|
await this.materializeAll()
|
|
@@ -1100,6 +1135,7 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1100
1135
|
},
|
|
1101
1136
|
branch,
|
|
1102
1137
|
appRoot,
|
|
1138
|
+
revision,
|
|
1103
1139
|
)
|
|
1104
1140
|
}
|
|
1105
1141
|
|
|
@@ -1179,7 +1215,9 @@ export class SpaceDO extends DurableObject<Env>
|
|
|
1179
1215
|
commitHash: r.commit_hash as string,
|
|
1180
1216
|
mainModule: r.main_module as string,
|
|
1181
1217
|
modules: JSON.parse(r.modules as string) as Record<string, string | Record<string, unknown>>,
|
|
1182
|
-
assets:
|
|
1218
|
+
assets: deserializeAssets(
|
|
1219
|
+
JSON.parse((r.assets as string) || "{}") as Record<string, SerializedAsset>,
|
|
1220
|
+
),
|
|
1183
1221
|
assetConfig: JSON.parse((r.asset_config as string) || "{}") as AssetConfig,
|
|
1184
1222
|
compatibilityDate: (r.compatibility_date as string) || FALLBACK_COMPATIBILITY_DATE,
|
|
1185
1223
|
}
|
|
@@ -234,7 +234,7 @@ export interface SpaceControlPort {
|
|
|
234
234
|
* and only an explicit deploy brings an app into existence.
|
|
235
235
|
*/
|
|
236
236
|
export interface SpaceAppPort {
|
|
237
|
-
deploy(branch: string, appRoot?: string): Promise<unknown>;
|
|
237
|
+
deploy(branch: string, appRoot?: string, revision?: string): Promise<unknown>;
|
|
238
238
|
undeploy(branch: string): Promise<unknown>;
|
|
239
239
|
listDeployments(): Promise<unknown>;
|
|
240
240
|
getDeployment(branch: string): Promise<unknown>;
|