@embeddables/deploy 0.0.0-stage → 0.2.0
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 +22 -2
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +282 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -3
package/README.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @embeddables/deploy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Upload a built Worker app to Embeddables Hosting with a project deploy key.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { deployHostingApp } from '@embeddables/deploy'
|
|
7
|
+
|
|
8
|
+
const result = await deployHostingApp({
|
|
9
|
+
deployKey: process.env.EMBEDDABLES_DEPLOY_KEY!,
|
|
10
|
+
appSlug: 'storefront',
|
|
11
|
+
branch: 'main',
|
|
12
|
+
sha: process.env.GITHUB_SHA!,
|
|
13
|
+
serverDir: 'build/server',
|
|
14
|
+
clientDir: 'build/client',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
console.log(result.branchUrl, result.commitUrl)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Only static files the backend does not already have are uploaded, so redeploying an unchanged app
|
|
21
|
+
sends no static file bytes. Each static file must be 25 MB or less.
|
|
22
|
+
|
|
23
|
+
Failures throw `DeployError` with the backend's `status` and `code` when it returned them.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/deploy.d.ts
|
|
2
|
+
declare const DEFAULT_API_ORIGIN = "https://backend.embeddables.com";
|
|
3
|
+
interface DeployHostingAppOptions {
|
|
4
|
+
/** Project deploy key (`edk_…`). */
|
|
5
|
+
deployKey: string;
|
|
6
|
+
appSlug: string;
|
|
7
|
+
branch: string;
|
|
8
|
+
sha: string;
|
|
9
|
+
/** ISO 8601 commit time. Keeps overlapping deploys from moving an alias back to an older commit. */
|
|
10
|
+
committedAt?: string;
|
|
11
|
+
/** Worker modules. `wrangler.json` here sets the main module, compatibility date, flags, and vars. */
|
|
12
|
+
serverDir: string;
|
|
13
|
+
/** Static files. Omit for an app with none. */
|
|
14
|
+
clientDir?: string;
|
|
15
|
+
/** When set, the backend checks that it owns the deploy key. */
|
|
16
|
+
projectId?: string;
|
|
17
|
+
apiOrigin?: string;
|
|
18
|
+
/** Asset batches uploaded at once. */
|
|
19
|
+
concurrency?: number;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
/** Wait between batch retries; replaced in tests. */
|
|
22
|
+
sleep?: (ms: number) => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
interface DeployPromotion {
|
|
25
|
+
status: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
interface DeployHostingAppResult {
|
|
29
|
+
scriptName: string;
|
|
30
|
+
branchUrl: string;
|
|
31
|
+
commitUrl: string;
|
|
32
|
+
appSlug: string;
|
|
33
|
+
projectSlug: string;
|
|
34
|
+
ref: string;
|
|
35
|
+
/** Null when the branch is not the app's production branch. */
|
|
36
|
+
promotion: DeployPromotion | null;
|
|
37
|
+
/** True when this deploy registered the app. Surface it: a misspelled slug registers a new app. */
|
|
38
|
+
appRegistered?: true;
|
|
39
|
+
/** Static files in the manifest. */
|
|
40
|
+
totalFiles: number;
|
|
41
|
+
/** Distinct files sent; the rest were already stored from earlier deploys. */
|
|
42
|
+
uploadedFiles: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Uploads a built app in three steps: a manifest of static files, the files
|
|
46
|
+
* the backend does not already have (in parallel batches), then the server
|
|
47
|
+
* modules. The same commit can be redeployed safely; unchanged files upload
|
|
48
|
+
* nothing.
|
|
49
|
+
*/
|
|
50
|
+
declare function deployHostingApp(options: DeployHostingAppOptions): Promise<DeployHostingAppResult>;
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/errors.d.ts
|
|
53
|
+
/** A deploy step failed. `status` and `code` come from the backend's problem response when it sent one. */
|
|
54
|
+
declare class DeployError extends Error {
|
|
55
|
+
readonly status: number | null;
|
|
56
|
+
readonly code: string | null;
|
|
57
|
+
constructor(message: string, options?: {
|
|
58
|
+
status?: number;
|
|
59
|
+
code?: string;
|
|
60
|
+
cause?: unknown;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/files.d.ts
|
|
65
|
+
/** Cloudflare's hard per-file limit for static assets. */
|
|
66
|
+
declare const MAX_STATIC_ASSET_BYTES: number;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { DEFAULT_API_ORIGIN, DeployError, type DeployHostingAppOptions, type DeployHostingAppResult, type DeployPromotion, MAX_STATIC_ASSET_BYTES, deployHostingApp };
|
|
69
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/deploy.ts","../src/errors.ts","../src/files.ts"],"mappings":";cASa;UAKI;;EAEf;EACA;EACA;EACA;;EAEA;;EAEA;;EAEA;;EAEA;EACA;;EAEA;EACA,eAAe;;EAEf,SAAS,eAAe;;UAGT;EACf;GACC;;UAGc;EACf;EACA;EACA;EACA;EACA;EACA;;EAEA,WAAW;;EAEX;;EAEA;;EAEA;;;;;;;;iBAeoB,iBACpB,SAAS,0BACR,QAAQ;;;;cCvEE,oBAAoB;WACtB;WACA;EAEG,YAAA,iBAAiB;IAAW;IAAiB;IAAe;;;;;;cCE7D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
import { gzipSync } from "node:zlib";
|
|
5
|
+
//#region src/batches.ts
|
|
6
|
+
const PART_OVERHEAD_BYTES = 512;
|
|
7
|
+
/**
|
|
8
|
+
* Splits the backend's buckets of missing content hashes into requests that
|
|
9
|
+
* each stay under the batch limit. A single file is at most 25 MB, so it
|
|
10
|
+
* always fits in a batch of its own.
|
|
11
|
+
*/
|
|
12
|
+
function planBatches(buckets, sizes) {
|
|
13
|
+
const budget = 31195136;
|
|
14
|
+
const batches = [];
|
|
15
|
+
for (const bucket of buckets) {
|
|
16
|
+
let current = [];
|
|
17
|
+
let used = 0;
|
|
18
|
+
for (const hash of bucket) {
|
|
19
|
+
const size = sizes.get(hash);
|
|
20
|
+
if (size === void 0) throw new Error(`Backend asked for unknown file ${hash}.`);
|
|
21
|
+
const cost = size + PART_OVERHEAD_BYTES;
|
|
22
|
+
if (current.length > 0 && used + cost > budget) {
|
|
23
|
+
batches.push(current);
|
|
24
|
+
current = [];
|
|
25
|
+
used = 0;
|
|
26
|
+
}
|
|
27
|
+
current.push(hash);
|
|
28
|
+
used += cost;
|
|
29
|
+
}
|
|
30
|
+
if (current.length > 0) batches.push(current);
|
|
31
|
+
}
|
|
32
|
+
return batches;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/errors.ts
|
|
36
|
+
/** A deploy step failed. `status` and `code` come from the backend's problem response when it sent one. */
|
|
37
|
+
var DeployError = class extends Error {
|
|
38
|
+
status;
|
|
39
|
+
code;
|
|
40
|
+
constructor(message, options = {}) {
|
|
41
|
+
super(message, { cause: options.cause });
|
|
42
|
+
this.name = "DeployError";
|
|
43
|
+
this.status = options.status ?? null;
|
|
44
|
+
this.code = options.code ?? null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/files.ts
|
|
49
|
+
/** Cloudflare's hard per-file limit for static assets. */
|
|
50
|
+
const MAX_STATIC_ASSET_BYTES = 26214400;
|
|
51
|
+
/** Every regular file under `root`, as posix paths relative to it, sorted. Symlinks are skipped. */
|
|
52
|
+
async function listFiles(root) {
|
|
53
|
+
const found = [];
|
|
54
|
+
async function walk(dir) {
|
|
55
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
56
|
+
const absolute = join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory()) await walk(absolute);
|
|
58
|
+
else if (entry.isFile()) found.push(relative(root, absolute).split(sep).join("/"));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await walk(root);
|
|
62
|
+
return found.sort();
|
|
63
|
+
}
|
|
64
|
+
/** Hashes one file at a time, so memory stays at the size of the largest file. */
|
|
65
|
+
async function readStaticFiles(clientDir) {
|
|
66
|
+
const files = [];
|
|
67
|
+
for (const path of await listFiles(clientDir)) {
|
|
68
|
+
const absolutePath = join(clientDir, path);
|
|
69
|
+
const bytes = await readFile(absolutePath);
|
|
70
|
+
if (bytes.byteLength > 26214400) throw new DeployError(`Static file ${path} is larger than 25 MB. Serve large media from R2 instead.`, { code: "validation.file_too_large" });
|
|
71
|
+
files.push({
|
|
72
|
+
path: `/${path}`,
|
|
73
|
+
absolutePath,
|
|
74
|
+
contentHash: createHash("sha256").update(bytes).digest("hex"),
|
|
75
|
+
size: bytes.byteLength
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/tar.ts
|
|
82
|
+
const BLOCK = 512;
|
|
83
|
+
const NAME_BYTES = 100;
|
|
84
|
+
const PREFIX_BYTES = 155;
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
/** A gzipped ustar archive. Paths over 100 bytes are split into the ustar `prefix` field. */
|
|
87
|
+
function gzipTar(entries) {
|
|
88
|
+
return new Uint8Array(gzipSync(tar(entries)));
|
|
89
|
+
}
|
|
90
|
+
function tar(entries) {
|
|
91
|
+
const size = entries.reduce((total, entry) => total + BLOCK + Math.ceil(entry.bytes.byteLength / BLOCK) * BLOCK, 2 * BLOCK);
|
|
92
|
+
const out = new Uint8Array(size);
|
|
93
|
+
let offset = 0;
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
out.set(header(entry.path, entry.bytes.byteLength), offset);
|
|
96
|
+
offset += BLOCK;
|
|
97
|
+
out.set(entry.bytes, offset);
|
|
98
|
+
offset += Math.ceil(entry.bytes.byteLength / BLOCK) * BLOCK;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
function header(path, size) {
|
|
103
|
+
const { name, prefix } = splitPath(path);
|
|
104
|
+
const block = new Uint8Array(BLOCK);
|
|
105
|
+
writeBytes(block, 0, name);
|
|
106
|
+
writeOctal(block, 100, 8, 420);
|
|
107
|
+
writeOctal(block, 108, 8, 0);
|
|
108
|
+
writeOctal(block, 116, 8, 0);
|
|
109
|
+
writeOctal(block, 124, 12, size);
|
|
110
|
+
writeOctal(block, 136, 12, 0);
|
|
111
|
+
block.fill(32, 148, 156);
|
|
112
|
+
block[156] = 48;
|
|
113
|
+
writeBytes(block, 257, encoder.encode("ustar\0"));
|
|
114
|
+
writeBytes(block, 263, encoder.encode("00"));
|
|
115
|
+
writeBytes(block, 345, prefix);
|
|
116
|
+
const checksum = block.reduce((total, byte) => total + byte, 0);
|
|
117
|
+
writeBytes(block, 148, encoder.encode(`${checksum.toString(8).padStart(6, "0")}\0 `));
|
|
118
|
+
return block;
|
|
119
|
+
}
|
|
120
|
+
function splitPath(path) {
|
|
121
|
+
const whole = encoder.encode(path);
|
|
122
|
+
if (whole.byteLength <= NAME_BYTES) return {
|
|
123
|
+
name: whole,
|
|
124
|
+
prefix: /* @__PURE__ */ new Uint8Array()
|
|
125
|
+
};
|
|
126
|
+
for (let index = path.lastIndexOf("/"); index > 0; index = path.lastIndexOf("/", index - 1)) {
|
|
127
|
+
const prefix = encoder.encode(path.slice(0, index));
|
|
128
|
+
const name = encoder.encode(path.slice(index + 1));
|
|
129
|
+
if (prefix.byteLength <= PREFIX_BYTES && name.byteLength <= NAME_BYTES && name.byteLength > 0) return {
|
|
130
|
+
name,
|
|
131
|
+
prefix
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
throw new DeployError(`Path ${path} is too long for a tar archive.`, { code: "validation.failed" });
|
|
135
|
+
}
|
|
136
|
+
function writeBytes(block, offset, bytes) {
|
|
137
|
+
block.set(bytes, offset);
|
|
138
|
+
}
|
|
139
|
+
function writeOctal(block, offset, length, value) {
|
|
140
|
+
writeBytes(block, offset, encoder.encode(`${value.toString(8).padStart(length - 1, "0")}\0`));
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/deploy.ts
|
|
144
|
+
const DEFAULT_API_ORIGIN = "https://backend.embeddables.com";
|
|
145
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
146
|
+
const REQUEST_ATTEMPTS = 3;
|
|
147
|
+
/**
|
|
148
|
+
* Uploads a built app in three steps: a manifest of static files, the files
|
|
149
|
+
* the backend does not already have (in parallel batches), then the server
|
|
150
|
+
* modules. The same commit can be redeployed safely; unchanged files upload
|
|
151
|
+
* nothing.
|
|
152
|
+
*/
|
|
153
|
+
async function deployHostingApp(options) {
|
|
154
|
+
const api = createApi(options);
|
|
155
|
+
const idempotencyKey = randomUUID();
|
|
156
|
+
const files = options.clientDir ? await readStaticFiles(options.clientDir) : [];
|
|
157
|
+
const server = await readServerArchive(options.serverDir);
|
|
158
|
+
const manifest = {};
|
|
159
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
manifest[file.path] = {
|
|
162
|
+
contentHash: file.contentHash,
|
|
163
|
+
size: file.size
|
|
164
|
+
};
|
|
165
|
+
if (!byHash.has(file.contentHash)) byHash.set(file.contentHash, {
|
|
166
|
+
absolutePath: file.absolutePath,
|
|
167
|
+
size: file.size
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const session = await api.retryingJson("/hosting/v1/deploys/sessions", {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
"content-type": "application/json",
|
|
174
|
+
"idempotency-key": idempotencyKey
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
appSlug: options.appSlug,
|
|
178
|
+
branch: options.branch,
|
|
179
|
+
sha: options.sha,
|
|
180
|
+
committedAt: options.committedAt,
|
|
181
|
+
manifest
|
|
182
|
+
})
|
|
183
|
+
});
|
|
184
|
+
const sizes = new Map([...byHash].map(([hash, file]) => [hash, file.size]));
|
|
185
|
+
const batches = planBatches(session.buckets, sizes);
|
|
186
|
+
const batchPath = `/hosting/v1/deploys/sessions/${encodeURIComponent(session.sessionId)}/assets`;
|
|
187
|
+
await runPool(batches, options.concurrency ?? DEFAULT_CONCURRENCY, async (batch) => {
|
|
188
|
+
const form = new FormData();
|
|
189
|
+
for (const hash of batch) {
|
|
190
|
+
const file = byHash.get(hash);
|
|
191
|
+
form.append(hash, new Blob([await readFile(file.absolutePath)]), hash);
|
|
192
|
+
}
|
|
193
|
+
await api.retrying(batchPath, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
body: form
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
...await api.retryingJson(`/hosting/v1/deploys/sessions/${encodeURIComponent(session.sessionId)}/finalize`, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "content-type": "application/gzip" },
|
|
202
|
+
body: server
|
|
203
|
+
}),
|
|
204
|
+
totalFiles: files.length,
|
|
205
|
+
uploadedFiles: batches.reduce((total, batch) => total + batch.length, 0)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function readServerArchive(serverDir) {
|
|
209
|
+
const paths = await listFiles(serverDir);
|
|
210
|
+
if (paths.length === 0) throw new DeployError(`No server modules found in ${serverDir}.`, { code: "validation.failed" });
|
|
211
|
+
return gzipTar(await Promise.all(paths.map(async (path) => ({
|
|
212
|
+
path: `server/${path}`,
|
|
213
|
+
bytes: new Uint8Array(await readFile(join(serverDir, path)))
|
|
214
|
+
}))));
|
|
215
|
+
}
|
|
216
|
+
function createApi(options) {
|
|
217
|
+
const origin = (options.apiOrigin ?? "https://backend.embeddables.com").replace(/\/+$/, "");
|
|
218
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
219
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
220
|
+
const auth = { authorization: `Bearer ${options.deployKey}` };
|
|
221
|
+
if (options.projectId) auth["x-project-id"] = options.projectId;
|
|
222
|
+
async function send(path, init) {
|
|
223
|
+
let response;
|
|
224
|
+
try {
|
|
225
|
+
response = await fetchImpl(`${origin}${path}`, {
|
|
226
|
+
...init,
|
|
227
|
+
headers: {
|
|
228
|
+
...auth,
|
|
229
|
+
...init.headers
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
throw new DeployError(`Could not reach ${origin}.`, { cause });
|
|
234
|
+
}
|
|
235
|
+
if (!response.ok) throw await problem(response);
|
|
236
|
+
return response;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
/** Retries network failures, 429, and 5xx for retry-safe deploy operations. */
|
|
240
|
+
async retrying(path, init) {
|
|
241
|
+
for (let attempt = 1;; attempt++) try {
|
|
242
|
+
return await send(path, init);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
const status = error instanceof DeployError ? error.status : null;
|
|
245
|
+
if (!(status === null || status === 429 || status >= 500) || attempt >= REQUEST_ATTEMPTS) throw error;
|
|
246
|
+
await sleep(500 * 3 ** (attempt - 1));
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
async retryingJson(path, init) {
|
|
250
|
+
return await (await this.retrying(path, init)).json();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function problem(response) {
|
|
255
|
+
let body = {};
|
|
256
|
+
try {
|
|
257
|
+
body = await response.json();
|
|
258
|
+
} catch {}
|
|
259
|
+
const detail = typeof body.detail === "string" ? body.detail : null;
|
|
260
|
+
const title = typeof body.title === "string" ? body.title : null;
|
|
261
|
+
return new DeployError(detail ?? title ?? `Deploy request failed with ${response.status}.`, {
|
|
262
|
+
status: response.status,
|
|
263
|
+
code: typeof body.code === "string" ? body.code : void 0
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function runPool(items, concurrency, task) {
|
|
267
|
+
let next = 0;
|
|
268
|
+
let failure = null;
|
|
269
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
|
|
270
|
+
while (!failure && next < items.length) try {
|
|
271
|
+
await task(items[next++]);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
failure ??= { error };
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
await Promise.all(workers);
|
|
277
|
+
if (failure) throw failure.error;
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { DEFAULT_API_ORIGIN, DeployError, MAX_STATIC_ASSET_BYTES, deployHostingApp };
|
|
281
|
+
|
|
282
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/batches.ts","../src/errors.ts","../src/files.ts","../src/tar.ts","../src/deploy.ts"],"sourcesContent":["/** The backend rejects an asset batch whose request body is over 30 MB. */\nexport const MAX_BATCH_BYTES = 30 * 1024 * 1024\n\n// * Multipart adds a boundary and part headers to every file; this leaves\n// room for them so a batch never trips the backend's Content-Length check.\nconst PART_OVERHEAD_BYTES = 512\nconst BATCH_HEADROOM_BYTES = 256 * 1024\n\n/**\n * Splits the backend's buckets of missing content hashes into requests that\n * each stay under the batch limit. A single file is at most 25 MB, so it\n * always fits in a batch of its own.\n */\nexport function planBatches(buckets: string[][], sizes: ReadonlyMap<string, number>): string[][] {\n const budget = MAX_BATCH_BYTES - BATCH_HEADROOM_BYTES\n const batches: string[][] = []\n for (const bucket of buckets) {\n let current: string[] = []\n let used = 0\n for (const hash of bucket) {\n const size = sizes.get(hash)\n if (size === undefined) throw new Error(`Backend asked for unknown file ${hash}.`)\n const cost = size + PART_OVERHEAD_BYTES\n if (current.length > 0 && used + cost > budget) {\n batches.push(current)\n current = []\n used = 0\n }\n current.push(hash)\n used += cost\n }\n if (current.length > 0) batches.push(current)\n }\n return batches\n}\n","/** A deploy step failed. `status` and `code` come from the backend's problem response when it sent one. */\nexport class DeployError extends Error {\n readonly status: number | null\n readonly code: string | null\n\n constructor(message: string, options: { status?: number; code?: string; cause?: unknown } = {}) {\n super(message, { cause: options.cause })\n this.name = 'DeployError'\n this.status = options.status ?? null\n this.code = options.code ?? null\n }\n}\n","import { createHash } from 'node:crypto'\nimport { readdir, readFile } from 'node:fs/promises'\nimport { join, relative, sep } from 'node:path'\n\nimport { DeployError } from './errors.js'\n\n/** Cloudflare's hard per-file limit for static assets. */\nexport const MAX_STATIC_ASSET_BYTES = 25 * 1024 * 1024\n\nexport interface StaticFile {\n /** Manifest path: posix, relative to the client directory, with a leading slash. */\n path: string\n absolutePath: string\n contentHash: string\n size: number\n}\n\n/** Every regular file under `root`, as posix paths relative to it, sorted. Symlinks are skipped. */\nexport async function listFiles(root: string): Promise<string[]> {\n const found: string[] = []\n async function walk(dir: string): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n const absolute = join(dir, entry.name)\n if (entry.isDirectory()) await walk(absolute)\n else if (entry.isFile()) found.push(relative(root, absolute).split(sep).join('/'))\n }\n }\n await walk(root)\n return found.sort()\n}\n\n/** Hashes one file at a time, so memory stays at the size of the largest file. */\nexport async function readStaticFiles(clientDir: string): Promise<StaticFile[]> {\n const files: StaticFile[] = []\n for (const path of await listFiles(clientDir)) {\n const absolutePath = join(clientDir, path)\n const bytes = await readFile(absolutePath)\n if (bytes.byteLength > MAX_STATIC_ASSET_BYTES) {\n throw new DeployError(\n `Static file ${path} is larger than 25 MB. Serve large media from R2 instead.`,\n { code: 'validation.file_too_large' },\n )\n }\n files.push({\n path: `/${path}`,\n absolutePath,\n contentHash: createHash('sha256').update(bytes).digest('hex'),\n size: bytes.byteLength,\n })\n }\n return files\n}\n","import { gzipSync } from 'node:zlib'\n\nimport { DeployError } from './errors.js'\n\nconst BLOCK = 512\nconst NAME_BYTES = 100\nconst PREFIX_BYTES = 155\nconst encoder = new TextEncoder()\n\nexport interface TarEntry {\n path: string\n bytes: Uint8Array\n}\n\n/** A gzipped ustar archive. Paths over 100 bytes are split into the ustar `prefix` field. */\nexport function gzipTar(entries: TarEntry[]): Uint8Array<ArrayBuffer> {\n return new Uint8Array(gzipSync(tar(entries)))\n}\n\nexport function tar(entries: TarEntry[]): Uint8Array {\n const size = entries.reduce(\n (total, entry) => total + BLOCK + Math.ceil(entry.bytes.byteLength / BLOCK) * BLOCK,\n 2 * BLOCK,\n )\n const out = new Uint8Array(size)\n let offset = 0\n for (const entry of entries) {\n out.set(header(entry.path, entry.bytes.byteLength), offset)\n offset += BLOCK\n out.set(entry.bytes, offset)\n offset += Math.ceil(entry.bytes.byteLength / BLOCK) * BLOCK\n }\n return out\n}\n\nfunction header(path: string, size: number): Uint8Array {\n const { name, prefix } = splitPath(path)\n const block = new Uint8Array(BLOCK)\n writeBytes(block, 0, name)\n writeOctal(block, 100, 8, 0o644)\n writeOctal(block, 108, 8, 0)\n writeOctal(block, 116, 8, 0)\n writeOctal(block, 124, 12, size)\n writeOctal(block, 136, 12, 0)\n block.fill(0x20, 148, 156)\n block[156] = 0x30\n writeBytes(block, 257, encoder.encode('ustar\\0'))\n writeBytes(block, 263, encoder.encode('00'))\n writeBytes(block, 345, prefix)\n const checksum = block.reduce((total, byte) => total + byte, 0)\n writeBytes(block, 148, encoder.encode(`${checksum.toString(8).padStart(6, '0')}\\0 `))\n return block\n}\n\nfunction splitPath(path: string): { name: Uint8Array; prefix: Uint8Array } {\n const whole = encoder.encode(path)\n if (whole.byteLength <= NAME_BYTES) return { name: whole, prefix: new Uint8Array() }\n // * Split at the last slash that still fits the prefix, leaving the longest prefix.\n for (let index = path.lastIndexOf('/'); index > 0; index = path.lastIndexOf('/', index - 1)) {\n const prefix = encoder.encode(path.slice(0, index))\n const name = encoder.encode(path.slice(index + 1))\n if (prefix.byteLength <= PREFIX_BYTES && name.byteLength <= NAME_BYTES && name.byteLength > 0) {\n return { name, prefix }\n }\n }\n throw new DeployError(`Path ${path} is too long for a tar archive.`, {\n code: 'validation.failed',\n })\n}\n\nfunction writeBytes(block: Uint8Array, offset: number, bytes: Uint8Array): void {\n block.set(bytes, offset)\n}\n\nfunction writeOctal(block: Uint8Array, offset: number, length: number, value: number): void {\n writeBytes(block, offset, encoder.encode(`${value.toString(8).padStart(length - 1, '0')}\\0`))\n}\n","import { randomUUID } from 'node:crypto'\nimport { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nimport { planBatches } from './batches.js'\nimport { DeployError } from './errors.js'\nimport { listFiles, readStaticFiles } from './files.js'\nimport { gzipTar } from './tar.js'\n\nexport const DEFAULT_API_ORIGIN = 'https://backend.embeddables.com'\n\nconst DEFAULT_CONCURRENCY = 3\nconst REQUEST_ATTEMPTS = 3\n\nexport interface DeployHostingAppOptions {\n /** Project deploy key (`edk_…`). */\n deployKey: string\n appSlug: string\n branch: string\n sha: string\n /** ISO 8601 commit time. Keeps overlapping deploys from moving an alias back to an older commit. */\n committedAt?: string\n /** Worker modules. `wrangler.json` here sets the main module, compatibility date, flags, and vars. */\n serverDir: string\n /** Static files. Omit for an app with none. */\n clientDir?: string\n /** When set, the backend checks that it owns the deploy key. */\n projectId?: string\n apiOrigin?: string\n /** Asset batches uploaded at once. */\n concurrency?: number\n fetch?: typeof fetch\n /** Wait between batch retries; replaced in tests. */\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface DeployPromotion {\n status: string\n [key: string]: unknown\n}\n\nexport interface DeployHostingAppResult {\n scriptName: string\n branchUrl: string\n commitUrl: string\n appSlug: string\n projectSlug: string\n ref: string\n /** Null when the branch is not the app's production branch. */\n promotion: DeployPromotion | null\n /** True when this deploy registered the app. Surface it: a misspelled slug registers a new app. */\n appRegistered?: true\n /** Static files in the manifest. */\n totalFiles: number\n /** Distinct files sent; the rest were already stored from earlier deploys. */\n uploadedFiles: number\n}\n\ninterface StartSessionResponse {\n sessionId: string\n buckets: string[][]\n expiresAt: string\n}\n\n/**\n * Uploads a built app in three steps: a manifest of static files, the files\n * the backend does not already have (in parallel batches), then the server\n * modules. The same commit can be redeployed safely; unchanged files upload\n * nothing.\n */\nexport async function deployHostingApp(\n options: DeployHostingAppOptions,\n): Promise<DeployHostingAppResult> {\n const api = createApi(options)\n const idempotencyKey = randomUUID()\n const files = options.clientDir ? await readStaticFiles(options.clientDir) : []\n const server = await readServerArchive(options.serverDir)\n\n const manifest: Record<string, { contentHash: string; size: number }> = {}\n const byHash = new Map<string, { absolutePath: string; size: number }>()\n for (const file of files) {\n manifest[file.path] = { contentHash: file.contentHash, size: file.size }\n if (!byHash.has(file.contentHash)) {\n byHash.set(file.contentHash, { absolutePath: file.absolutePath, size: file.size })\n }\n }\n\n const session = await api.retryingJson<StartSessionResponse>('/hosting/v1/deploys/sessions', {\n method: 'POST',\n headers: { 'content-type': 'application/json', 'idempotency-key': idempotencyKey },\n body: JSON.stringify({\n appSlug: options.appSlug,\n branch: options.branch,\n sha: options.sha,\n committedAt: options.committedAt,\n manifest,\n }),\n })\n\n const sizes = new Map([...byHash].map(([hash, file]) => [hash, file.size]))\n const batches = planBatches(session.buckets, sizes)\n const batchPath = `/hosting/v1/deploys/sessions/${encodeURIComponent(session.sessionId)}/assets`\n await runPool(batches, options.concurrency ?? DEFAULT_CONCURRENCY, async (batch) => {\n const form = new FormData()\n for (const hash of batch) {\n const file = byHash.get(hash)!\n form.append(hash, new Blob([await readFile(file.absolutePath)]), hash)\n }\n await api.retrying(batchPath, { method: 'POST', body: form })\n })\n\n const result = await api.retryingJson<\n Omit<DeployHostingAppResult, 'totalFiles' | 'uploadedFiles'>\n >(`/hosting/v1/deploys/sessions/${encodeURIComponent(session.sessionId)}/finalize`, {\n method: 'POST',\n headers: { 'content-type': 'application/gzip' },\n body: server,\n })\n return {\n ...result,\n totalFiles: files.length,\n uploadedFiles: batches.reduce((total, batch) => total + batch.length, 0),\n }\n}\n\nasync function readServerArchive(serverDir: string): Promise<Uint8Array<ArrayBuffer>> {\n const paths = await listFiles(serverDir)\n if (paths.length === 0) {\n throw new DeployError(`No server modules found in ${serverDir}.`, { code: 'validation.failed' })\n }\n const entries = await Promise.all(\n paths.map(async (path) => ({\n path: `server/${path}`,\n bytes: new Uint8Array(await readFile(join(serverDir, path))),\n })),\n )\n return gzipTar(entries)\n}\n\nfunction createApi(options: DeployHostingAppOptions) {\n const origin = (options.apiOrigin ?? DEFAULT_API_ORIGIN).replace(/\\/+$/, '')\n const fetchImpl = options.fetch ?? fetch\n const sleep = options.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)))\n const auth: Record<string, string> = { authorization: `Bearer ${options.deployKey}` }\n if (options.projectId) auth['x-project-id'] = options.projectId\n\n async function send(path: string, init: RequestInit): Promise<Response> {\n let response: Response\n try {\n response = await fetchImpl(`${origin}${path}`, {\n ...init,\n headers: { ...auth, ...(init.headers as Record<string, string> | undefined) },\n })\n } catch (cause) {\n throw new DeployError(`Could not reach ${origin}.`, { cause })\n }\n if (!response.ok) throw await problem(response)\n return response\n }\n\n return {\n /** Retries network failures, 429, and 5xx for retry-safe deploy operations. */\n async retrying(path: string, init: RequestInit): Promise<Response> {\n for (let attempt = 1; ; attempt++) {\n try {\n return await send(path, init)\n } catch (error) {\n const status = error instanceof DeployError ? error.status : null\n const transient = status === null || status === 429 || status >= 500\n if (!transient || attempt >= REQUEST_ATTEMPTS) throw error\n await sleep(500 * 3 ** (attempt - 1))\n }\n }\n },\n async retryingJson<T>(path: string, init: RequestInit): Promise<T> {\n return (await (await this.retrying(path, init)).json()) as T\n },\n }\n}\n\nasync function problem(response: Response): Promise<DeployError> {\n let body: { detail?: unknown; title?: unknown; code?: unknown } = {}\n try {\n body = (await response.json()) as typeof body\n } catch {\n // * Not every failure (a proxy, a gateway timeout) returns problem JSON.\n }\n const detail = typeof body.detail === 'string' ? body.detail : null\n const title = typeof body.title === 'string' ? body.title : null\n return new DeployError(detail ?? title ?? `Deploy request failed with ${response.status}.`, {\n status: response.status,\n code: typeof body.code === 'string' ? body.code : undefined,\n })\n}\n\nasync function runPool<T>(\n items: T[],\n concurrency: number,\n task: (item: T) => Promise<void>,\n): Promise<void> {\n let next = 0\n let failure: { error: unknown } | null = null\n const workers = Array.from(\n { length: Math.max(1, Math.min(concurrency, items.length)) },\n async () => {\n while (!failure && next < items.length) {\n try {\n await task(items[next++]!)\n } catch (error) {\n failure ??= { error }\n }\n }\n },\n )\n // * Wait for in-flight batches so nothing uploads after the deploy rejects.\n await Promise.all(workers)\n if (failure) throw (failure as { error: unknown }).error\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@embeddables/deploy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Upload a built Worker app to Embeddables Hosting with a project deploy key.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/heysavvy/embeddables.git",
|
|
10
|
+
"directory": "packages/deploy-client"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^22.15.21",
|
|
33
|
+
"tsdown": "^0.22.14",
|
|
34
|
+
"typescript": "~6.0.2",
|
|
35
|
+
"vitest": "^4.1.9"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"typecheck": "tsc -b && tsc -p tsconfig.test.json",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"test": "vitest run"
|
|
42
|
+
}
|
|
6
43
|
}
|