alchemy 0.4.0 → 0.5.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 +3 -463
- package/lib/ai/data.d.ts +5 -0
- package/lib/ai/data.js +3 -0
- package/lib/alchemy.d.ts +3 -3
- package/lib/alchemy.js +82 -85
- package/lib/cloudflare/assets.d.ts +74 -0
- package/lib/cloudflare/assets.js +75 -0
- package/lib/cloudflare/bindings.d.ts +6 -1
- package/lib/cloudflare/bindings.js +6 -0
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/custom-domain.d.ts +1 -15
- package/lib/cloudflare/custom-domain.js +1 -15
- package/lib/cloudflare/index.d.ts +1 -1
- package/lib/cloudflare/index.js +1 -1
- package/lib/cloudflare/r2-rest-state-store.js +52 -22
- package/lib/cloudflare/worker.d.ts +18 -0
- package/lib/cloudflare/worker.js +153 -4
- package/lib/internal/docs/providers.js +2 -1
- package/lib/os/exec.d.ts +103 -0
- package/lib/os/exec.js +103 -0
- package/lib/os/index.d.ts +1 -0
- package/lib/os/index.js +1 -0
- package/lib/secret.d.ts +2 -2
- package/lib/secret.js +2 -2
- package/lib/web/vitepress/vitepress.js +1 -1
- package/package.json +2 -1
- package/src/ai/data.ts +10 -0
- package/src/alchemy.ts +97 -100
- package/src/cloudflare/assets.ts +158 -0
- package/src/cloudflare/bindings.ts +11 -2
- package/src/cloudflare/bound.ts +4 -1
- package/src/cloudflare/custom-domain.ts +1 -15
- package/src/cloudflare/index.ts +1 -1
- package/src/cloudflare/r2-rest-state-store.ts +78 -34
- package/src/cloudflare/worker.ts +261 -6
- package/src/internal/docs/providers.ts +2 -1
- package/src/os/exec.ts +190 -0
- package/src/os/index.ts +1 -0
- package/src/secret.ts +2 -2
- package/src/web/vitepress/vitepress.ts +1 -1
- package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
- package/lib/cloudflare/generate-asset-manifest.js +0 -68
- package/lib/cloudflare/static-site-router.d.ts +0 -18
- package/lib/cloudflare/static-site-router.js +0 -115
- package/lib/cloudflare/static-site.d.ts +0 -211
- package/lib/cloudflare/static-site.js +0 -213
- package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
- package/lib/cloudflare/upload-asset-manifest.js +0 -56
- package/src/cloudflare/generate-asset-manifest.ts +0 -93
- package/src/cloudflare/static-site-router.ts +0 -157
- package/src/cloudflare/static-site.ts +0 -416
- package/src/cloudflare/upload-asset-manifest.ts +0 -85
- package/src/web/vitepress/index.md +0 -75
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
|
|
3
|
-
// see: https://developers.cloudflare.com/workers/runtime-apis/cache/
|
|
4
|
-
declare var caches: {
|
|
5
|
-
default: Cache;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
// injected by src/cloudflare/static-site.ts
|
|
9
|
-
declare var __ASSET_MANIFEST__: Record<string, string>;
|
|
10
|
-
|
|
11
|
-
export type Env = {
|
|
12
|
-
ASSETS: KVNamespace;
|
|
13
|
-
INDEX_PAGE: string;
|
|
14
|
-
ERROR_PAGE?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Optional backend worker bound to this static site
|
|
17
|
-
* Used to handle requests that don't match any static files
|
|
18
|
-
*/
|
|
19
|
-
BACKEND_WORKER?: Service;
|
|
20
|
-
} & {
|
|
21
|
-
// the binding to route the fetch call to
|
|
22
|
-
[key in `ROUTE_${string}`]: Service;
|
|
23
|
-
} & {
|
|
24
|
-
// the route to match on, e.g. /api/*
|
|
25
|
-
[key in `__ROUTE_${string}`]: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
let isInit = false;
|
|
29
|
-
const app = new Hono<{ Bindings: Env }>();
|
|
30
|
-
|
|
31
|
-
export default {
|
|
32
|
-
async fetch(request: Request, env: Env): Promise<Response> {
|
|
33
|
-
if (!isInit) {
|
|
34
|
-
isInit = true;
|
|
35
|
-
for (const [key, value] of Object.entries(env)) {
|
|
36
|
-
if (key.startsWith("ROUTE_")) {
|
|
37
|
-
const service = value as Service;
|
|
38
|
-
const patternKey = `__${key}`;
|
|
39
|
-
if (!(patternKey in env)) {
|
|
40
|
-
throw new Error(`Missing pattern key: ${patternKey}`);
|
|
41
|
-
}
|
|
42
|
-
const pattern = (env as any)[`__${key}`];
|
|
43
|
-
// TODO(sam): we should support narrowing down METHOD
|
|
44
|
-
app.all(pattern, (ctx) => service.fetch(ctx.req.raw));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return app.fetch(request, env);
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// fall back to assuming the request is a static file
|
|
53
|
-
app.notFound(async (ctx) => {
|
|
54
|
-
const request = ctx.req;
|
|
55
|
-
const env = ctx.env;
|
|
56
|
-
const url = new URL(request.url);
|
|
57
|
-
const pathname = url.pathname.replace(/^\//, "");
|
|
58
|
-
const filePath = pathname === "" ? env.INDEX_PAGE : pathname;
|
|
59
|
-
|
|
60
|
-
// Return from cache if available
|
|
61
|
-
const cachedResponse = await lookupCache();
|
|
62
|
-
if (cachedResponse) {
|
|
63
|
-
return cachedResponse;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Fetch from KV
|
|
67
|
-
{
|
|
68
|
-
const object = await env.ASSETS.getWithMetadata(filePath, {
|
|
69
|
-
type: "arrayBuffer",
|
|
70
|
-
});
|
|
71
|
-
if (object.value) {
|
|
72
|
-
return await respond(200, filePath, object);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
{
|
|
76
|
-
const guess = filePath + (filePath.endsWith("/") ? "" : "/") + "index.html";
|
|
77
|
-
const object = await env.ASSETS.getWithMetadata(guess, {
|
|
78
|
-
type: "arrayBuffer",
|
|
79
|
-
});
|
|
80
|
-
if (object.value) {
|
|
81
|
-
return await respond(200, guess, object);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
{
|
|
85
|
-
const guess = filePath + ".html";
|
|
86
|
-
const object = await env.ASSETS.getWithMetadata(guess, {
|
|
87
|
-
type: "arrayBuffer",
|
|
88
|
-
});
|
|
89
|
-
if (object.value) {
|
|
90
|
-
return await respond(200, guess, object);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Handle error page
|
|
95
|
-
if (env.ERROR_PAGE) {
|
|
96
|
-
const object = await env.ASSETS.getWithMetadata(env.ERROR_PAGE, {
|
|
97
|
-
type: "arrayBuffer",
|
|
98
|
-
});
|
|
99
|
-
if (object.value) {
|
|
100
|
-
return await respond(404, env.ERROR_PAGE, object);
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
const object = await env.ASSETS.getWithMetadata(env.INDEX_PAGE, {
|
|
104
|
-
type: "arrayBuffer",
|
|
105
|
-
});
|
|
106
|
-
if (object.value) {
|
|
107
|
-
return await respond(200, env.INDEX_PAGE, object);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return new Response("Page Not Found", { status: 404 });
|
|
112
|
-
|
|
113
|
-
async function lookupCache() {
|
|
114
|
-
const cache = caches.default;
|
|
115
|
-
const r = await cache.match(request.url);
|
|
116
|
-
|
|
117
|
-
// cache does not exist
|
|
118
|
-
if (!r) return;
|
|
119
|
-
|
|
120
|
-
// cache exists but etag does not match
|
|
121
|
-
if (r.headers.get("etag") !== __ASSET_MANIFEST__[filePath]) return;
|
|
122
|
-
|
|
123
|
-
// cache exists
|
|
124
|
-
return r;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function saveCache(response: Response) {
|
|
128
|
-
const cache = caches.default;
|
|
129
|
-
await cache.put(request.url, response.clone());
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function respond(
|
|
133
|
-
status: number,
|
|
134
|
-
filePath: string,
|
|
135
|
-
object: KVNamespaceGetWithMetadataResult<any, any>
|
|
136
|
-
) {
|
|
137
|
-
// build response
|
|
138
|
-
const headers = new Headers();
|
|
139
|
-
if (__ASSET_MANIFEST__[filePath]) {
|
|
140
|
-
headers.set("etag", __ASSET_MANIFEST__[filePath]);
|
|
141
|
-
headers.set("content-type", object.metadata.contentType);
|
|
142
|
-
headers.set("cache-control", object.metadata.cacheControl);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Create response with the raw data directly
|
|
146
|
-
const response = new Response(object.value, {
|
|
147
|
-
status,
|
|
148
|
-
headers,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (request.method === "GET") {
|
|
152
|
-
await saveCache(response);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return response;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { exec } from "child_process";
|
|
2
|
-
import { promises as fs } from "fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { promisify } from "util";
|
|
5
|
-
import type { Context } from "../context";
|
|
6
|
-
import type { BundleProps } from "../esbuild/bundle";
|
|
7
|
-
import { Resource } from "../resource";
|
|
8
|
-
import { createCloudflareApi } from "./api";
|
|
9
|
-
import type { Bindings } from "./bindings";
|
|
10
|
-
import { generateAssetManifest } from "./generate-asset-manifest";
|
|
11
|
-
import { KVNamespace } from "./kv-namespace";
|
|
12
|
-
import { uploadAssetManifest } from "./upload-asset-manifest";
|
|
13
|
-
import { Worker } from "./worker";
|
|
14
|
-
|
|
15
|
-
const __dirname = import.meta.dirname;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Idea of using KV with timeout inspired by SST: https://github.com/sst/sst/blob/dev/platform/src/components/cloudflare/static-site.ts
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Properties for creating or updating a StaticSite
|
|
23
|
-
*/
|
|
24
|
-
export interface StaticSiteProps {
|
|
25
|
-
/**
|
|
26
|
-
* Name for the site
|
|
27
|
-
* This is mandatory - must be explicitly specified
|
|
28
|
-
*/
|
|
29
|
-
name: string;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Path to the directory containing static assets
|
|
33
|
-
*/
|
|
34
|
-
dir: string;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Custom Worker script to serve the site
|
|
38
|
-
* If not provided, a default script will be used
|
|
39
|
-
*/
|
|
40
|
-
workerScript?: string;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Worker script format
|
|
44
|
-
* 'esm' - ECMAScript modules (default)
|
|
45
|
-
* 'cjs' - CommonJS modules
|
|
46
|
-
* @default 'esm'
|
|
47
|
-
*/
|
|
48
|
-
format?: "esm" | "cjs";
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Custom error page path (relative to directory)
|
|
52
|
-
* @example "404.html"
|
|
53
|
-
*/
|
|
54
|
-
errorPage?: string;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Index page filename
|
|
58
|
-
* @default "index.html"
|
|
59
|
-
*/
|
|
60
|
-
indexPage?: string;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Configure how the static site's assets are uploaded to KV
|
|
64
|
-
*/
|
|
65
|
-
assets?: {
|
|
66
|
-
/**
|
|
67
|
-
* File options for configuring caching behavior
|
|
68
|
-
*/
|
|
69
|
-
fileOptions?: {
|
|
70
|
-
/**
|
|
71
|
-
* File pattern(s) to match
|
|
72
|
-
* Can be a glob string or array of glob strings
|
|
73
|
-
*/
|
|
74
|
-
files: string | string[];
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Files to ignore (glob patterns)
|
|
78
|
-
*/
|
|
79
|
-
ignore?: string | string[];
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Cache-Control header to set for matching files
|
|
83
|
-
*/
|
|
84
|
-
cacheControl: string;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Content-Type to set (overrides auto-detection)
|
|
88
|
-
*/
|
|
89
|
-
contentType?: string;
|
|
90
|
-
}[];
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Whether the site should be deployed to production
|
|
95
|
-
* @default true
|
|
96
|
-
*/
|
|
97
|
-
production?: boolean;
|
|
98
|
-
|
|
99
|
-
build?: {
|
|
100
|
-
/**
|
|
101
|
-
* Command to run before deploying the site
|
|
102
|
-
* Will be executed in the shell to build the site
|
|
103
|
-
*/
|
|
104
|
-
command?: string;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Bundle options for the worker script
|
|
109
|
-
*/
|
|
110
|
-
bundle?: Partial<BundleProps>;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Additional workers to route requests to.
|
|
114
|
-
*/
|
|
115
|
-
routes?: {
|
|
116
|
-
[key: string]: Worker;
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Output returned after StaticSite creation/update
|
|
122
|
-
*/
|
|
123
|
-
export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
|
|
124
|
-
/**
|
|
125
|
-
* The ID of the worker
|
|
126
|
-
*/
|
|
127
|
-
workerId: string;
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Name for the site
|
|
131
|
-
*/
|
|
132
|
-
name: string;
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Path to the directory containing static assets
|
|
136
|
-
*/
|
|
137
|
-
directory: string;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Worker script format
|
|
141
|
-
*/
|
|
142
|
-
format?: "esm" | "cjs";
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Custom error page path
|
|
146
|
-
*/
|
|
147
|
-
errorPage?: string;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Index page filename
|
|
151
|
-
*/
|
|
152
|
-
indexPage?: string;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Time at which the site was created
|
|
156
|
-
*/
|
|
157
|
-
createdAt: number;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Time at which the site was last updated
|
|
161
|
-
*/
|
|
162
|
-
updatedAt: number;
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* List of uploaded asset files
|
|
166
|
-
*/
|
|
167
|
-
assets: string[];
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Whether the site is deployed to production
|
|
171
|
-
*/
|
|
172
|
-
production?: boolean;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* The URL of the deployed site
|
|
176
|
-
*/
|
|
177
|
-
url?: string;
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Information about the backend worker if configured
|
|
181
|
-
*/
|
|
182
|
-
routes?: Record<string, string>;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* A StaticSite resource deploys static web content to Cloudflare Workers, using KV for asset storage.
|
|
187
|
-
* It provides an efficient way to serve static websites with global distribution and caching.
|
|
188
|
-
*
|
|
189
|
-
* @example
|
|
190
|
-
* // Create a basic static site with default settings
|
|
191
|
-
* const basicSite = await StaticSite("my-site", {
|
|
192
|
-
* name: "my-site",
|
|
193
|
-
* dir: "./dist",
|
|
194
|
-
* url: true
|
|
195
|
-
* });
|
|
196
|
-
*
|
|
197
|
-
* @example
|
|
198
|
-
* // Create a static site with custom build command and minification disabled
|
|
199
|
-
* const devSite = await StaticSite("dev-site", {
|
|
200
|
-
* name: "dev-site",
|
|
201
|
-
* dir: "./public",
|
|
202
|
-
* build: {
|
|
203
|
-
* command: "npm run build"
|
|
204
|
-
* },
|
|
205
|
-
* bundle: {
|
|
206
|
-
* minify: false
|
|
207
|
-
* }
|
|
208
|
-
* });
|
|
209
|
-
*
|
|
210
|
-
* @example
|
|
211
|
-
* // Create a static site with a backend API worker
|
|
212
|
-
* const backend = await Worker("api-backend", {
|
|
213
|
-
* name: "api-backend",
|
|
214
|
-
* entrypoint: "./src/api.ts"
|
|
215
|
-
* });
|
|
216
|
-
*
|
|
217
|
-
* const fullSite = await StaticSite("full-site", {
|
|
218
|
-
* name: "full-site",
|
|
219
|
-
* dir: "./dist",
|
|
220
|
-
* routes: {
|
|
221
|
-
* "/api/*": backend
|
|
222
|
-
* }
|
|
223
|
-
* });
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* // Create a static site with a custom domain using the CustomDomain resource
|
|
227
|
-
* const site = await StaticSite("custom-site", {
|
|
228
|
-
* name: "custom-site",
|
|
229
|
-
* dir: "./www",
|
|
230
|
-
* errorPage: "404.html",
|
|
231
|
-
* indexPage: "home.html"
|
|
232
|
-
* });
|
|
233
|
-
*
|
|
234
|
-
* // Then configure the custom domain separately
|
|
235
|
-
* const domain = await CustomDomain("custom-domain", {
|
|
236
|
-
* name: "www.example.com",
|
|
237
|
-
* zoneId: "abcdef123456789",
|
|
238
|
-
* workerName: site.name
|
|
239
|
-
* });
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* // Create a static site with multiple domains (primary + redirects)
|
|
243
|
-
* const site = await StaticSite("multi-domain-site", {
|
|
244
|
-
* name: "multi-domain-site",
|
|
245
|
-
* dir: "./public"
|
|
246
|
-
* });
|
|
247
|
-
*
|
|
248
|
-
* @see https://developers.cloudflare.com/workers/platform/sites
|
|
249
|
-
*/
|
|
250
|
-
export const StaticSite = Resource(
|
|
251
|
-
"cloudflare::StaticSite",
|
|
252
|
-
{
|
|
253
|
-
alwaysUpdate: true,
|
|
254
|
-
},
|
|
255
|
-
async function (
|
|
256
|
-
this: Context<StaticSite>,
|
|
257
|
-
id: string,
|
|
258
|
-
props: StaticSiteProps
|
|
259
|
-
) {
|
|
260
|
-
// Create Cloudflare API client with automatic account discovery
|
|
261
|
-
const api = await createCloudflareApi();
|
|
262
|
-
|
|
263
|
-
if (this.phase === "delete") {
|
|
264
|
-
// For delete operations, we'll rely on the Worker delete to clean up
|
|
265
|
-
// Return empty output for deleted state
|
|
266
|
-
return this.destroy();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Validate that a name is provided
|
|
270
|
-
if (!props.name) {
|
|
271
|
-
throw new Error(
|
|
272
|
-
"StaticSite name is required - must be explicitly specified"
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Validate directory exists
|
|
277
|
-
if (!props.dir) {
|
|
278
|
-
throw new Error("Directory is required for StaticSite");
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Run build command if provided
|
|
282
|
-
if (props.build?.command) {
|
|
283
|
-
try {
|
|
284
|
-
if (!this.quiet) {
|
|
285
|
-
console.log(props.build.command);
|
|
286
|
-
}
|
|
287
|
-
const execAsync = promisify(exec);
|
|
288
|
-
const { stdout, stderr } = await execAsync(props.build.command, {
|
|
289
|
-
cwd: process.cwd(),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (stdout && !this.quiet) console.log(stdout);
|
|
293
|
-
} catch (error: any) {
|
|
294
|
-
// Log detailed error information
|
|
295
|
-
console.error(`Build command failed with exit code ${error.code}`);
|
|
296
|
-
if (error.stdout) console.error(`Command stdout: ${error.stdout}`);
|
|
297
|
-
if (error.stderr) console.error(`Command stderr: ${error.stderr}`);
|
|
298
|
-
|
|
299
|
-
// Throw a more descriptive error that includes the exit code and stderr
|
|
300
|
-
throw new Error(
|
|
301
|
-
`Build command "${props.build.command}" failed with exit code ${error.code}:\n${error.stderr || error.message}`
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const dirStat = await fs.stat(props.dir);
|
|
308
|
-
if (!dirStat.isDirectory()) {
|
|
309
|
-
throw new Error(`"${props.dir}" is not a directory`);
|
|
310
|
-
}
|
|
311
|
-
} catch (error: any) {
|
|
312
|
-
throw new Error(
|
|
313
|
-
`Directory "${props.dir}" does not exist: ${error.message}`
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Use the provided name
|
|
318
|
-
const siteName = props.name;
|
|
319
|
-
const indexPage = props.indexPage || "index.html";
|
|
320
|
-
|
|
321
|
-
// Step 1: Create or get the KV namespace for assets
|
|
322
|
-
const [kv, assetManifest] = await Promise.all([
|
|
323
|
-
KVNamespace("assets", {
|
|
324
|
-
title: `${siteName}-assets`,
|
|
325
|
-
}),
|
|
326
|
-
generateAssetManifest(props.dir),
|
|
327
|
-
]);
|
|
328
|
-
|
|
329
|
-
// Step 3: Upload assets to KV
|
|
330
|
-
await uploadAssetManifest(api, kv.namespaceId, assetManifest);
|
|
331
|
-
|
|
332
|
-
// Prepare the bindings for the worker
|
|
333
|
-
const bindings: Bindings = {
|
|
334
|
-
ASSETS: kv,
|
|
335
|
-
INDEX_PAGE: indexPage,
|
|
336
|
-
// Add error page binding if specified
|
|
337
|
-
...(props.errorPage ? { ERROR_PAGE: props.errorPage } : {}),
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const routes: Record<string, string> = {};
|
|
341
|
-
// Create backend worker if configured
|
|
342
|
-
if (props.routes) {
|
|
343
|
-
for (const [path, worker] of Object.entries(props.routes)) {
|
|
344
|
-
// @ts-ignore - TODO: need to use Resolved<Worker> to get the string ...
|
|
345
|
-
routes[path] = worker.id;
|
|
346
|
-
bindings[`ROUTE_${worker.id}`] = worker;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Create asset manifest banner for static site router
|
|
351
|
-
const assetManifestBanner = `const __ASSET_MANIFEST__ = ${JSON.stringify(
|
|
352
|
-
Object.fromEntries(assetManifest.map((item) => [item.key, item.hash]))
|
|
353
|
-
)};\n`;
|
|
354
|
-
|
|
355
|
-
const bundleOptions = {
|
|
356
|
-
...props.bundle,
|
|
357
|
-
options: {
|
|
358
|
-
...props.bundle?.options,
|
|
359
|
-
banner: {
|
|
360
|
-
js: assetManifestBanner,
|
|
361
|
-
...props.bundle?.options?.banner,
|
|
362
|
-
},
|
|
363
|
-
},
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// Determine the entrypoint file - check for .ts first, fallback to .js
|
|
367
|
-
let entrypointFile = "static-site-router.js";
|
|
368
|
-
try {
|
|
369
|
-
const tsFile = path.resolve(__dirname, "static-site-router.ts");
|
|
370
|
-
await fs.access(tsFile);
|
|
371
|
-
// If we reach here, the TypeScript file exists
|
|
372
|
-
entrypointFile = "static-site-router.ts";
|
|
373
|
-
} catch (error) {
|
|
374
|
-
// TypeScript file doesn't exist, use JavaScript (default)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const worker = await Worker("worker", {
|
|
378
|
-
name: siteName,
|
|
379
|
-
url: true,
|
|
380
|
-
format: props.format || "esm",
|
|
381
|
-
// If a custom worker script is provided, use it, otherwise use the entrypoint from router
|
|
382
|
-
...(props.workerScript
|
|
383
|
-
? { script: props.workerScript }
|
|
384
|
-
: {
|
|
385
|
-
entrypoint: path.resolve(__dirname, entrypointFile),
|
|
386
|
-
bundle: bundleOptions as any,
|
|
387
|
-
}),
|
|
388
|
-
bindings,
|
|
389
|
-
env: {
|
|
390
|
-
...Object.fromEntries(
|
|
391
|
-
Object.entries(routes).map(([key, value]) => [
|
|
392
|
-
`__ROUTE_${value}`,
|
|
393
|
-
key,
|
|
394
|
-
])
|
|
395
|
-
),
|
|
396
|
-
},
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
const now = Date.now();
|
|
400
|
-
|
|
401
|
-
return this({
|
|
402
|
-
workerId: worker.id,
|
|
403
|
-
name: siteName,
|
|
404
|
-
directory: props.dir,
|
|
405
|
-
format: props.format || "esm",
|
|
406
|
-
errorPage: props.errorPage,
|
|
407
|
-
indexPage,
|
|
408
|
-
assets: assetManifest.map((item) => item.key),
|
|
409
|
-
createdAt: this.output?.createdAt || now,
|
|
410
|
-
updatedAt: now,
|
|
411
|
-
production: props.production !== false,
|
|
412
|
-
url: worker.url,
|
|
413
|
-
routes,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
);
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import type { CloudflareApi } from "./api";
|
|
3
|
-
import type { AssetManifest } from "./asset-manifest";
|
|
4
|
-
|
|
5
|
-
// Maximum number of concurrent uploads
|
|
6
|
-
const MAX_CONCURRENT = 100; // Adjust based on API limits and performance testing
|
|
7
|
-
|
|
8
|
-
// Define types for our queue operations
|
|
9
|
-
type AssetItem = AssetManifest[number];
|
|
10
|
-
type UploadResult = {
|
|
11
|
-
item: AssetItem;
|
|
12
|
-
success: boolean;
|
|
13
|
-
status?: number;
|
|
14
|
-
error?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export async function uploadAssetManifest(
|
|
18
|
-
api: CloudflareApi,
|
|
19
|
-
namespaceId: string,
|
|
20
|
-
manifest: AssetManifest
|
|
21
|
-
): Promise<UploadResult[]> {
|
|
22
|
-
const results: UploadResult[] = [];
|
|
23
|
-
|
|
24
|
-
// Process in batches with controlled concurrency
|
|
25
|
-
let index = 0;
|
|
26
|
-
const activePromises = new Set<Promise<void>>();
|
|
27
|
-
|
|
28
|
-
while (index < manifest.length || activePromises.size > 0) {
|
|
29
|
-
// Fill up to MAX_CONCURRENT active uploads
|
|
30
|
-
while (activePromises.size < MAX_CONCURRENT && index < manifest.length) {
|
|
31
|
-
const item = manifest[index++];
|
|
32
|
-
const promise = uploadItem(item).then((result) => {
|
|
33
|
-
results.push(result);
|
|
34
|
-
activePromises.delete(promise);
|
|
35
|
-
});
|
|
36
|
-
activePromises.add(promise);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Wait for at least one promise to complete if we have any active
|
|
40
|
-
if (activePromises.size > 0) {
|
|
41
|
-
await Promise.race(activePromises);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return results;
|
|
46
|
-
|
|
47
|
-
async function uploadItem(item: AssetItem): Promise<UploadResult> {
|
|
48
|
-
const content = await fs.readFile(item.source);
|
|
49
|
-
|
|
50
|
-
// Create metadata object
|
|
51
|
-
const metadata = {
|
|
52
|
-
key: item.key,
|
|
53
|
-
hash: item.hash,
|
|
54
|
-
contentType: item.contentType,
|
|
55
|
-
cacheControl: item.cacheControl,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Create FormData for multipart upload
|
|
59
|
-
const formData = new FormData();
|
|
60
|
-
formData.append("metadata", JSON.stringify(metadata));
|
|
61
|
-
|
|
62
|
-
// Add file content as 'value' - Fix for binary file corruption
|
|
63
|
-
const blob = new Blob([new Uint8Array(content)], {
|
|
64
|
-
type: item.contentType,
|
|
65
|
-
});
|
|
66
|
-
formData.append("value", blob);
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const response = await api.put(
|
|
70
|
-
`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/values/${item.key}`,
|
|
71
|
-
formData
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const result = { item, success: response.ok, status: response.status };
|
|
75
|
-
if (!result.success) {
|
|
76
|
-
console.warn(`Error uploading asset ${item.key}: ${result.status}`);
|
|
77
|
-
}
|
|
78
|
-
return result;
|
|
79
|
-
} catch (error) {
|
|
80
|
-
const result = { item, success: false, error };
|
|
81
|
-
console.warn(`Error uploading asset ${item.key}: ${error}`);
|
|
82
|
-
return result;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|