alchemy 0.11.3 → 0.12.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/lib/alchemy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { destroy } from "./destroy";
2
+ import { env } from "./env";
2
3
  import { Scope } from "./scope";
3
4
  import { secret } from "./secret";
4
5
  import type { StateStoreType } from "./state";
@@ -143,5 +144,4 @@ declare function run<T>(...args: [id: string, fn: (this: Scope, scope: Scope) =>
143
144
  options: RunOptions,
144
145
  fn: (this: Scope, scope: Scope) => Promise<T>
145
146
  ]): Promise<T>;
146
- export declare function env<T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
147
147
  export {};
package/lib/alchemy.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { destroy, DestroyedSignal } from "./destroy";
4
+ import { env } from "./env";
4
5
  import { Scope } from "./scope";
5
6
  import { secret } from "./secret";
6
7
  export const alchemy = _alchemy;
8
+ _alchemy.destroy = destroy;
9
+ _alchemy.run = run;
10
+ _alchemy.secret = secret;
11
+ _alchemy.env = env;
7
12
  /**
8
13
  * Implementation of the alchemy function that handles both application scoping
9
14
  * and template string interpolation.
@@ -119,10 +124,6 @@ async function _alchemy(...args) {
119
124
  ].join("\n");
120
125
  }
121
126
  }
122
- _alchemy.destroy = destroy;
123
- _alchemy.run = run;
124
- _alchemy.secret = secret;
125
- _alchemy.env = env;
126
127
  /**
127
128
  * Run a function in a new scope asynchronously.
128
129
  * Useful for isolating secret handling with a specific password.
@@ -183,23 +184,3 @@ async function run(...args) {
183
184
  await _scope.finalize();
184
185
  }
185
186
  }
186
- export async function env(name, value, error) {
187
- if (value !== undefined) {
188
- return value;
189
- }
190
- else if (typeof process !== undefined) {
191
- // we are in a node environment
192
- return process.env[name];
193
- }
194
- else {
195
- // we are in a browser environment
196
- try {
197
- const { env } = await import("cloudflare:workers");
198
- if (name in env) {
199
- return env[name];
200
- }
201
- }
202
- catch (error) { }
203
- }
204
- throw new Error(error ?? `Environment variable ${name} is not set`);
205
- }
@@ -13,9 +13,11 @@ export * from "./permission-groups";
13
13
  export * from "./pipeline";
14
14
  export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
+ export * from "./tanstack-start";
16
17
  export * from "./vectorize-index";
17
18
  export * from "./vectorize-metadata-index";
18
- export * from "./vite-site";
19
+ export * from "./vite";
20
+ export * from "./website";
19
21
  export * from "./worker";
20
22
  export { Workflow } from "./workflow";
21
23
  export * from "./wrangler.json";
@@ -13,9 +13,11 @@ export * from "./permission-groups";
13
13
  export * from "./pipeline";
14
14
  export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
+ export * from "./tanstack-start";
16
17
  export * from "./vectorize-index";
17
18
  export * from "./vectorize-metadata-index";
18
- export * from "./vite-site";
19
+ export * from "./vite";
20
+ export * from "./website";
19
21
  export * from "./worker";
20
22
  export { Workflow } from "./workflow";
21
23
  export * from "./wrangler.json";
@@ -0,0 +1,12 @@
1
+ import type { Assets } from "./assets";
2
+ import type { Bindings } from "./bindings";
3
+ import { type WebsiteProps } from "./website";
4
+ import type { Worker } from "./worker";
5
+ export interface TanStackStartProps<B extends Bindings> extends WebsiteProps<B> {
6
+ }
7
+ export type TanStackStart<B extends Bindings> = B extends {
8
+ ASSETS: any;
9
+ } ? never : Worker<B & {
10
+ ASSETS: Assets;
11
+ }>;
12
+ export declare function TanStackStart<B extends Bindings>(id: string, props?: Partial<TanStackStartProps<B>>): Promise<TanStackStart<B>>;
@@ -0,0 +1,11 @@
1
+ import { Website } from "./website";
2
+ export async function TanStackStart(id, props) {
3
+ return Website(id, {
4
+ ...props,
5
+ command: props?.command ?? "bun run build",
6
+ wrangler: props?.wrangler ?? false,
7
+ entrypoint: props?.entrypoint ?? ".output/server/index.mjs",
8
+ compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
9
+ assets: props?.assets ?? ".output/public",
10
+ });
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { Assets } from "./assets";
2
+ import type { Bindings } from "./bindings";
3
+ import { type WebsiteProps } from "./website";
4
+ import type { Worker } from "./worker";
5
+ export interface ViteProps<B extends Bindings> extends WebsiteProps<B> {
6
+ }
7
+ export type Vite<B extends Bindings> = B extends {
8
+ ASSETS: any;
9
+ } ? never : Worker<B & {
10
+ ASSETS: Assets;
11
+ }>;
12
+ export declare function Vite<B extends Bindings>(id: string, props: ViteProps<B>): Promise<Vite<B>>;
@@ -0,0 +1,18 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Website } from "./website";
4
+ export async function Vite(id, props) {
5
+ return Website(id, {
6
+ ...props,
7
+ assets: props.assets ??
8
+ (await (async () => {
9
+ try {
10
+ await fs.access(path.join("dist", "client", "index.html"));
11
+ return path.join(".", "dist", "client");
12
+ }
13
+ catch {
14
+ return path.join(".", "dist");
15
+ }
16
+ })()),
17
+ });
18
+ }
@@ -1,7 +1,7 @@
1
1
  import { Assets } from "./assets";
2
2
  import type { Bindings } from "./bindings";
3
- import { Worker } from "./worker";
4
- export interface ViteSiteProps<B extends Bindings> {
3
+ import { Worker, type AssetsConfig, type WorkerProps } from "./worker";
4
+ export interface WebsiteProps<B extends Bindings> extends Omit<WorkerProps<B>, "name" | "assets"> {
5
5
  /**
6
6
  * The command to run to build the site
7
7
  */
