alchemy 0.4.1 → 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 (47) 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.js +3 -1
  7. package/lib/cloudflare/bound.d.ts +2 -1
  8. package/lib/cloudflare/custom-domain.d.ts +1 -15
  9. package/lib/cloudflare/custom-domain.js +1 -15
  10. package/lib/cloudflare/index.d.ts +0 -1
  11. package/lib/cloudflare/index.js +0 -1
  12. package/lib/cloudflare/worker.d.ts +4 -0
  13. package/lib/cloudflare/worker.js +3 -1
  14. package/lib/internal/docs/providers.js +2 -1
  15. package/lib/os/exec.d.ts +103 -0
  16. package/lib/os/exec.js +103 -0
  17. package/lib/os/index.d.ts +1 -0
  18. package/lib/os/index.js +1 -0
  19. package/lib/secret.d.ts +2 -2
  20. package/lib/secret.js +2 -2
  21. package/lib/web/vitepress/vitepress.js +1 -1
  22. package/package.json +2 -1
  23. package/src/ai/data.ts +10 -0
  24. package/src/alchemy.ts +97 -100
  25. package/src/cloudflare/assets.ts +3 -0
  26. package/src/cloudflare/bound.ts +4 -1
  27. package/src/cloudflare/custom-domain.ts +1 -15
  28. package/src/cloudflare/index.ts +0 -1
  29. package/src/cloudflare/worker.ts +8 -1
  30. package/src/internal/docs/providers.ts +2 -1
  31. package/src/os/exec.ts +190 -0
  32. package/src/os/index.ts +1 -0
  33. package/src/secret.ts +2 -2
  34. package/src/web/vitepress/vitepress.ts +1 -1
  35. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
  36. package/lib/cloudflare/generate-asset-manifest.js +0 -68
  37. package/lib/cloudflare/static-site-router.d.ts +0 -18
  38. package/lib/cloudflare/static-site-router.js +0 -115
  39. package/lib/cloudflare/static-site.d.ts +0 -211
  40. package/lib/cloudflare/static-site.js +0 -213
  41. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
  42. package/lib/cloudflare/upload-asset-manifest.js +0 -56
  43. package/src/cloudflare/generate-asset-manifest.ts +0 -93
  44. package/src/cloudflare/static-site-router.ts +0 -157
  45. package/src/cloudflare/static-site.ts +0 -416
  46. package/src/cloudflare/upload-asset-manifest.ts +0 -85
  47. package/src/web/vitepress/index.md +0 -75
package/src/alchemy.ts CHANGED
@@ -22,7 +22,7 @@ export const alchemy: Alchemy = _alchemy as any;
22
22
  *
23
23
  * @example
24
24
  * // Create an application scope with stage and secret handling
