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.
Files changed (53) hide show
  1. package/README.md +3 -463
  2. package/lib/ai/data.d.ts +5 -0
  3. package/lib/ai/data.js +3 -0
  4. package/lib/alchemy.d.ts +3 -3
  5. package/lib/alchemy.js +82 -85
  6. package/lib/cloudflare/assets.d.ts +74 -0
  7. package/lib/cloudflare/assets.js +75 -0
  8. package/lib/cloudflare/bindings.d.ts +6 -1
  9. package/lib/cloudflare/bindings.js +6 -0
  10. package/lib/cloudflare/bound.d.ts +2 -1
  11. package/lib/cloudflare/custom-domain.d.ts +1 -15
  12. package/lib/cloudflare/custom-domain.js +1 -15
  13. package/lib/cloudflare/index.d.ts +1 -1
  14. package/lib/cloudflare/index.js +1 -1
  15. package/lib/cloudflare/r2-rest-state-store.js +52 -22
  16. package/lib/cloudflare/worker.d.ts +18 -0
  17. package/lib/cloudflare/worker.js +153 -4
  18. package/lib/internal/docs/providers.js +2 -1
  19. package/lib/os/exec.d.ts +103 -0
  20. package/lib/os/exec.js +103 -0
  21. package/lib/os/index.d.ts +1 -0
  22. package/lib/os/index.js +1 -0
  23. package/lib/secret.d.ts +2 -2
  24. package/lib/secret.js +2 -2
  25. package/lib/web/vitepress/vitepress.js +1 -1
  26. package/package.json +2 -1
  27. package/src/ai/data.ts +10 -0
  28. package/src/alchemy.ts +97 -100
  29. package/src/cloudflare/assets.ts +158 -0
  30. package/src/cloudflare/bindings.ts +11 -2
  31. package/src/cloudflare/bound.ts +4 -1
  32. package/src/cloudflare/custom-domain.ts +1 -15
  33. package/src/cloudflare/index.ts +1 -1
  34. package/src/cloudflare/r2-rest-state-store.ts +78 -34
  35. package/src/cloudflare/worker.ts +261 -6
  36. package/src/internal/docs/providers.ts +2 -1
  37. package/src/os/exec.ts +190 -0
  38. package/src/os/index.ts +1 -0
  39. package/src/secret.ts +2 -2
  40. package/src/web/vitepress/vitepress.ts +1 -1
  41. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
  42. package/lib/cloudflare/generate-asset-manifest.js +0 -68
  43. package/lib/cloudflare/static-site-router.d.ts +0 -18
  44. package/lib/cloudflare/static-site-router.js +0 -115
  45. package/lib/cloudflare/static-site.d.ts +0 -211
  46. package/lib/cloudflare/static-site.js +0 -213
  47. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
  48. package/lib/cloudflare/upload-asset-manifest.js +0 -56
  49. package/src/cloudflare/generate-asset-manifest.ts +0 -93
  50. package/src/cloudflare/static-site-router.ts +0 -157
  51. package/src/cloudflare/static-site.ts +0 -416
  52. package/src/cloudflare/upload-asset-manifest.ts +0 -85
  53. 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
- }