@@ -23,19 +23,23 @@ export interface ViteSiteProps<B extends Bindings> {
23
23
  *
24
24
  * @default "./dist"
25
25
  */
26
- assets?: string;
27
- /**
28
- * The bindings to pass to the worker
29
- */
30
- bindings?: B;
26
+ assets?: string | ({
27
+ dist?: string;
28
+ } & AssetsConfig);
31
29
  /**
32
30
  * @default process.cwd()
33
31
  */
34
32
  cwd?: string;
33
+ /**
34
+ * Write a wrangler.jsonc file
35
+ *
36
+ * @default - no wrangler.jsonc file is written
37
+ */
38
+ wrangler?: boolean | string;
35
39
  }
36
- export type ViteSite<B extends Bindings> = Promise<B extends {
40
+ export type Website<B extends Bindings> = B extends {
37
41
  ASSETS: any;
38
42
  } ? never : Worker<B & {
39
43
  ASSETS: Assets;
40
- }>>;
41
- export declare function ViteSite<B extends Bindings>(id: string, props: ViteSiteProps<B>): ViteSite<B>;
44
+ }>;
45
+ export declare function Website<B extends Bindings>(id: string, props: WebsiteProps<B>): Promise<Website<B>>;
@@ -0,0 +1,72 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { alchemy } from "../alchemy";
4
+ import { Exec } from "../os";
5
+ import { Assets } from "./assets";
6
+ import { Worker } from "./worker";
7
+ import { WranglerJson } from "./wrangler.json";
8
+ export async function Website(id, props) {
9
+ if (props.bindings?.ASSETS) {
10
+ throw new Error("ASSETS binding is reserved for internal use");
11
+ }
12
+ return alchemy.run(id, async () => {
13
+ // building the site requires a wrangler.jsonc file to start
14
+ // - so initialize an empty one if it doesn't exist
15
+ const cwd = path.resolve(props.cwd || process.cwd());
16
+ const fileName = typeof props.wrangler === "boolean" ? "wrangler.jsonc" : props.wrangler;
17
+ const wranglerPath = fileName && path.relative(cwd, path.join(cwd, fileName));
18
+ if (props.wrangler) {
19
+ try {
20
+ await fs.access(wranglerPath);
21
+ }
22
+ catch {
23
+ await fs.writeFile(wranglerPath, JSON.stringify({
24
+ name: id,
25
+ main: props.main,
26
+ compatibility_date: new Date().toISOString().split("T")[0],
27
+ }, null, 2));
28
+ }
29
+ }
30
+ await Exec("build", {
31
+ command: props.command,
32
+ });
33
+ const dist = typeof props.assets === "string"
34
+ ? props.assets
35
+ : (props.assets?.dist ?? "dist");
36
+ const staticAssets = await Assets("assets", {
37
+ path: dist,
38
+ });
39
+ const worker = await Worker("worker", {
40
+ ...props,
41
+ name: props.name ?? id,
42
+ entrypoint: props.main,
43
+ assets: {
44
+ html_handling: "auto-trailing-slash",
45
+ not_found_handling: "single-page-application",
46
+ run_worker_first: false,
47
+ ...(typeof props.assets === "string" ? {} : props.assets),
48
+ },
49
+ script: props.main
50
+ ? undefined
51
+ : `
52
+ export default {
53
+ async fetch(request, env) {
54
+ return new Response("Not Found", { status: 404 });
55
+ },
56
+ };`,
57
+ url: true,
58
+ adopt: true,
59
+ bindings: {
60
+ ...props.bindings,
61
+ ASSETS: staticAssets,
62
+ },
63
+ });
64
+ if (props.wrangler) {
65
+ await WranglerJson("wrangler.jsonc", {
66
+ path: wranglerPath,
67
+ worker,
68
+ });
69
+ }
70
+ return worker;
71
+ });
72
+ }
@@ -5,6 +5,40 @@ import { type CloudflareApiOptions } from "./api";
5
5
  import { type Bindings } from "./bindings";
6
6
  import type { Bound } from "./bound";
7
7
  import type { SingleStepMigration } from "./worker-migration";
8
+ /**
9
+ * Configuration options for static assets
10
+ */
11
+ export interface AssetsConfig {
12
+ /**
13
+ * The contents of a _headers file (used to attach custom headers on asset responses)
14
+ */
15
+ _headers?: string;
16
+ /**
17
+ * The contents of a _redirects file (used to apply redirects or proxy paths ahead of asset serving)
18
+ */
19
+ _redirects?: string;
20
+ /**
21
+ * Determines the redirects and rewrites of requests for HTML content
22
+ * @default "auto-trailing-slash"
23
+ */
24
+ html_handling?: "auto-trailing-slash" | "force-trailing-slash" | "drop-trailing-slash" | "none";
25
+ /**
26
+ * Determines the response when a request does not match a static asset, and there is no Worker script
27
+ */
28
+ not_found_handling?: "none" | "404-page" | "single-page-application";
29
+ /**
30
+ * When true, requests will always invoke the Worker script.
31
+ * Otherwise, attempt to serve an asset matching the request, falling back to the Worker script.
32
+ */
33
+ run_worker_first?: boolean;
34
+ /**
35
+ * When true and the incoming request matches an asset, that will be served instead of invoking the Worker script.
36
+ * When false, requests will always invoke the Worker script.
37
+ * @default true
38
+ * @deprecated
39
+ */
40
+ serve_directly?: boolean;
41
+ }
8
42
  /**
9
43
  * Properties for creating or updating a Worker
10
44
  */