25
- * const app = alchemy("github:alchemy", {
25
+ * const app = await alchemy("github:alchemy", {
26
26
  * stage: "prod",
27
27
  * phase: "up",
28
28
  * // Required for encrypting/decrypting secrets
@@ -57,13 +57,13 @@ export interface Alchemy {
57
57
  * Used to create and manage resources with proper secret handling.
58
58
  *
59
59
  * @example
60
- * const app = alchemy("my-app", {
60
+ * const app = await alchemy("my-app", {
61
61
  * stage: "prod",
62
62
  * // Required for encrypting/decrypting secrets
63
63
  * password: process.env.SECRET_PASSPHRASE
64
64
  * });
65
65
  */
66
- (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
66
+ (...parameters: Parameters<typeof scope>): Promise<Scope>;
67
67
  /**
68
68
  * Template literal tag that supports file interpolation for documentation.
69
69
  * Automatically formats the content and appends file contents as code blocks.
@@ -88,11 +88,11 @@ export interface Alchemy {
88
88
  * Implementation of the alchemy function that handles both application scoping
89
89
  * and template string interpolation.
90
90
  */
91
- function _alchemy(
91
+ async function _alchemy(
92
92
  ...args:
93
93
  | [template: TemplateStringsArray, ...values: any[]]
94
94
  | [appName: string, options?: Omit<AlchemyOptions, "appName">]
95
- ): any {
95
+ ): Promise<Scope | string | never> {
96
96
  if (typeof args[0] === "string") {
97
97
  const [appName, options] = args as [string, AlchemyOptions?];
98
98
  const root = scope(undefined, {
@@ -101,9 +101,8 @@ function _alchemy(
101
101
  stage: options?.stage,
102
102
  });
103
103
  if (options?.phase === "destroy") {
104
- return destroy(root).finally(() => {
105
- process.exit(0);
106
- });
104
+ await destroy(root);
105
+ return process.exit(0);
107
106
  }
108
107
  return root;
109
108
  } else {
@@ -114,106 +113,104 @@ function _alchemy(
114
113
  : 0;
115
114
  const indent = " ".repeat(leadingSpaces);
116
115
 
117
- return (async () => {
118
- const { isFileCollection, isFileRef } = await import("./fs");
116
+ const { isFileCollection, isFileRef } = await import("./fs");
119
117
 
120
- const appendices: Record<string, string> = {};
118
+ const appendices: Record<string, string> = {};
121
119
 
122
- const stringValues = await Promise.all(
123
- values.map(async function resolve(value): Promise<string> {
124
- if (typeof value === "string") {
125
- return indent + value;
126
- } else if (value === null) {
127
- return "null";
128
- } else if (value === undefined) {
129
- return "undefined";
130
- } else if (
131
- typeof value === "number" ||
132
- typeof value === "boolean" ||
133
- typeof value === "bigint"
134
- ) {
135
- return value.toString();
136
- } else if (value instanceof Promise) {
137
- return resolve(await value);
138
- } else if (isFileRef(value)) {
139
- if (!(value.path in appendices)) {
140
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
141
- }
120
+ const stringValues = await Promise.all(
121
+ values.map(async function resolve(value): Promise<string> {
122
+ if (typeof value === "string") {
123
+ return indent + value;
124
+ } else if (value === null) {
125
+ return "null";
126
+ } else if (value === undefined) {
127
+ return "undefined";
128
+ } else if (
129
+ typeof value === "number" ||
130
+ typeof value === "boolean" ||
131
+ typeof value === "bigint"
132
+ ) {
133
+ return value.toString();
134
+ } else if (value instanceof Promise) {
135
+ return resolve(await value);
136
+ } else if (isFileRef(value)) {
137
+ if (!(value.path in appendices)) {
138
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
139
+ }
140
+ return `[${path.basename(value.path)}](${value.path})`;
141
+ } else if (isFileCollection(value)) {
142
+ return Object.entries(value.files)
143
+ .map(([filePath, content]) => {
144
+ appendices[filePath] = content;
145
+ return `[${path.basename(filePath)}](${filePath})`;
146
+ })
147
+ .join("\n\n");
148
+ } else if (Array.isArray(value)) {
149
+ return (
150
+ await Promise.all(
151
+ value.map(async (value, i) => `${i}. ${await resolve(value)}`)
152
+ )
153
+ ).join("\n");
154
+ } else if (
155
+ typeof value === "object" &&
156
+ typeof value.path === "string"
157
+ ) {
158
+ if (typeof value.content === "string") {
159
+ appendices[value.path] = value.content;
142
160
  return `[${path.basename(value.path)}](${value.path})`;
143
- } else if (isFileCollection(value)) {
144
- return Object.entries(value.files)
145
- .map(([filePath, content]) => {
146
- appendices[filePath] = content;
147
- return `[${path.basename(filePath)}](${filePath})`;
148
- })
149
- .join("\n\n");
150
- } else if (Array.isArray(value)) {
151
- return (
152
- await Promise.all(
153
- value.map(async (value, i) => `${i}. ${await resolve(value)}`)
154
- )
155
- ).join("\n");
156
- } else if (
157
- typeof value === "object" &&
158
- typeof value.path === "string"
159
- ) {
160
- if (typeof value.content === "string") {
161
- appendices[value.path] = value.content;
162
- return `[${path.basename(value.path)}](${value.path})`;
163
- } else {
164
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
165
- return `[${path.basename(value.path)}](${value.path})`;
166
- }
167
- } else if (typeof value === "object") {
168
- return (
169
- await Promise.all(
170
- Object.entries(value).map(async ([key, value]) => {
171
- return `* ${key}: ${await resolve(value)}`;
172
- })
173
- )
174
- ).join("\n");
175
161
  } else {
176
- // TODO: support other types
177
- console.log(value);
178
- throw new Error(`Unsupported value type: ${value}`);
162
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
163
+ return `[${path.basename(value.path)}](${value.path})`;
179
164
  }
180
- })
181
- );
182
-
183
- // Construct the string template by joining template parts with interpolated values
184
- const lines = template
185
- .map((part) =>
186
- part
187
- .split("\n")
188
- .map((line) =>
189
- line.startsWith(indent) ? line.slice(indent.length) : line
165
+ } else if (typeof value === "object") {
166
+ return (
167
+ await Promise.all(
168
+ Object.entries(value).map(async ([key, value]) => {
169
+ return `* ${key}: ${await resolve(value)}`;
170
+ })
190
171
  )
191
- .join("\n")
192
- )
193
- .flatMap((part, i) =>
194
- i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
195
- )
196
- .join("")
197
- .split("\n");
172
+ ).join("\n");
173
+ } else {
174
+ // TODO: support other types
175
+ console.log(value);
176
+ throw new Error(`Unsupported value type: ${value}`);
177
+ }
178
+ })
179
+ );
198
180
 
199
- // Collect and sort appendices by file path
200
- return [
201
- // format the user prompt and trim the first line if it's empty
202
- lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
203
- ? lines.slice(1).join("\n")
204
- : lines.join("\n"),
181
+ // Construct the string template by joining template parts with interpolated values
182
+ const lines = template
183
+ .map((part) =>
184
+ part
185
+ .split("\n")
186
+ .map((line) =>
187
+ line.startsWith(indent) ? line.slice(indent.length) : line
188
+ )
189
+ .join("\n")
190
+ )
191
+ .flatMap((part, i) =>
192
+ i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
193
+ )
194
+ .join("")
195
+ .split("\n");
205
196
 
206
- // sort appendices by path and include at the end of the prompt
207
- Object.entries(appendices)
208
- .sort(([a], [b]) => a.localeCompare(b))
209
- .map(([filePath, content]) => {
210
- const extension = path.extname(filePath).slice(1);
211
- const codeTag = extension ? extension : "";
212
- return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
213
- })
214
- .join("\n\n"),
215
- ].join("\n");
216
- })();
197
+ // Collect and sort appendices by file path
198
+ return [
199
+ // format the user prompt and trim the first line if it's empty
200
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
201
+ ? lines.slice(1).join("\n")
202
+ : lines.join("\n"),
203
+
204
+ // sort appendices by path and include at the end of the prompt
205
+ Object.entries(appendices)
206
+ .sort(([a], [b]) => a.localeCompare(b))
207
+ .map(([filePath, content]) => {
208
+ const extension = path.extname(filePath).slice(1);
209
+ const codeTag = extension ? extension : "";
210
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
211
+ })
212
+ .join("\n\n"),
213
+ ].join("\n");
217
214
  }
218
215
  }
219
216
  _alchemy.destroy = destroy;
@@ -86,6 +86,9 @@ export interface AssetFile {
86
86
  */
87
87
  export const Assets = Resource(
88
88
  "cloudflare::Asset",
89
+ {
90
+ alwaysUpdate: true,
91
+ },
89
92
  async function (
90
93
  this: Context<Assets>,
91
94
  id: string,
@@ -1,4 +1,5 @@
1
1
  import type { Secret } from "../secret";
2
+ import type { Assets } from "./assets";
2
3
  import type { Binding } from "./bindings";
3
4
  import type { R2Bucket as _R2Bucket } from "./bucket";
4
5
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
@@ -15,4 +16,6 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
15
16
  ? R2Bucket
16
17
  : T extends Secret
17
18
  ? string
18
- : never;
19
+ : T extends Assets
20
+ ? Service
21
+ : Service;
@@ -64,8 +64,7 @@ export interface CustomDomain
64
64
 
65
65
  /**
66
66
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
67
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
68
- * to a specific hostname within a zone.
67
+ * This attaches a worker to a specific hostname within a zone.
69
68
  *
70
69
  * @example
71
70
  * // Bind a domain to a standard Cloudflare Worker
@@ -80,19 +79,6 @@ export interface CustomDomain
80
79
  * workerName: apiWorker.name // Use the name from the Worker resource
81
80
  * });
82
81
  *
83
- * @example
84
- * // Bind a domain to a Cloudflare Static Site
85
- * const mySite = await StaticSite("landing-page", {
86
- * name: "my-landing-page-site", // This becomes the underlying worker name
87
- * dir: "./dist/landing-page"
88
- * });
89
- *
90
- * const siteDomain = await CustomDomain("site-domain-binding", {
91
- * name: "www.example.com",
92
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
93
- * workerName: mySite.name // Use the name from the StaticSite resource
94
- * });
95
- *
96
82
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
97
83
  */
98
84
  export const CustomDomain = Resource(
@@ -9,7 +9,6 @@ export * from "./durable-object-namespace";
9
9
  export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
- export * from "./static-site";
13
12
  export * from "./worker";
14
13
  export * from "./wrangler.json";
15
14
  export * from "./zone";
@@ -101,6 +101,11 @@ export interface WorkerProps<B extends Bindings = Bindings> {
101
101
  * Migrations to apply to the worker
102
102
  */
103
103
  migrations?: SingleStepMigration;
104
+
105
+ /**
106
+ * Whether to adopt the Worker if it already exists when creating
107
+ */
108
+ adopt?: boolean;
104
109
  }
105
110
 
106
111
  /**
@@ -245,7 +250,9 @@ export const Worker = Resource(
245
250
  await deleteWorker(this, api, workerName);
246
251
  return this.destroy();
247
252
  } else if (this.phase === "create") {
248
- await assertWorkerDoesNotExist(this, api, workerName);
253
+ if (!props.adopt) {
254
+ await assertWorkerDoesNotExist(this, api, workerName);
255
+ }
249
256
  }
250
257
 
251
258
  const oldBindings = await this.get<Bindings>("bindings");
@@ -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;