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
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>):
|
|
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
|
-
):
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
118
|
-
const { isFileCollection, isFileRef } = await import("./fs");
|
|
116
|
+
const { isFileCollection, isFileRef } = await import("./fs");
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
const appendices: Record<string, string> = {};
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type { Context } from "../context";
|
|
4
|
+
import { Resource } from "../resource";
|
|
5
|
+
import { getContentType } from "../util/content-type";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Properties for creating or updating Assets
|
|
9
|
+
*/
|
|
10
|
+
export interface AssetsProps {
|
|
11
|
+
/**
|
|
12
|
+
* Path to a directory containing static assets to be uploaded
|
|
13
|
+
* These files will be served by Cloudflare's Workers runtime
|
|
14
|
+
*/
|
|
15
|
+
path: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Output returned after Assets creation/update
|
|
20
|
+
*/
|
|
21
|
+
export interface Assets extends Resource<"cloudflare::Asset">, AssetsProps {
|
|
22
|
+
/**
|
|
23
|
+
* The type of binding
|
|
24
|
+
*/
|
|
25
|
+
type: "assets";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The ID of the assets bundle
|
|
29
|
+
*/
|
|
30
|
+
id: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Asset files that were found
|
|
34
|
+
*/
|
|
35
|
+
files: AssetFile[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Time at which the assets were created
|
|
39
|
+
*/
|
|
40
|
+
createdAt: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Time at which the assets were last updated
|
|
44
|
+
*/
|
|
45
|
+
updatedAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Represents a single asset file
|
|
50
|
+
*/
|
|
51
|
+
export interface AssetFile {
|
|
52
|
+
/**
|
|
53
|
+
* Path relative to the assets directory
|
|
54
|
+
*/
|
|
55
|
+
path: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Full filesystem path to the file
|
|
59
|
+
*/
|
|
60
|
+
filePath: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Content type of the file
|
|
64
|
+
*/
|
|
65
|
+
contentType: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cloudflare Assets represent a collection of static files that can be uploaded and served
|
|
70
|
+
* by Cloudflare Workers.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Create a basic assets bundle from a local directory
|
|
74
|
+
* const staticAssets = await Assets("static", {
|
|
75
|
+
* path: "./src/assets"
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* // Use these assets with a worker
|
|
79
|
+
* const worker = await Worker("frontend", {
|
|
80
|
+
* name: "frontend-worker",
|
|
81
|
+
* entrypoint: "./src/worker.ts",
|
|
82
|
+
* bindings: {
|
|
83
|
+
* ASSETS: staticAssets
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
export const Assets = Resource(
|
|
88
|
+
"cloudflare::Asset",
|
|
89
|
+
{
|
|
90
|
+
alwaysUpdate: true,
|
|
91
|
+
},
|
|
92
|
+
async function (
|
|
93
|
+
this: Context<Assets>,
|
|
94
|
+
id: string,
|
|
95
|
+
props: AssetsProps
|
|
96
|
+
): Promise<Assets> {
|
|
97
|
+
if (this.phase === "delete") {
|
|
98
|
+
return this.destroy();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Check if the assets directory exists
|
|
103
|
+
const stats = await fs.stat(props.path);
|
|
104
|
+
if (!stats.isDirectory()) {
|
|
105
|
+
throw new Error(`Assets path ${props.path} is not a directory`);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Assets directory ${props.path} does not exist or is not accessible`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Recursively get all files in the assets directory
|
|
114
|
+
const filesList = await getFilesRecursively(props.path);
|
|
115
|
+
|
|
116
|
+
// Create asset file objects
|
|
117
|
+
const files: AssetFile[] = filesList.map((filePath) => {
|
|
118
|
+
const relativePath = path.relative(props.path, filePath);
|
|
119
|
+
const normalizedPath = relativePath.split(path.sep).join("/"); // Ensure forward slashes for URLs
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
path: normalizedPath,
|
|
123
|
+
filePath,
|
|
124
|
+
contentType: getContentType(filePath),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Get current timestamp
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
|
|
131
|
+
// Construct the output
|
|
132
|
+
return this({
|
|
133
|
+
id,
|
|
134
|
+
type: "assets",
|
|
135
|
+
path: props.path,
|
|
136
|
+
files,
|
|
137
|
+
createdAt: this.output?.createdAt || now,
|
|
138
|
+
updatedAt: now,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Helper functions for file operations
|
|
144
|
+
async function getFilesRecursively(dir: string): Promise<string[]> {
|
|
145
|
+
const files = await fs.readdir(dir, { withFileTypes: true });
|
|
146
|
+
|
|
147
|
+
const allFiles = await Promise.all(
|
|
148
|
+
files.map(async (file) => {
|
|
149
|
+
const path = `${dir}/${file.name}`;
|
|
150
|
+
if (file.isDirectory()) {
|
|
151
|
+
return getFilesRecursively(path);
|
|
152
|
+
}
|
|
153
|
+
return path;
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return allFiles.flat();
|
|
158
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
|
|
5
5
|
*/
|
|
6
6
|
import type { Secret } from "../secret";
|
|
7
|
+
import type { Assets } from "./assets";
|
|
7
8
|
import type { R2Bucket } from "./bucket";
|
|
8
9
|
import type { DurableObjectNamespace } from "./durable-object-namespace";
|
|
9
10
|
import type { KVNamespace } from "./kv-namespace";
|
|
@@ -22,16 +23,24 @@ export type Binding =
|
|
|
22
23
|
| Worker
|
|
23
24
|
| R2Bucket
|
|
24
25
|
| Secret
|
|
25
|
-
| string
|
|
26
|
+
| string
|
|
27
|
+
| Assets;
|
|
26
28
|
|
|
27
29
|
export function isDurableObjectNamespace(
|
|
28
|
-
binding: Binding
|
|
30
|
+
binding: Binding
|
|
29
31
|
): binding is DurableObjectNamespace {
|
|
30
32
|
return (
|
|
31
33
|
typeof binding === "object" && binding.type === "durable_object_namespace"
|
|
32
34
|
);
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Check if a binding is an Assets resource
|
|
39
|
+
*/
|
|
40
|
+
export function isAssets(binding: Binding): binding is Assets {
|
|
41
|
+
return typeof binding === "object" && binding.type === "assets";
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* Union type for all Worker binding types (API spec)
|
|
37
46
|
*/
|
package/src/cloudflare/bound.ts
CHANGED
|
@@ -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
|
-
:
|
|
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
|
|
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(
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./account-api-token";
|
|
2
2
|
export * from "./api";
|
|
3
|
+
export * from "./assets";
|
|
3
4
|
export * from "./bindings";
|
|
4
5
|
export * from "./bucket";
|
|
5
6
|
export * from "./custom-domain";
|
|
@@ -8,7 +9,6 @@ export * from "./durable-object-namespace";
|
|
|
8
9
|
export * from "./kv-namespace";
|
|
9
10
|
export * from "./permission-groups";
|
|
10
11
|
export * from "./r2-rest-state-store";
|
|
11
|
-
export * from "./static-site";
|
|
12
12
|
export * from "./worker";
|
|
13
13
|
export * from "./wrangler.json";
|
|
14
14
|
export * from "./zone";
|
|
@@ -123,16 +123,30 @@ export class R2RestStateStore implements StateStore {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
|
|
127
|
+
const response = await withExponentialBackoff(
|
|
128
|
+
async () => {
|
|
129
|
+
const response = await this.api.get(listPath);
|
|
130
|
+
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
const errorData: any = await response.json().catch(() => ({
|
|
133
|
+
errors: [{ message: response.statusText }],
|
|
134
|
+
}));
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return response;
|
|
141
|
+
},
|
|
142
|
+
// Retry on transient errors
|
|
143
|
+
(error) =>
|
|
144
|
+
error.message?.includes("502") ||
|
|
145
|
+
error.message?.includes("503") ||
|
|
146
|
+
error.message?.includes("timeout"),
|
|
147
|
+
5, // 5 retry attempts
|
|
148
|
+
1000 // Start with 1 second delay
|
|
149
|
+
);
|
|
136
150
|
|
|
137
151
|
const data = (await response.json()) as any;
|
|
138
152
|
|
|
@@ -176,21 +190,38 @@ export class R2RestStateStore implements StateStore {
|
|
|
176
190
|
await this.ensureInitialized();
|
|
177
191
|
|
|
178
192
|
try {
|
|
179
|
-
const response = await
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
const response = await withExponentialBackoff(
|
|
194
|
+
async () => {
|
|
195
|
+
const response = await this.api.get(
|
|
196
|
+
`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
|
|
197
|
+
);
|
|
182
198
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
if (response.status === 404) {
|
|
201
|
+
return response;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const errorData: any = await response.json().catch(() => ({
|
|
205
|
+
errors: [{ message: response.statusText }],
|
|
206
|
+
}));
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
187
211
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
212
|
+
return response;
|
|
213
|
+
},
|
|
214
|
+
// Retry on transient errors
|
|
215
|
+
(error) =>
|
|
216
|
+
error.message?.includes("502") ||
|
|
217
|
+
error.message?.includes("503") ||
|
|
218
|
+
error.message?.includes("timeout"),
|
|
219
|
+
5, // 5 retry attempts
|
|
220
|
+
1000 // Start with 1 second delay
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
if (response.status === 404) {
|
|
224
|
+
return undefined;
|
|
194
225
|
}
|
|
195
226
|
|
|
196
227
|
// Parse and deserialize the state data
|
|
@@ -298,18 +329,31 @@ export class R2RestStateStore implements StateStore {
|
|
|
298
329
|
async delete(key: string): Promise<void> {
|
|
299
330
|
await this.ensureInitialized();
|
|
300
331
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
332
|
+
await withExponentialBackoff(
|
|
333
|
+
async () => {
|
|
334
|
+
const response = await this.api.delete(
|
|
335
|
+
`/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
|
|
336
|
+
);
|
|
304
337
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
if (!response.ok && response.status !== 404) {
|
|
339
|
+
const errorData: any = await response.json().catch(() => ({
|
|
340
|
+
errors: [{ message: response.statusText }],
|
|
341
|
+
}));
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return response;
|
|
348
|
+
},
|
|
349
|
+
// Retry on transient errors
|
|
350
|
+
(error) =>
|
|
351
|
+
error.message?.includes("502") ||
|
|
352
|
+
error.message?.includes("503") ||
|
|
353
|
+
error.message?.includes("timeout"),
|
|
354
|
+
5, // 5 retry attempts
|
|
355
|
+
1000 // Start with 1 second delay
|
|
356
|
+
);
|
|
313
357
|
}
|
|
314
358
|
|
|
315
359
|
/**
|