@@ -83,6 +117,10 @@ export interface WorkerProps<B extends Bindings = Bindings> extends CloudflareAp
83
117
  * The compatibility flags for the worker
84
118
  */
85
119
  compatibilityFlags?: string[];
120
+ /**
121
+ * Configuration for static assets
122
+ */
123
+ assets?: AssetsConfig;
86
124
  }
87
125
  /**
88
126
  * Output returned after Worker creation/update
@@ -110,6 +148,10 @@ export interface Worker<B extends Bindings = Bindings> extends Resource<"cloudfl
110
148
  * The bindings that were created
111
149
  */
112
150
  bindings: B | undefined;
151
+ /**
152
+ * Configuration for static assets
153
+ */
154
+ assets?: AssetsConfig;
113
155
  Env: {
114
156
  [bindingName in keyof B]: Bound<B[bindingName]>;
115
157
  };
@@ -132,7 +132,7 @@ export const Worker = Resource("cloudflare::Worker", {
132
132
  // In the future, we might want to support multiple asset bindings
133
133
  const assetBinding = assetsBindings[0];
134
134
  // Upload the assets and get the completion token
135
- assetUploadResult = await uploadAssets(api, workerName, assetBinding.assets);
135
+ assetUploadResult = await uploadAssets(api, workerName, assetBinding.assets, props.assets);
136
136
  }
137
137
  // Prepare metadata with bindings
138
138
  const scriptMetadata = await prepareWorkerMetadata(this, oldBindings, props, assetUploadResult);
@@ -165,6 +165,8 @@ export const Worker = Resource("cloudflare::Worker", {
165
165
  createdAt: now,
166
166
  updatedAt: now,
167
167
  url: workerUrl,
168
+ // Include assets configuration in the output
169
+ assets: props.assets,
168
170
  // phantom property
169
171
  Env: undefined,
170
172
  });
@@ -236,6 +238,35 @@ class NotFoundError extends Error {
236
238
  this.name = "NotFoundError";
237
239
  }
238
240
  }
