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,14 +1,18 @@
1
+ import * as crypto from "crypto";
1
2
  import * as fs from "fs/promises";
2
3
  import type { Context } from "../context";
3
4
  import { Bundle, type BundleProps } from "../esbuild/bundle";
4
5
  import { Resource } from "../resource";
5
6
  import { isSecret } from "../secret";
7
+ import { getContentType } from "../util/content-type";
6
8
  import { withExponentialBackoff } from "../util/retry";
7
9
  import { slugify } from "../util/slugify";
8
10
  import { type CloudflareApi, createCloudflareApi } from "./api";
11
+ import type { Assets } from "./assets";
9
12
  import {
10
13
  type Bindings,
11
14
  type WorkerBindingSpec,
15
+ isAssets,
12
16
  isDurableObjectNamespace,
13
17
  } from "./bindings";
14
18
  import type { Bound } from "./bound";
@@ -97,6 +101,11 @@ export interface WorkerProps<B extends Bindings = Bindings> {
97
101
  * Migrations to apply to the worker
98
102
  */
99
103
  migrations?: SingleStepMigration;
104
+
105
+ /**
106
+ * Whether to adopt the Worker if it already exists when creating
107
+ */
108
+ adopt?: boolean;
100
109
  }
101
110
 
102
111
  /**
@@ -200,6 +209,20 @@ export interface Worker<B extends Bindings = Bindings>
200
209
  * }
201
210
  * });
202
211
  *
212
+ * @example
213
+ * // Create a worker with static assets:
214
+ * const staticAssets = await Assets("static", {
215
+ * path: "./src/assets"
216
+ * });
217
+ *
218
+ * const frontendWorker = await Worker("frontend", {
219
+ * name: "frontend-worker",
220
+ * entrypoint: "./src/worker.ts",
221
+ * bindings: {
222
+ * ASSETS: staticAssets
223
+ * }
224
+ * });
225
+ *
203
226
  * @see https://developers.cloudflare.com/workers/
204
227
  */
