Skip to content

Commit bdfc2f1

Browse files
authored
feat: Assets resource & Worker Binding (#40)
1 parent 3ef2476 commit bdfc2f1

File tree

7 files changed

+619
-33
lines changed

7 files changed

+619
-33
lines changed

alchemy-web/.vitepress/config.mts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ export default defineConfig({
4444
],
4545
socialLinks: [
4646
{ icon: "github", link: "https://github.com/sam-goodwin/alchemy" },
47-
{
48-
icon: "discord",
49-
link: "https://discord.gg/jwKw8dBJdN",
50-
},
47+
{ icon: "discord", link: "https://discord.gg/jwKw8dBJdN" },
5148
{ icon: "x", link: "https://twitter.com/samgoodwin89" },
5249
],
5350
sidebar: [
@@ -153,6 +150,7 @@ export default defineConfig({
153150
text: "AccountId",
154151
link: "/docs/providers/cloudflare/account-id",
155152
},
153+
{ text: "Assets", link: "/docs/providers/cloudflare/assets" },
156154
{
157155
text: "CustomDomain",
158156
link: "/docs/providers/cloudflare/custom-domain",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Assets
2+
3+
The Assets resource lets you add [static assets](https://developers.cloudflare.com/workers/platform/sites/) to your Cloudflare Workers.
4+
5+
# Minimal Example
6+
7+
Create a basic assets bundle from a local directory:
8+
9+
```ts
10+
import { Assets } from "alchemy/cloudflare";
11+
12+
const staticAssets = await Assets("static", {
13+
path: "./src/assets"
14+
});
15+
```
16+
17+
# Bind to a Worker
18+
19+
Use the assets with a Cloudflare Worker:
20+
21+
```ts
22+
import { Worker, Assets } from "alchemy/cloudflare";
23+
24+
const staticAssets = await Assets("static", {
25+
path: "./src/assets"
26+
});
27+
28+
const worker = await Worker("frontend", {
29+
name: "frontend-worker",
30+
entrypoint: "./src/worker.ts",
31+
bindings: {
32+
ASSETS: staticAssets
33+
}
34+
});
35+
```

alchemy/src/cloudflare/assets.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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+
async function (
90+
this: Context<Assets>,
91+
id: string,
92+
props: AssetsProps
93+
): Promise<Assets> {
94+
if (this.phase === "delete") {
95+
return this.destroy();
96+
}
97+
98+
try {
99+
// Check if the assets directory exists
100+
const stats = await fs.stat(props.path);
101+
if (!stats.isDirectory()) {
102+
throw new Error(`Assets path ${props.path} is not a directory`);
103+
}
104+
} catch (error) {
105+
throw new Error(
106+
`Assets directory ${props.path} does not exist or is not accessible`
107+
);
108+
}
109+
110+
// Recursively get all files in the assets directory
111+
const filesList = await getFilesRecursively(props.path);
112+
113+
// Create asset file objects
114+
const files: AssetFile[] = filesList.map((filePath) => {
115+
const relativePath = path.relative(props.path, filePath);
116+
const normalizedPath = relativePath.split(path.sep).join("/"); // Ensure forward slashes for URLs
117+
118+
return {
119+
path: normalizedPath,
120+
filePath,
121+
contentType: getContentType(filePath),
122+
};
123+
});
124+
125+
// Get current timestamp
126+
const now = Date.now();
127+
128+
// Construct the output
129+
return this({
130+
id,
131+
type: "assets",
132+
path: props.path,
133+
files,
134+
createdAt: this.output?.createdAt || now,
135+
updatedAt: now,
136+
});
137+
}
138+
);
139+
140+
// Helper functions for file operations
141+
async function getFilesRecursively(dir: string): Promise<string[]> {
142+
const files = await fs.readdir(dir, { withFileTypes: true });
143+
144+
const allFiles = await Promise.all(
145+
files.map(async (file) => {
146+
const path = `${dir}/${file.name}`;
147+
if (file.isDirectory()) {
148+
return getFilesRecursively(path);
149+
}
150+
return path;
151+
})
152+
);
153+
154+
return allFiles.flat();
155+
}

alchemy/src/cloudflare/bindings.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
55
*/
66
import type { Secret } from "../secret";
7+
import type { Assets } from "./assets";
78
import type { R2Bucket } from "./bucket";
89
import type { DurableObjectNamespace } from "./durable-object-namespace";
910
import type { KVNamespace } from "./kv-namespace";
@@ -22,16 +23,24 @@ export type Binding =
2223
| Worker
2324
| R2Bucket
2425
| Secret
25-
| string;
26+
| string
27+
| Assets;
2628

2729
export function isDurableObjectNamespace(
28-
binding: Binding,
30+
binding: Binding
2931
): binding is DurableObjectNamespace {
3032
return (
3133
typeof binding === "object" && binding.type === "durable_object_namespace"
3234
);
3335
}
3436

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+
3544
/**
3645
* Union type for all Worker binding types (API spec)
3746
*/

alchemy/src/cloudflare/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./account-api-token";
22
export * from "./api";
3+
export * from "./assets";
34
export * from "./bindings";
45
export * from "./bucket";
56
export * from "./custom-domain";

0 commit comments

Comments
 (0)