241
+ /**
242
+ * Creates asset configuration object from provided config or defaults
243
+ */
244
+ function createAssetConfig(config) {
245
+ const assetConfig = {
246
+ html_handling: "auto-trailing-slash",
247
+ };
248
+ if (config) {
249
+ if (config._headers !== undefined) {
250
+ assetConfig._headers = config._headers;
251
+ }
252
+ if (config._redirects !== undefined) {
253
+ assetConfig._redirects = config._redirects;
254
+ }
255
+ if (config.html_handling !== undefined) {
256
+ assetConfig.html_handling = config.html_handling;
257
+ }
258
+ if (config.not_found_handling !== undefined) {
259
+ assetConfig.not_found_handling = config.not_found_handling;
260
+ }
261
+ if (config.run_worker_first !== undefined) {
262
+ assetConfig.run_worker_first = config.run_worker_first;
263
+ }
264
+ if (config.serve_directly !== undefined) {
265
+ assetConfig.serve_directly = config.serve_directly;
266
+ }
267
+ }
268
+ return assetConfig;
269
+ }
239
270
  async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult) {
240
271
  // Prepare metadata with bindings
241
272
  const meta = {
@@ -260,8 +291,16 @@ async function prepareWorkerMetadata(ctx, oldBindings, props, assetUploadResult)
260
291
  meta.assets = {
261
292
  jwt: assetUploadResult.completionToken,
262
293
  };
294
+ // Initialize config from assetUploadResult if it exists
263
295
  if (assetUploadResult.assetConfig) {
264
- meta.assets.config = assetUploadResult.assetConfig;
296
+ meta.assets.config = {
297
+ ...assetUploadResult.assetConfig,
298
+ };
299
+ }
300
+ // If there's no config from assetUploadResult but we have props.assets,
301
+ // we need to create the config ourselves (this handles the case when no assets were uploaded)
302
+ if (!meta.assets.config && props.assets) {
303
+ meta.assets.config = createAssetConfig(props.assets);
265
304
  }
266
305
  }
267
306
  const bindings = (props.bindings ?? {});
@@ -538,9 +577,12 @@ async function getWorkerBindings(api, workerName, environment = "production") {
538
577
  * @param api CloudflareApi instance
539
578
  * @param workerName Name of the worker
540
579
  * @param assets Assets resource containing files to upload
580
+ * @param assetConfig Configuration for the assets
541
581
  * @returns Completion token for the assets upload
542
582
  */
543
- async function uploadAssets(api, workerName, assets) {
583
+ async function uploadAssets(api, workerName, assets, assetConfig) {
584
+ // Process the assets configuration once at the beginning
585
+ const processedConfig = createAssetConfig(assetConfig);
544
586
  // Generate the file manifest
545
587
  const fileMetadata = {};
546
588
  // Process each file in the assets
@@ -561,7 +603,10 @@ async function uploadAssets(api, workerName, assets) {
561
603
  const sessionData = (await uploadSessionResponse.json());
562
604
  // If there are no buckets, assets are already uploaded or empty
563
605
  if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
564
- return { completionToken: sessionData.result.jwt };
606
+ return {
607
+ completionToken: sessionData.result.jwt,
608
+ assetConfig: processedConfig,
609
+ };
565
610
  }
566
611
  // Upload the files in batches as specified by the API
567
612
  let completionToken = sessionData.result.jwt;
@@ -607,12 +652,10 @@ async function uploadAssets(api, workerName, assets) {
607
652
  completionToken = uploadData.result.jwt;
608
653
  }
609
654
  }
610
- // Return the final completion token
655
+ // Return the final completion token with asset configuration
611
656
  return {
612
657
  completionToken,
613
- assetConfig: {
614
- html_handling: "auto-trailing-slash",
615
- },
658
+ assetConfig: processedConfig,
616
659
  };
617
660
  }
618
661
  /**
package/lib/env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export interface Env {
2
+ [key: string]: Promise<string>;
3
+ <T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
4
+ }
5
+ export declare const env: Env;
package/lib/env.js ADDED
@@ -0,0 +1,24 @@
1
+ export const env = new Proxy(_env, {
2
+ get: (_, name) => _env(name),
3
+ apply: (_, __, args) => _env(...args),
4
+ });
5
+ async function _env(name, value, error) {
6
+ if (value !== undefined) {
7
+ return value;
8
+ }
9
+ else if (typeof process !== undefined) {
10
+ // we are in a node environment
11
+ return process.env[name];
12
+ }
13
+ else {
14
+ // we are in a browser environment
15
+ try {
16
+ const { env } = await import("cloudflare:workers");
17
+ if (name in env) {
18
+ return env[name];
19
+ }
20
+ }
21
+ catch (error) { }
22
+ }
23
+ throw new Error(error ?? `Environment variable ${name} is not set`);
24
+ }
@@ -49,7 +49,7 @@ async function generateProviderDocs({ provider, outDir, parallel, }) {
49
49
  reasoningEffort: "high",
50
50
  },
51
51
  },
52
- freeze: true,
52
+ freeze: false,
53
53
  temperature: 0.1,
54
54
  schema: type({
55
55
  groups: type({
@@ -96,7 +96,7 @@ async function generateProviderDocs({ provider, outDir, parallel, }) {
96
96
  return Document(`docs/${providerName}/${g.identifier}`, {
97
97
  title: g.identifier,
98
98
  path: path.join(providerDocsDir, `${g.filename.replace(".ts", "").replace(".md", "")}.md`),
99
- freeze: true,
99
+ freeze: false,
100
100
  model: {
101
101
  id: "claude-3-5-sonnet-latest",
102
102
  provider: "anthropic",
package/lib/secret.d.ts CHANGED
@@ -73,5 +73,9 @@ export declare function isSecret(binding: any): binding is Secret;
73
73
  */
74
74
  export declare function secret<S extends string | undefined>(unencrypted: S): Secret;
75
75
  export declare namespace secret {
76
- function env(name: string, value?: string, error?: string): Promise<Secret>;
76
+ interface Env {
77
+ [key: string]: Promise<Secret>;
78
+ (name: string, value?: string, error?: string): Promise<Secret>;
79
+ }
80
+ const env: Env;
77
81
  }
package/lib/secret.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { alchemy } from "./alchemy";
1
2
  /**
2
3
  * Internal wrapper for sensitive values like API keys and credentials.
3
4
  * When stored in alchemy state files, the value is automatically encrypted
@@ -83,8 +84,11 @@ export function secret(unencrypted) {
83
84
  return new Secret(unencrypted);
84
85
  }
85
86
  (function (secret) {
86
- async function env(name, value, error) {
87
- const alchemy = await import("./alchemy");
87
+ secret.env = new Proxy(_env, {
88
+ get: (_, name) => _env(name),
89
+ apply: (_, __, args) => _env(...args),
90
+ });
91
+ async function _env(name, value, error) {
88
92
  const result = await alchemy.env(name, value, error);
89
93
  if (typeof result === "string") {
90
94
  return secret(result);
@@ -93,5 +97,4 @@ export function secret(unencrypted) {
93
97
  throw new Error(`Secret environment variable ${name} is not a string`);
94
98
  }
95
99
  }
96
- secret.env = env;
97
100
  })(secret || (secret = {}));
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
7
7
  "publish:npm": "cp ../README.md . && rm -rf ./*.tsbuildinfo && rm -rf ./lib && tsc -b && npm publish && rm README.md",
8
8
  "docs:gen": "rm -rf ./docs && typedoc"
9
9
  },
10
- "files": ["lib", "src"],
10
+ "files": [
11
+ "lib",
12
+ "src"
13
+ ],
11
14
  "exports": {
12
15
  ".": "./lib/index.js",
13
16
  "./ai": "./lib/ai/index.js",
package/src/alchemy.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import { destroy, DestroyedSignal } from "./destroy";
5
+ import { env } from "./env";
5
6
  import type { PendingResource } from "./resource";
6
7
  import { Scope } from "./scope";
7
8
  import { secret } from "./secret";
@@ -81,6 +82,11 @@ export interface Alchemy {
81
82
  (template: TemplateStringsArray, ...values: any[]): Promise<string>;
82
83
  }
83
84
 
85
+ _alchemy.destroy = destroy;
86
+ _alchemy.run = run;
87
+ _alchemy.secret = secret;
88
+ _alchemy.env = env;
89
+
84
90
  /**
85
91
  * Implementation of the alchemy function that handles both application scoping
86
92
  * and template string interpolation.
@@ -214,10 +220,6 @@ async function _alchemy(
214
220
  ].join("\n");
215
221
  }
216
222
  }
217
- _alchemy.destroy = destroy;
218
- _alchemy.run = run;
219
- _alchemy.secret = secret;
220
- _alchemy.env = env;
221
223
 
222
224
  export interface AlchemyOptions {
223
225
  /**
@@ -349,25 +351,3 @@ async function run<T>(
349
351
  await _scope.finalize();
350
352
  }
351
353
  }
352
-
353
- export async function env<T = string>(
354
- name: string,
355
- value?: T | undefined,
356
- error?: string
357
- ): Promise<T> {
358
- if (value !== undefined) {
359
- return value;
360
- } else if (typeof process !== undefined) {
361
- // we are in a node environment
362
- return process.env[name]! as T;
363
- } else {
364
- // we are in a browser environment
365
- try {
366
- const { env } = await import("cloudflare:workers");
367
- if (name in env) {
368
- return env[name as keyof typeof env];
369
- }
370
- } catch (error) {}
371
- }
372
- throw new Error(error ?? `Environment variable ${name} is not set`);
373
- }
@@ -13,9 +13,11 @@ export * from "./permission-groups";
13
13
  export * from "./pipeline";
14
14
  export * from "./queue";
15
15
  export * from "./r2-rest-state-store";
16
+ export * from "./tanstack-start";
16
17
  export * from "./vectorize-index";
17
18
  export * from "./vectorize-metadata-index";
18
- export * from "./vite-site";
19
+ export * from "./vite";
20
+ export * from "./website";
19
21
  export * from "./worker";
20
22
  export { Workflow } from "./workflow";
21
23
  export * from "./wrangler.json";
@@ -0,0 +1,26 @@
1
+ import type { Assets } from "./assets";
2
+ import type { Bindings } from "./bindings";
3
+ import { Website, type WebsiteProps } from "./website";
4
+ import type { Worker } from "./worker";
5
+
6
+ export interface TanStackStartProps<B extends Bindings>
7
+ extends WebsiteProps<B> {}
8
+
9
+ // don't allow the ASSETS to be overriden
10
+ export type TanStackStart<B extends Bindings> = B extends { ASSETS: any }
11
+ ? never
12
+ : Worker<B & { ASSETS: Assets }>;
13
+
14
+ export async function TanStackStart<B extends Bindings>(
15
+ id: string,
16
+ props?: Partial<TanStackStartProps<B>>
17
+ ): Promise<TanStackStart<B>> {
18
+ return Website(id, {
19
+ ...props,
20
+ command: props?.command ?? "bun run build",
21
+ wrangler: props?.wrangler ?? false,
22
+ entrypoint: props?.entrypoint ?? ".output/server/index.mjs",
23
+ compatibilityFlags: ["nodejs_compat", ...(props?.compatibilityFlags ?? [])],
24
+ assets: props?.assets ?? ".output/public",
25
+ });
26
+ }
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Assets } from "./assets";
4
+ import type { Bindings } from "./bindings";
5
+ import { Website, type WebsiteProps } from "./website";
6
+ import type { Worker } from "./worker";
7
+
8
+ export interface ViteProps<B extends Bindings> extends WebsiteProps<B> {}
9
+
10
+ // don't allow the ASSETS to be overriden
11
+ export type Vite<B extends Bindings> = B extends { ASSETS: any }
12
+ ? never
13
+ : Worker<B & { ASSETS: Assets }>;
14
+
15
+ export async function Vite<B extends Bindings>(
16
+ id: string,
17
+ props: ViteProps<B>
18
+ ): Promise<Vite<B>> {
19
+ return Website(id, {
20
+ ...props,
21
+ assets:
22
+ props.assets ??
23
+ (await (async () => {
24
+ try {
25
+ await fs.access(path.join("dist", "client", "index.html"));
26
+ return path.join(".", "dist", "client");
27
+ } catch {
28
+ return path.join(".", "dist");
29
+ }
30
+ })()),
31
+ });
32
+ }
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { alchemy } from "../alchemy";
4
+ import { Exec } from "../os";
5
+ import { Assets } from "./assets";
6
+ import type { Bindings } from "./bindings";
7
+ import { Worker, type AssetsConfig, type WorkerProps } from "./worker";
8
+ import { WranglerJson } from "./wrangler.json";
9
+
10
+ export interface WebsiteProps<B extends Bindings>
11
+ extends Omit<WorkerProps<B>, "name" | "assets"> {
12
+ /**
13
+ * The command to run to build the site
14
+ */
15
+ command: string;
16
+ /**
17
+ * The name of the worker
18
+ *
19
+ * @default id
20
+ */
21
+ name?: string;
22
+ /**
23
+ * The entrypoint to your server
24
+ *
25
+ * @default - a simple server that serves static assets is generated
26
+ */
27
+ main?: string;
28
+ /**
29
+ * The directory containing your static assets
30
+ *
31
+ * @default "./dist"
32
+ */
33
+ assets?:
34
+ | string
35
+ | ({
36
+ dist?: string;
37
+ } & AssetsConfig);
38
+ /**
39
+ * @default process.cwd()
40
+ */
41
+ cwd?: string;
42
+
43
+ /**
44
+ * Write a wrangler.jsonc file
45
+ *
46
+ * @default - no wrangler.jsonc file is written
47
+ */
48
+ wrangler?: boolean | string;
49
+ }
50
+
51
+ export type Website<B extends Bindings> = B extends { ASSETS: any }
52
+ ? never
53
+ : Worker<B & { ASSETS: Assets }>;
54
+
55
+ export async function Website<B extends Bindings>(
56
+ id: string,
57
+ props: WebsiteProps<B>
58
+ ): Promise<Website<B>> {
59
+ if (props.bindings?.ASSETS) {
60
+ throw new Error("ASSETS binding is reserved for internal use");
61
+ }
62
+
63
+ return alchemy.run(id, async () => {
64
+ // building the site requires a wrangler.jsonc file to start
65
+ // - so initialize an empty one if it doesn't exist
66
+
67
+ const cwd = path.resolve(props.cwd || process.cwd());
68
+ const fileName =
69
+ typeof props.wrangler === "boolean" ? "wrangler.jsonc" : props.wrangler;
70
+ const wranglerPath =
71
+ fileName && path.relative(cwd, path.join(cwd, fileName));
72
+
73
+ if (props.wrangler) {
74
+ try {
75
+ await fs.access(wranglerPath!);
76
+ } catch {
77
+ await fs.writeFile(
78
+ wranglerPath!,
79
+ JSON.stringify(
80
+ {
81
+ name: id,
82
+ main: props.main,
83
+ compatibility_date: new Date().toISOString().split("T")[0],
84
+ },
85
+ null,
86
+ 2
87
+ )
88
+ );
89
+ }
90
+ }
91
+
92
+ await Exec("build", {
93
+ command: props.command,
94
+ });
95
+
96
+ const dist =
97
+ typeof props.assets === "string"
98
+ ? props.assets
99
+ : (props.assets?.dist ?? "dist");
100
+
101
+ const staticAssets = await Assets("assets", {
102
+ path: dist,
103
+ });
104
+
105
+ const worker = await Worker("worker", {
106
+ ...props,
107
+ name: props.name ?? id,
108
+ entrypoint: props.main,
109
+ assets: {
110
+ html_handling: "auto-trailing-slash",
111
+ not_found_handling: "single-page-application",
112
+ run_worker_first: false,
113
+ ...(typeof props.assets === "string" ? {} : props.assets),
114
+ },
115
+ script: props.main
116
+ ? undefined
117
+ : `
118
+ export default {
119
+ async fetch(request, env) {
120
+ return new Response("Not Found", { status: 404 });
121
+ },
122
+ };`,
123
+ url: true,
124
+ adopt: true,
125
+ bindings: {
126
+ ...props.bindings,
127
+ ASSETS: staticAssets,
128
+ },
129
+ });
130
+
131
+ if (props.wrangler) {
132
+ await WranglerJson("wrangler.jsonc", {
133
+ path: wranglerPath,
134
+ worker,
135
+ });
136
+ }
137
+
138
+ return worker as Website<B>;
139
+ });
140
+ }
@@ -19,6 +19,50 @@ import type { WorkerScriptMetadata } from "./worker-metadata";
19
19
  import type { SingleStepMigration } from "./worker-migration";
20
20
  import { upsertWorkflow, type Workflow } from "./workflow";
21
21
 
22
+ /**
23
+ * Configuration options for static assets
24
+ */
25
+ export interface AssetsConfig {
26
+ /**
27
+ * The contents of a _headers file (used to attach custom headers on asset responses)
28
+ */
29
+ _headers?: string;
30
+
31
+ /**
32
+ * The contents of a _redirects file (used to apply redirects or proxy paths ahead of asset serving)
33
+ */
34
+ _redirects?: string;
35
+
36
+ /**
37
+ * Determines the redirects and rewrites of requests for HTML content
38
+ * @default "auto-trailing-slash"
39
+ */
40
+ html_handling?:
41
+ | "auto-trailing-slash"
42
+ | "force-trailing-slash"
43
+ | "drop-trailing-slash"
44
+ | "none";
45
+
46
+ /**
47
+ * Determines the response when a request does not match a static asset, and there is no Worker script
48
+ */
49
+ not_found_handling?: "none" | "404-page" | "single-page-application";
50
+
51
+ /**
52
+ * When true, requests will always invoke the Worker script.
53
+ * Otherwise, attempt to serve an asset matching the request, falling back to the Worker script.
54
+ */
55
+ run_worker_first?: boolean;
56
+
57
+ /**
58
+ * When true and the incoming request matches an asset, that will be served instead of invoking the Worker script.
59
+ * When false, requests will always invoke the Worker script.
60
+ * @default true
61
+ * @deprecated
62
+ */
63
+ serve_directly?: boolean;
64
+ }
65
+
22
66
  /**
23
67
  * Properties for creating or updating a Worker
24
68
  */
@@ -110,6 +154,11 @@ export interface WorkerProps<B extends Bindings = Bindings>
110
154
  * The compatibility flags for the worker
111
155
  */
112
156
  compatibilityFlags?: string[];
157
+
158
+ /**
159
+ * Configuration for static assets
160
+ */
161
+ assets?: AssetsConfig;
113
162
  }