205
228
  export const Worker = Resource(
@@ -227,20 +250,49 @@ export const Worker = Resource(
227
250
  await deleteWorker(this, api, workerName);
228
251
  return this.destroy();
229
252
  } else if (this.phase === "create") {
230
- await assertWorkerDoesNotExist(this, api, workerName);
253
+ if (!props.adopt) {
254
+ await assertWorkerDoesNotExist(this, api, workerName);
255
+ }
231
256
  }
232
257
 
233
258
  const oldBindings = await this.get<Bindings>("bindings");
234
259
 
260
+ // Get the script content - either from props.script, or by bundling
261
+ const scriptContent = props.script ?? (await bundleWorkerScript(props));
262
+
263
+ // Find any assets bindings
264
+ const assetsBindings: { name: string; assets: Assets }[] = [];
265
+ if (props.bindings) {
266
+ for (const [bindingName, binding] of Object.entries(props.bindings)) {
267
+ if (isAssets(binding)) {
268
+ assetsBindings.push({ name: bindingName, assets: binding });
269
+ }
270
+ }
271
+ }
272
+
273
+ // Upload any assets and get completion tokens
274
+ let assetUploadResult: AssetUploadResult | undefined;
275
+ if (assetsBindings.length > 0) {
276
+ // We'll use the first asset binding for now
277
+ // In the future, we might want to support multiple asset bindings
278
+ const assetBinding = assetsBindings[0];
279
+
280
+ // Upload the assets and get the completion token
281
+ assetUploadResult = await uploadAssets(
282
+ api,
283
+ workerName,
284
+ assetBinding.assets
285
+ );
286
+ }
287
+
288
+ // Prepare metadata with bindings
235
289
  const scriptMetadata = await prepareWorkerMetadata(
236
290
  this,
237
291
  oldBindings,
238
- props
292
+ props,
293
+ assetUploadResult
239
294
  );
240
295
 
241
- // Get the script content - either from props.script, or by bundling
242
- const scriptContent = props.script ?? (await bundleWorkerScript(props));
243
-
244
296
  // Upload the worker script
245
297
  await putWorker(api, workerName, scriptContent, scriptMetadata);
246
298
 
@@ -437,12 +489,29 @@ interface WorkerMetadata {
437
489
  main_module?: string;
438
490
  body_part?: string;
439
491
  tags?: string[];
492
+ assets?: {
493
+ jwt?: string;
494
+ keep_assets?: boolean;
495
+ config?: {
496
+ html_handling?: "auto-trailing-slash" | "none";
497
+ not_found_handling?: "none" | "fall-through";
498
+ };
499
+ };
500
+ }
501
+
502
+ interface AssetUploadResult {
503
+ completionToken: string;
504
+ assetConfig?: {
505
+ html_handling?: "auto-trailing-slash" | "none";
506
+ not_found_handling?: "none" | "fall-through";
507
+ };
440
508
  }
441
509
 
442
510
  async function prepareWorkerMetadata<B extends Bindings>(
443
511
  ctx: Context<Worker<B>>,
444
512
  oldBindings: Bindings | undefined,
445
- props: WorkerProps
513
+ props: WorkerProps,
514
+ assetUploadResult?: AssetUploadResult
446
515
  ): Promise<WorkerMetadata> {
447
516
  // Prepare metadata with bindings
448
517
  const meta: WorkerMetadata = {
@@ -461,6 +530,17 @@ async function prepareWorkerMetadata<B extends Bindings>(
461
530
  },
462
531
  };
463
532
 
533
+ // If we have asset upload results, add them to the metadata
534
+ if (assetUploadResult) {
535
+ meta.assets = {
536
+ jwt: assetUploadResult.completionToken,
537
+ };
538
+
539
+ if (assetUploadResult.assetConfig) {
540
+ meta.assets.config = assetUploadResult.assetConfig;
541
+ }
542
+ }
543
+
464
544
  const bindings = (props.bindings ?? {}) as Bindings;
465
545
 
466
546
  // Convert bindings to the format expected by the API
@@ -522,6 +602,11 @@ async function prepareWorkerMetadata<B extends Bindings>(
522
602
  name: bindingName,
523
603
  bucket_name: binding.name,
524
604
  });
605
+ } else if (isAssets(binding)) {
606
+ meta.bindings.push({
607
+ type: "assets",
608
+ name: bindingName,
609
+ });
525
610
  } else if (isSecret(binding)) {
526
611
  meta.bindings.push({
527
612
  type: "secret_text",
@@ -802,3 +887,173 @@ async function getWorkerBindings(
802
887
 
803
888
  return data.result;
804
889
  }
890
+
891
+ /**
892
+ * Interface for a file's metadata to be uploaded
893
+ */
894
+ interface FileMetadata {
895
+ hash: string;
896
+ size: number;
897
+ }
898
+
899
+ /**
900
+ * Response from the assets upload session API
901
+ */
902
+ interface UploadSessionResponse {
903
+ result: {
904
+ jwt: string;
905
+ buckets: string[][];
906
+ };
907
+ success: boolean;
908
+ errors: any[];
909
+ messages: any[];
910
+ }
911
+
912
+ /**
913
+ * Response from the file upload API
914
+ */
915
+ interface UploadResponse {
916
+ result: {
917
+ jwt: string;
918
+ buckets?: string[][];
919
+ };
920
+ success: boolean;
921
+ errors: any[];
922
+ messages: any[];
923
+ }
924
+
925
+ /**
926
+ * Uploads assets to Cloudflare and returns a completion token
927
+ *
928
+ * @param api CloudflareApi instance
929
+ * @param workerName Name of the worker
930
+ * @param assets Assets resource containing files to upload
931
+ * @returns Completion token for the assets upload
932
+ */
933
+ async function uploadAssets(
934
+ api: CloudflareApi,
935
+ workerName: string,
936
+ assets: Assets
937
+ ): Promise<AssetUploadResult> {
938
+ // Generate the file manifest
939
+ const fileMetadata: Record<string, FileMetadata> = {};
940
+
941
+ // Process each file in the assets
942
+ for (const file of assets.files) {
943
+ const { hash, size } = await calculateFileMetadata(file.filePath);
944
+ // Use the relative path as the key, ensuring it starts with a slash
945
+ const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
946
+ fileMetadata[key] = { hash, size };
947
+ }
948
+
949
+ // Start the upload session
950
+ const uploadSessionUrl = `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`;
951
+ const uploadSessionResponse = await api.post(
952
+ uploadSessionUrl,
953
+ JSON.stringify({ manifest: fileMetadata }),
954
+ {
955
+ headers: { "Content-Type": "application/json" },
956
+ }
957
+ );
958
+
959
+ if (!uploadSessionResponse.ok) {
960
+ throw new Error(
961
+ `Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText}`
962
+ );
963
+ }
964
+
965
+ const sessionData =
966
+ (await uploadSessionResponse.json()) as UploadSessionResponse;
967
+
968
+ // If there are no buckets, assets are already uploaded or empty
969
+ if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
970
+ return { completionToken: sessionData.result.jwt };
971
+ }
972
+
973
+ // Upload the files in batches as specified by the API
974
+ let completionToken = sessionData.result.jwt;
975
+ const buckets = sessionData.result.buckets;
976
+
977
+ // Process each bucket of files
978
+ for (const bucket of buckets) {
979
+ const formData = new FormData();
980
+
981
+ // Add each file in the bucket to the form
982
+ for (const fileHash of bucket) {
983
+ // Find the file with this hash
984
+ const file = assets.files.find((f) => {
985
+ const filePath = f.path.startsWith("/") ? f.path : `/${f.path}`;
986
+ return fileMetadata[filePath]?.hash === fileHash;
987
+ });
988
+
989
+ if (!file) {
990
+ throw new Error(`Could not find file with hash ${fileHash}`);
991
+ }
992
+
993
+ // Read the file content
994
+ const fileContent = await fs.readFile(file.filePath);
995
+
996
+ // Convert to base64 as required by the API when using base64=true
997
+ const base64Content = fileContent.toString("base64");
998
+
999
+ // Add the file to the form with the hash as the key and set the correct content type
1000
+ const blob = new Blob([base64Content], {
1001
+ type: getContentType(file.filePath),
1002
+ });
1003
+ formData.append(fileHash, blob, fileHash);
1004
+ }
1005
+
1006
+ // Upload this batch of files
1007
+ const uploadResponse = await api.post(
1008
+ `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
1009
+ formData,
1010
+ {
1011
+ headers: {
1012
+ Authorization: `Bearer ${completionToken}`,
1013
+ "Content-Type": "multipart/form-data",
1014
+ },
1015
+ }
1016
+ );
1017
+
1018
+ if (!uploadResponse.ok) {
1019
+ throw new Error(
1020
+ `Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`
1021
+ );
1022
+ }
1023
+
1024
+ const uploadData = (await uploadResponse.json()) as UploadResponse;
1025
+ // Update the completion token for the next batch
1026
+ if (uploadData.result.jwt) {
1027
+ completionToken = uploadData.result.jwt;
1028
+ }
1029
+ }
1030
+
1031
+ // Return the final completion token
1032
+ return {
1033
+ completionToken,
1034
+ assetConfig: {
1035
+ html_handling: "auto-trailing-slash",
1036
+ },
1037
+ };
1038
+ }
1039
+
1040
+ /**
1041
+ * Calculate the SHA-256 hash and size of a file
1042
+ *
1043
+ * @param filePath Path to the file
1044
+ * @returns Hash (first 32 chars of SHA-256) and size of the file
1045
+ */
1046
+ async function calculateFileMetadata(
1047
+ filePath: string
1048
+ ): Promise<{ hash: string; size: number }> {
1049
+ const hash = crypto.createHash("sha256");
1050
+ const fileContent = await fs.readFile(filePath);
1051
+
1052
+ hash.update(fileContent);
1053
+ const fileHash = hash.digest("hex").substring(0, 32); // First 32 chars of hash
1054
+
1055
+ return {
1056
+ hash: fileHash,
1057
+ size: fileContent.length,
1058
+ };
1059
+ }
@@ -124,6 +124,7 @@ async function generateProviderDocs({
124
124
  reasoningEffort: "high",
125
125
  },
126
126
  },
127
+ freeze: true,
127
128
  temperature: 0.1,
128
129
  schema: type({
129
130
  groups: type({
@@ -156,7 +157,7 @@ async function generateProviderDocs({
156
157
  A file is considered a "Utility" if it contains utility functions that are not resources or clients.
157
158
  A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
158
159
 
159
- The title should be simply the name of the resource's const in code (with spaces added in between each word), e.g. "Bucket" or "Function", except with spaces, e.g. "Static Site" for "const StaticSite". Maintain all other casing.
160
+ The title should be simply the name of the resource's const in code (with spaces added in between each word), e.g. "Bucket" or "Function", except with spaces, e.g. "StaticSite" for "const StaticSite". Maintain all other casing.
160
161
 
161
162
  // "Resource Name"
162
163
  const ResourceName = Resource(...)
package/src/os/exec.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import type { Context } from "../context";
4
+ import { Resource } from "../resource";
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ /**
9
+ * Properties for executing a shell command
10
+ */
11
+ export interface ExecProps {
12
+ /**
13
+ * The command to execute (including any arguments)
14
+ */
15
+ command: string;
16
+
17
+ /**
18
+ * Whether to memoize the command (only re-run if the command changes)
19
+ *
20
+ * @default false
21
+ */
22
+ memoize?: boolean;
23
+
24
+ /**
25
+ * Working directory for the command
26
+ */
27
+ cwd?: string;
28
+
29
+ /**
30
+ * Environment variables to set
31
+ */
32
+ env?: Record<string, string>;
33
+
34
+ /**
35
+ * Maximum buffer size for stdout and stderr (in bytes)
36
+ * @default 1024 * 1024 (1MB)
37
+ */
38
+ maxBuffer?: number;
39
+
40
+ /**
41
+ * Whether to throw an error if the command exits with a non-zero status
42
+ */
43
+ throwOnError?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Output returned after command execution
48
+ */
49
+ export interface Exec extends Resource<"os::Exec">, ExecProps {
50
+ /**
51
+ * Unique identifier for this execution
52
+ */
53
+ id: string;
54
+
55
+ /**
56
+ * Exit code of the command
57
+ */
58
+ exitCode: number;
59
+
60
+ /**
61
+ * Standard output from the command
62
+ */
63
+ stdout: string;
64
+
65
+ /**
66
+ * Standard error from the command
67
+ */
68
+ stderr: string;
69
+
70
+ /**
71
+ * Time at which the command was executed
72
+ */
73
+ executedAt: number;
74
+
75
+ /**
76
+ * Whether the command has completed execution
77
+ */
78
+ completed: boolean;
79
+ }
80
+
81
+ /**
82
+ * Execute a shell command
83
+ *
84
+ * @example
85
+ * // Run a simple command
86
+ * const result = await Exec("list-files", {
87
+ * command: "ls -la"
88
+ * });
89
+ *
90
+ * console.log(result.stdout);
91
+ *
92
+ * @example
93
+ * // Run a command in a specific directory with custom environment
94
+ * const build = await Exec("build-project", {
95
+ * command: "npm run build",
96
+ * cwd: "./my-project",
97
+ * env: { NODE_ENV: "production" }
98
+ * });
99
+ *
100
+ * @example
101
+ * // Run a command with a larger buffer for output
102
+ * const logs = await Exec("get-logs", {
103
+ * command: "cat large-log-file.log",
104
+ * maxBuffer: 10 * 1024 * 1024 // 10MB
105
+ * });
106
+ *
107
+ * @example
108
+ * // Run a memoized command that only re-executes when the command changes
109
+ * const memoizedCmd = await Exec("status-check", {
110
+ * command: "git status",
111
+ * memoize: true
112
+ * });
113
+ *
114
+ * // This won't actually run the command again if nothing has changed
115
+ * await Exec("status-check", {
116
+ * command: "git status",
117
+ * memoize: true
118
+ * });
119
+ */
120
+ export const Exec = Resource(
121
+ "os::Exec",
122
+ {
123
+ alwaysUpdate: true,
124
+ },
125
+ async function (
126
+ this: Context<Exec>,
127
+ id: string,
128
+ props: ExecProps
129
+ ): Promise<Exec> {
130
+ if (this.phase === "delete") {
131
+ // Nothing to actually delete for an exec command
132
+ return this.destroy();
133
+ } else if (
134
+ this.phase === "update" &&
135
+ props.memoize &&
136
+ this.output?.command === props.command
137
+ ) {
138
+ // If memoize is enabled and the command hasn't changed, return the existing output
139
+ return this.output;
140
+ } else {
141
+ // Default values
142
+ let stdout = "";
143
+ let stderr = "";
144
+ let exitCode = 0;
145
+
146
+ try {
147
+ console.log(props.command);
148
+ // Execute the command
149
+ const result = await execAsync(props.command, {
150
+ cwd: props.cwd || process.cwd(),
151
+ env: { ...process.env, ...props.env },
152
+ maxBuffer: props.maxBuffer || 1024 * 1024, // Default 1MB
153
+ });
154
+
155
+ stdout = result.stdout;
156
+ console.log(stdout);
157
+ stderr = result.stderr;
158
+ exitCode = 0; // Success
159
+ } catch (error: any) {
160
+ console.log("error", error);
161
+ if (props.throwOnError) {
162
+ throw error;
163
+ }
164
+
165
+ // If not throwing, capture the error information
166
+ exitCode = error.code || 1;
167
+ stdout = error.stdout || "";
168
+ stderr = error.stderr || String(error);
169
+ console.log(stdout);
170
+ console.error(stderr);
171
+ }
172
+
173
+ // Return the execution result
174
+ return this({
175
+ id,
176
+ command: props.command,
177
+ cwd: props.cwd,
178
+ env: props.env,
179
+ maxBuffer: props.maxBuffer,
180
+ throwOnError: props.throwOnError,
181
+ memoize: props.memoize,
182
+ exitCode,
183
+ stdout,
184
+ stderr,
185
+ executedAt: Date.now(),
186
+ completed: true,
187
+ });
188
+ }
189
+ }
190
+ );
@@ -0,0 +1 @@
1
+ export * from "./exec";
package/src/secret.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 1. Globally when initializing the alchemy application:
7
7
  * ```ts
8
- * const app = alchemy("my-app", {
8
+ * const app = await alchemy("my-app", {
9
9
  * password: process.env.SECRET_PASSPHRASE
10
10
  * });
11
11
  * ```
@@ -55,7 +55,7 @@ export function isSecret(binding: any): binding is Secret {
55
55
  *
56
56
  * @example
57
57
  * // Global password for all secrets
58
- * const app = alchemy("my-app", {
58
+ * const app = await alchemy("my-app", {
59
59
  * password: process.env.SECRET_PASSPHRASE
60
60
  * });
61
61
  *
@@ -190,7 +190,7 @@ export const VitepressProject = Resource(
190
190
  // import { Document } from "alchemy/docs";
191
191
  // import path from "path";
192
192
 
193
- // await using _ = alchemy("alchemy.run", {
193
+ // const app = await alchemy("alchemy.run", {
194
194
  // stage: "prod",
195
195
  // phase: process.argv.includes("--destroy") ? "destroy" : "up",
196
196
  // password: process.env.SECRET_PASSPHRASE,
@@ -1,2 +0,0 @@
1
- import type { AssetManifest, FileOption } from "./asset-manifest";
2
- export declare function generateAssetManifest(dir: string, fileOptions?: FileOption[]): Promise<AssetManifest>;
@@ -1,68 +0,0 @@
1
- import crypto from "crypto";
2
- import { promises as fs } from "fs";
3
- import { glob } from "glob";
4
- import path from "path";
5
- import { getContentType } from "../util/content-type";
6
- export async function generateAssetManifest(dir, fileOptions = [
7
- {
8
- files: "**",
9
- cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
10
- },
11
- {
12
- files: ["**/*.js", "**/*.css", "**/*.png", "**/*.webp"],
13
- cacheControl: "max-age=31536000,public,immutable",
14
- },
15
- ]) {
16
- // First, collect all file info based on fileOptions (reversed to prioritize later rules)
17
- const fileInfos = [];
18
- const filesProcessed = new Set();
19
- // Process file options in reverse order (later rules override earlier ones)
20
- // Use for...of to maintain order but allow await inside the loop
21
- const reversedOptions = [...fileOptions].reverse();
22
- for (const fileOption of reversedOptions) {
23
- const patterns = Array.isArray(fileOption.files)
24
- ? fileOption.files
25
- : [fileOption.files];
26
- // Get all files matching the pattern
27
- const files = (await Promise.all(patterns.map(async (p) => {
28
- // Use glob as async function instead of globSync
29
- return glob(p, {
30
- cwd: path.resolve(dir),
31
- nodir: true,
32
- dot: true,
33
- ignore: [
34
- ".git/**",
35
- "node_modules/**",
36
- ...(typeof fileOption.ignore === "string"
37
- ? [fileOption.ignore]
38
- : (fileOption.ignore ?? [])),
39
- ],
40
- });
41
- })))
42
- .flat()
43
- .filter((file) => !filesProcessed.has(file));
44
- // Add files to processed set
45
- for (const file of files) {
46
- filesProcessed.add(file);
47
- fileInfos.push({
48
- file,
49
- cacheControl: fileOption.cacheControl,
50
- contentType: fileOption.contentType,
51
- });
52
- }
53
- }
54
- // Process all files in parallel
55
- const manifestEntries = await Promise.all(fileInfos.map(async ({ file, cacheControl, contentType }) => {
56
- const source = path.resolve(dir, file);
57
- const content = await fs.readFile(source);
58
- const hash = crypto.createHash("sha256").update(content).digest("hex");
59
- return {
60
- source,
61
- key: file,
62
- hash,
63
- cacheControl,
64
- contentType: contentType ?? getContentType(file),
65
- };
66
- }));
67
- return manifestEntries;
68
- }
@@ -1,18 +0,0 @@
1
- export type Env = {
2
- ASSETS: KVNamespace;
3
- INDEX_PAGE: string;
4
- ERROR_PAGE?: string;
5
- /**
6
- * Optional backend worker bound to this static site
7
- * Used to handle requests that don't match any static files
8
- */
9
- BACKEND_WORKER?: Service;
10
- } & {
11
- [key in `ROUTE_${string}`]: Service;
12
- } & {
13
- [key in `__ROUTE_${string}`]: string;
14
- };
15
- declare const _default: {
16
- fetch(request: Request, env: Env): Promise<Response>;
17
- };
18
- export default _default;