114
163
 
115
164
  /**
@@ -146,6 +195,11 @@ export interface Worker<B extends Bindings = Bindings>
146
195
  */
147
196
  bindings: B | undefined;
148
197
 
198
+ /**
199
+ * Configuration for static assets
200
+ */
201
+ assets?: AssetsConfig;
202
+
149
203
  // phantom property (for typeof myWorker.Env)
150
204
  Env: {
151
205
  [bindingName in keyof B]: Bound<B[bindingName]>;
@@ -291,7 +345,8 @@ export const Worker = Resource(
291
345
  assetUploadResult = await uploadAssets(
292
346
  api,
293
347
  workerName,
294
- assetBinding.assets
348
+ assetBinding.assets,
349
+ props.assets
295
350
  );
296
351
  }
297
352
 
@@ -342,6 +397,8 @@ export const Worker = Resource(
342
397
  createdAt: now,
343
398
  updatedAt: now,
344
399
  url: workerUrl,
400
+ // Include assets configuration in the output
401
+ assets: props.assets,
345
402
  // phantom property
346
403
  Env: undefined!,
347
404
  });
@@ -480,19 +537,50 @@ interface WorkerMetadata {
480
537
  assets?: {
481
538
  jwt?: string;
482
539
  keep_assets?: boolean;
483
- config?: {
484
- html_handling?: "auto-trailing-slash" | "none";
485
- not_found_handling?: "none" | "fall-through";
486
- };
540
+ config?: AssetsConfig;
487
541
  };
488
542
  }
489
543
 
490
544
  interface AssetUploadResult {
491
545
  completionToken: string;
492
- assetConfig?: {
493
- html_handling?: "auto-trailing-slash" | "none";
494
- not_found_handling?: "none" | "fall-through";
546
+ assetConfig?: AssetsConfig;
547
+ }
548
+
549
+ /**
550
+ * Creates asset configuration object from provided config or defaults
551
+ */
552
+ function createAssetConfig(config?: AssetsConfig): AssetsConfig {
553
+ const assetConfig: AssetsConfig = {
554
+ html_handling: "auto-trailing-slash",
495
555
  };
556
+
557
+ if (config) {
558
+ if (config._headers !== undefined) {
559
+ assetConfig._headers = config._headers;
560
+ }
561
+
562
+ if (config._redirects !== undefined) {
563
+ assetConfig._redirects = config._redirects;
564
+ }
565
+
566
+ if (config.html_handling !== undefined) {
567
+ assetConfig.html_handling = config.html_handling;
568
+ }
569
+
570
+ if (config.not_found_handling !== undefined) {
571
+ assetConfig.not_found_handling = config.not_found_handling;
572
+ }
573
+
574
+ if (config.run_worker_first !== undefined) {
575
+ assetConfig.run_worker_first = config.run_worker_first;
576
+ }
577
+
578
+ if (config.serve_directly !== undefined) {
579
+ assetConfig.serve_directly = config.serve_directly;
580
+ }
581
+ }
582
+
583
+ return assetConfig;
496
584
  }
497
585
 
498
586
  async function prepareWorkerMetadata<B extends Bindings>(
@@ -526,8 +614,17 @@ async function prepareWorkerMetadata<B extends Bindings>(
526
614
  jwt: assetUploadResult.completionToken,
527
615
  };
528
616
 
617
+ // Initialize config from assetUploadResult if it exists
529
618
  if (assetUploadResult.assetConfig) {
530
- meta.assets.config = assetUploadResult.assetConfig;
619
+ meta.assets.config = {
620
+ ...assetUploadResult.assetConfig,
621
+ };
622
+ }
623
+
624
+ // If there's no config from assetUploadResult but we have props.assets,
625
+ // we need to create the config ourselves (this handles the case when no assets were uploaded)
626
+ if (!meta.assets.config && props.assets) {
627
+ meta.assets.config = createAssetConfig(props.assets);
531
628
  }
532
629
  }
533
630
 
@@ -910,13 +1007,18 @@ interface UploadResponse {
910
1007
  * @param api CloudflareApi instance
911
1008
  * @param workerName Name of the worker
912
1009
  * @param assets Assets resource containing files to upload
1010
+ * @param assetConfig Configuration for the assets
913
1011
  * @returns Completion token for the assets upload
914
1012
  */
915
1013
  async function uploadAssets(
916
1014
  api: CloudflareApi,
917
1015
  workerName: string,
918
- assets: Assets
1016
+ assets: Assets,
1017
+ assetConfig?: WorkerProps["assets"]
919
1018
  ): Promise<AssetUploadResult> {
1019
+ // Process the assets configuration once at the beginning
1020
+ const processedConfig = createAssetConfig(assetConfig);
1021
+
920
1022
  // Generate the file manifest
921
1023
  const fileMetadata: Record<string, FileMetadata> = {};
922
1024
 
@@ -949,7 +1051,10 @@ async function uploadAssets(
949
1051
 
950
1052
  // If there are no buckets, assets are already uploaded or empty
951
1053
  if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
952
- return { completionToken: sessionData.result.jwt };
1054
+ return {
1055
+ completionToken: sessionData.result.jwt,
1056
+ assetConfig: processedConfig,
1057
+ };
953
1058
  }
954
1059
 
955
1060
  // Upload the files in batches as specified by the API
@@ -1013,12 +1118,10 @@ async function uploadAssets(
1013
1118
  }
1014
1119
  }
1015
1120
 
1016
- // Return the final completion token
1121
+ // Return the final completion token with asset configuration
1017
1122
  return {
1018
1123
  completionToken,
1019
- assetConfig: {
1020
- html_handling: "auto-trailing-slash",
1021
- },
1124
+ assetConfig: processedConfig,
1022
1125
  };
1023
1126
  }
1024
1127
 
package/src/env.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface Env {
2
+ [key: string]: Promise<string>;
3
+ <T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
4
+ }
5
+
6
+ export const env = new Proxy(_env, {
7
+ get: (_, name: string) => _env(name),
8
+ apply: (_, __, args: [string, any?, string?]) => _env(...args),
9
+ }) as Env;
10
+
11
+ async function _env<T = string>(
12
+ name: string,
13
+ value?: T | undefined,
14
+ error?: string
15
+ ): Promise<T> {
16
+ if (value !== undefined) {
17
+ return value;
18
+ } else if (typeof process !== undefined) {
19
+ // we are in a node environment
20
+ return process.env[name]! as T;
21
+ } else {
22
+ // we are in a browser environment
23
+ try {
24
+ const { env } = await import("cloudflare:workers");
25
+ if (name in env) {
26
+ return env[name as keyof typeof env];
27
+ }
28
+ } catch (error) {}
29
+ }
30
+ throw new Error(error ?? `Environment variable ${name} is not set`);
31
+ }
@@ -118,7 +118,7 @@ async function generateProviderDocs({
118
118
  reasoningEffort: "high",
119
119
  },
120
120
  },
121
- freeze: true,
121
+ freeze: false,
122
122
  temperature: 0.1,
123
123
  schema: type({
124
124
  groups: type({
@@ -178,7 +178,7 @@ async function generateProviderDocs({
178
178
  providerDocsDir,
179
179
  `${g.filename.replace(".ts", "").replace(".md", "")}.md`
180
180
  ),
181
- freeze: true,
181
+ freeze: false,
182
182
  model: {
183
183
  id: "claude-3-5-sonnet-latest",
184
184
  provider: "anthropic",
package/src/secret.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { alchemy } from "./alchemy";
2
+
1
3
  /**
2
4
  * Internal wrapper for sensitive values like API keys and credentials.
3
5
  * When stored in alchemy state files, the value is automatically encrypted
@@ -85,12 +87,21 @@ export function secret<S extends string | undefined>(unencrypted: S): Secret {
85
87
  }
86
88
 
87
89
  export namespace secret {
88
- export async function env(
90
+ export interface Env {
91
+ [key: string]: Promise<Secret>;
92
+ (name: string, value?: string, error?: string): Promise<Secret>;
93
+ }
94
+
95
+ export const env = new Proxy(_env, {
96
+ get: (_, name: string) => _env(name),
97
+ apply: (_, __, args: [string, any?, string?]) => _env(...args),
98
+ }) as Env;
99
+
100
+ async function _env(
89
101
  name: string,
90
102
  value?: string,
91
103
  error?: string
92
104
  ): Promise<Secret> {
93
- const alchemy = await import("./alchemy");
94
105
  const result = await alchemy.env(name, value, error);
95
106
  if (typeof result === "string") {
96
107
  return secret(result);
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+
3
4
  /**
4
5
  * Process markdown files with frontmatter to generate navigation items
5
6
  * @param directoryPath The directory containing markdown files
@@ -1,57 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { alchemy } from "../alchemy";
4
- import { Exec } from "../os";
5
- import { Assets } from "./assets";
6
- import { Worker } from "./worker";
7
- import { WranglerJson } from "./wrangler.json";
8
- export async function ViteSite(id, props) {
9
- if (props.bindings?.ASSETS) {
10
- throw new Error("ASSETS binding is reserved for internal use");
11
- }
12
- // @ts-ignore - we know the types are correct
13
- return await alchemy.run(id, async () => {
14
- // Create minimal wrangler.jsonc if it doesn't exist
15
- // `building the site requires a wrangler.jsonc file to start - so initialize an empty one if it doesn't exist`
16
- const cwd = props.cwd || process.cwd();
17
- const wranglerPath = path.join(cwd, "wrangler.jsonc");
18
- try {
19
- await fs.access(wranglerPath);
20
- }
21
- catch {
22
- await fs.writeFile(wranglerPath, JSON.stringify({
23
- name: id,
24
- main: props.main,
25
- compatibility_date: new Date().toISOString().split("T")[0],
26
- }, null, 2));
27
- }
28
- await Exec("build", {
29
- command: props.command,
30
- });
31
- const staticAssets = await Assets("assets", {
32
- path: props.assets ?? "./dist",
33
- });
34
- const worker = await Worker("worker", {
35
- name: props.name ?? id,
36
- entrypoint: props.main,
37
- script: props.main
38
- ? undefined
39
- : `
40
- export default {
41
- async fetch(request, env) {
42
- return env.ASSETS.fetch(request);
43
- },
44
- };`,
45
- url: true,
46
- adopt: true,
47
- bindings: {
48
- ...props.bindings,
49
- ASSETS: staticAssets,
50
- },
51
- });
52
- await WranglerJson("wrangler.jsonc", {
53
- worker,
54
- });
55
- return worker;
56
- });
57
- }
@@ -1,113 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { alchemy } from "../alchemy";
4
- import { Exec } from "../os";
5
- import { Assets } from "./assets";
6
- import type { Bindings } from "./bindings";
7
- import { Worker } from "./worker";
8
- import { WranglerJson } from "./wrangler.json";
9
-
10
- export interface ViteSiteProps<B extends Bindings> {
11
- /**
12
- * The command to run to build the site
13
- */
14
- command: string;
15
- /**
16
- * The name of the worker
17
- *
18
- * @default id
19
- */
20
- name?: string;
21
- /**
22
- * The entrypoint to your server
23
- *
24
- * @default - a simple server that serves static assets is generated
25
- */
26
- main?: string;
27
- /**
28
- * The directory containing your static assets
29
- *
30
- * @default "./dist"
31
- */
32
- assets?: string;
33
- /**
34
- * The bindings to pass to the worker
35
- */
36
- bindings?: B;
37
- /**
38
- * @default process.cwd()
39
- */
40
- cwd?: string;
41
- }
42
-
43
- export type ViteSite<B extends Bindings> = Promise<
44
- // don't allow the ASSETS to be overriden
45
- B extends { ASSETS: any } ? never : Worker<B & { ASSETS: Assets }>
46
- >;
47
-
48
- export async function ViteSite<B extends Bindings>(
49
- id: string,
50
- props: ViteSiteProps<B>
51
- ): ViteSite<B> {
52
- if (props.bindings?.ASSETS) {
53
- throw new Error("ASSETS binding is reserved for internal use");
54
- }
55
-
56
- // @ts-ignore - we know the types are correct
57
- return await alchemy.run(id, async () => {
58
- // Create minimal wrangler.jsonc if it doesn't exist
59
-
60
- // `building the site requires a wrangler.jsonc file to start - so initialize an empty one if it doesn't exist`
61
- const cwd = props.cwd || process.cwd();
62
- const wranglerPath = path.join(cwd, "wrangler.jsonc");
63
- try {
64
- await fs.access(wranglerPath);
65
- } catch {
66
- await fs.writeFile(
67
- wranglerPath,
68
- JSON.stringify(
69
- {
70
- name: id,
71
- main: props.main,
72
- compatibility_date: new Date().toISOString().split("T")[0],
73
- },
74
- null,
75
- 2
76
- )
77
- );
78
- }
79
-
80
- await Exec("build", {
81
- command: props.command,
82
- });
83
-
84
- const staticAssets = await Assets("assets", {
85
- path: props.assets ?? "./dist",
86
- });
87
-
88
- const worker = await Worker("worker", {
89
- name: props.name ?? id,
90
- entrypoint: props.main,
91
- script: props.main
92
- ? undefined
93
- : `
94
- export default {
95
- async fetch(request, env) {
96
- return env.ASSETS.fetch(request);
97
- },
98
- };`,
99
- url: true,
100
- adopt: true,
101
- bindings: {
102
- ...props.bindings,
103
- ASSETS: staticAssets,
104
- },
105
- });
106
-
107
- await WranglerJson("wrangler.jsonc", {
108
- worker,
109
- });
110
-
111
- return worker;
112
- });
113
- }