alchemy 0.36.0 → 0.37.1

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 (38) hide show
  1. package/lib/cloudflare/bindings.d.ts +1 -1
  2. package/lib/cloudflare/bindings.d.ts.map +1 -1
  3. package/lib/cloudflare/bound.d.ts +1 -1
  4. package/lib/cloudflare/bound.d.ts.map +1 -1
  5. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  6. package/lib/cloudflare/d1-database.js +0 -3
  7. package/lib/cloudflare/d1-database.js.map +1 -1
  8. package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
  9. package/lib/cloudflare/do-state-store/internal.js +35 -28
  10. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  11. package/lib/cloudflare/durable-object-namespace.d.ts +1 -1
  12. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -1
  13. package/lib/cloudflare/durable-object-namespace.js.map +1 -1
  14. package/lib/cloudflare/website.d.ts.map +1 -1
  15. package/lib/cloudflare/website.js +28 -13
  16. package/lib/cloudflare/website.js.map +1 -1
  17. package/lib/cloudflare/worker-metadata.d.ts +8 -5
  18. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  19. package/lib/cloudflare/worker-metadata.js +48 -11
  20. package/lib/cloudflare/worker-metadata.js.map +1 -1
  21. package/lib/cloudflare/worker.d.ts +18 -12
  22. package/lib/cloudflare/worker.d.ts.map +1 -1
  23. package/lib/cloudflare/worker.js +74 -68
  24. package/lib/cloudflare/worker.js.map +1 -1
  25. package/lib/stripe/price.d.ts +24 -0
  26. package/lib/stripe/price.d.ts.map +1 -1
  27. package/lib/stripe/price.js +30 -0
  28. package/lib/stripe/price.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/cloudflare/bindings.ts +1 -1
  31. package/src/cloudflare/bound.ts +1 -1
  32. package/src/cloudflare/d1-database.ts +0 -4
  33. package/src/cloudflare/do-state-store/internal.ts +45 -29
  34. package/src/cloudflare/durable-object-namespace.ts +2 -3
  35. package/src/cloudflare/website.ts +40 -16
  36. package/src/cloudflare/worker-metadata.ts +101 -19
  37. package/src/cloudflare/worker.ts +104 -148
  38. package/src/stripe/price.ts +47 -1
@@ -48,7 +48,7 @@ export type Binding =
48
48
  | D1DatabaseResource
49
49
  | DispatchNamespaceResource
50
50
  | AnalyticsEngineDataset
51
- | DurableObjectNamespace<Rpc.DurableObjectBranded | undefined>
51
+ | DurableObjectNamespace<any>
52
52
  | HyperdriveResource
53
53
  | Images
54
54
  | KVNamespaceResource
@@ -36,7 +36,7 @@ type BoundWorker<
36
36
  export type Bound<T extends Binding> = T extends _DurableObjectNamespace<
37
37
  infer O
38
38
  >
39
- ? DurableObjectNamespace<O>
39
+ ? DurableObjectNamespace<O & Rpc.DurableObjectBranded>
40
40
  : T extends { type: "kv_namespace" }
41
41
  ? KVNamespace
42
42
  : T extends WorkerStub<infer RPC>
@@ -241,13 +241,9 @@ const D1DatabaseResource = Resource(
241
241
  const databaseName = props.name ?? id;
242
242
 
243
243
  if (this.phase === "delete") {
244
- logger.log("Deleting D1 database:", databaseName);
245
244
  if (props.delete !== false) {
246
- // Delete D1 database
247
- logger.log("Deleting D1 database:", databaseName);
248
245
  await deleteDatabase(api, this.output?.id);
249
246
  }
250
-
251
247
  // Return void (a deleted database has no content)
252
248
  return this.destroy();
253
249
  }
@@ -3,7 +3,7 @@ import { bundle } from "../../esbuild/index.ts";
3
3
  import { withExponentialBackoff } from "../../util/retry.ts";
4
4
  import { handleApiError } from "../api-error.ts";
5
5
  import type { CloudflareApi } from "../api.ts";
6
- import { putWorker } from "../worker.ts";
6
+ import type { WorkerMetadata } from "../worker-metadata.ts";
7
7
  import type { DOStateStoreAPI } from "./types.ts";
8
8
 
9
9
  interface DOStateStoreClientOptions {
@@ -116,33 +116,49 @@ export async function upsertStateStoreWorker(
116
116
  cache.set(key, TAG);
117
117
  return;
118
118
  }
119
- const script = await bundleWorkerScript();
120
- await putWorker(api, workerName, script, {
121
- main_module: "worker.js",
122
- compatibility_date: "2025-06-01",
123
- compatibility_flags: ["nodejs_compat"],
124
- bindings: [
125
- {
126
- name: "DOFS_STATE_STORE",
127
- type: "durable_object_namespace",
128
- class_name: "DOFSStateStore",
129
- },
130
- {
131
- name: "DOFS_TOKEN",
132
- type: "secret_text",
133
- text: token,
134
- },
135
- ],
136
- migrations: !found
137
- ? {
138
- new_sqlite_classes: ["DOFSStateStore"],
139
- }
140
- : undefined,
141
- tags: [TAG],
142
- observability: {
143
- enabled: true,
144
- },
145
- });
119
+ const formData = new FormData();
120
+ formData.append("worker.js", await bundleWorkerScript());
121
+ formData.append(
122
+ "metadata",
123
+ new Blob([
124
+ JSON.stringify({
125
+ main_module: "worker.js",
126
+ compatibility_date: "2025-06-01",
127
+ compatibility_flags: ["nodejs_compat"],
128
+ bindings: [
129
+ {
130
+ name: "DOFS_STATE_STORE",
131
+ type: "durable_object_namespace",
132
+ class_name: "DOFSStateStore",
133
+ },
134
+ {
135
+ name: "DOFS_TOKEN",
136
+ type: "secret_text",
137
+ text: token,
138
+ },
139
+ ],
140
+ migrations: !found
141
+ ? {
142
+ new_sqlite_classes: ["DOFSStateStore"],
143
+ }
144
+ : undefined,
145
+ tags: [TAG],
146
+ observability: {
147
+ enabled: true,
148
+ },
149
+ } satisfies WorkerMetadata),
150
+ ]),
151
+ );
152
+
153
+ // Put the worker with migration tag v1
154
+ const response = await api.post(
155
+ `/accounts/${api.accountId}/workers/scripts/${workerName}`,
156
+ formData,
157
+ );
158
+ if (!response.ok) {
159
+ throw await handleApiError(response, "upload", "worker", workerName);
160
+ }
161
+
146
162
  const subdomainRes = await api.post(
147
163
  `/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`,
148
164
  { enabled: true, preview_enabled: false },
@@ -151,7 +167,7 @@ export async function upsertStateStoreWorker(
151
167
  },
152
168
  );
153
169
  if (!subdomainRes.ok) {
154
- await handleApiError(
170
+ throw await handleApiError(
155
171
  subdomainRes,
156
172
  "creating worker subdomain",
157
173
  "worker",
@@ -41,9 +41,8 @@ export function isDurableObjectNamespace(
41
41
  * environment: "production"
42
42
  * });
43
43
  */
44
- export class DurableObjectNamespace<
45
- T extends Rpc.DurableObjectBranded | undefined = undefined,
46
- > implements DurableObjectNamespaceInput
44
+ export class DurableObjectNamespace<T = any>
45
+ implements DurableObjectNamespaceInput
47
46
  {
48
47
  public readonly type = "durable_object_namespace" as const;
49
48
  // alias for bindingName to be consistent with other bindings
@@ -115,33 +115,52 @@ export async function Website<B extends Bindings>(
115
115
  parent: Scope.current,
116
116
  },
117
117
  async () => {
118
+ // directory from which all relative paths are resolved
118
119
  const cwd = path.resolve(props.cwd || process.cwd());
119
- const fileName =
120
+
121
+ function resolveAbsPath<S extends string | undefined>(f: S): S {
122
+ return (
123
+ f ? (path.isAbsolute(f) ? f : path.resolve(cwd, f)) : undefined
124
+ ) as S;
125
+ }
126
+
127
+ // absolute path to the wrangler.jsonc file
128
+ const wranglerJsonPath = resolveAbsPath(
120
129
  typeof wrangler === "boolean"
121
130
  ? "wrangler.jsonc"
122
131
  : typeof wrangler === "string"
123
132
  ? wrangler
124
- : (wrangler?.path ?? "wrangler.jsonc");
125
- const wranglerPath =
126
- fileName && path.relative(cwd, path.join(cwd, fileName));
127
- const wranglerMain =
128
- typeof wrangler === "object"
129
- ? (wrangler.main ?? props.main)
130
- : props.main;
133
+ : (wrangler?.path ?? "wrangler.jsonc"),
134
+ );
131
135
 
132
- const workerName = props.name ?? id;
136
+ // absolute path to the worker entrypoint
137
+ const mainPath = resolveAbsPath(props.main);
133
138
 
134
- const assetDir =
139
+ // absolute path to the worker entrypoint (if different in wrangler.jsonc)
140
+ const wranglerMainPath = resolveAbsPath(
141
+ typeof wrangler === "object"
142
+ ? (wrangler.main ?? props.main!)
143
+ : props.main,
144
+ );
145
+
146
+ // absolute path to the assets directory
147
+ const assetsDirPath = resolveAbsPath(
135
148
  typeof props.assets === "string"
136
149
  ? props.assets
137
- : (props.assets?.dist ?? "dist");
150
+ : (props.assets?.dist ?? "dist"),
151
+ );
152
+
153
+ const workerName = props.name ?? id;
138
154
 
139
155
  const workerProps = {
140
156
  ...props,
141
157
  compatibilityDate:
142
158
  props.compatibilityDate ?? DEFAULT_COMPATIBILITY_DATE,
143
159
  name: workerName,
144
- entrypoint: props.main,
160
+ entrypoint: mainPath
161
+ ? // this path should be relative to the process.cwd() because it is used by esbuild
162
+ path.relative(process.cwd(), mainPath)
163
+ : undefined,
145
164
  assets: {
146
165
  html_handling: "auto-trailing-slash",
147
166
  not_found_handling: props.spa ? "single-page-application" : "none",
@@ -161,14 +180,18 @@ export default {
161
180
  } as WorkerProps<any> & { name: string };
162
181
 
163
182
  if (wrangler) {
183
+ // paths in wrangler.jsonc must be relative to it
184
+ const relativeToWrangler = <S extends string | undefined>(f: S): S =>
185
+ (f ? path.relative(path.dirname(wranglerJsonPath), f) : f) as S;
164
186
  await WranglerJson("wrangler.jsonc", {
165
- path: wranglerPath,
187
+ path: wranglerJsonPath,
166
188
  worker: workerProps,
167
- main: wranglerMain,
189
+ main: relativeToWrangler(wranglerMainPath),
168
190
  // hard-code the assets directory because we haven't yet included the assets binding
169
191
  assets: {
170
192
  binding: "ASSETS",
171
- directory: assetDir,
193
+ // path must be relative to the wrangler.jsonc file
194
+ directory: relativeToWrangler(assetsDirPath),
172
195
  },
173
196
  });
174
197
  }
@@ -189,7 +212,8 @@ export default {
189
212
  // we don't include the Assets binding until after build to make sure the asset manifest is correct
190
213
  // we generate the wrangler.json using all the bind
191
214
  ASSETS: await Assets("assets", {
192
- path: assetDir,
215
+ // Assets are discovered from proces.cwd(), not Website.cwd or wrangler.jsonc
216
+ path: path.relative(process.cwd(), assetsDirPath),
193
217
  }),
194
218
  },
195
219
  } as WorkerProps<any> & { name: string })) as Website<B>;
@@ -1,7 +1,6 @@
1
1
  import path from "node:path";
2
- import type { Context } from "../context.ts";
3
2
  import { logger } from "../util/logger.ts";
4
- import { slugify } from "../util/slugify.ts";
3
+ import type { CloudflareApi } from "./api.ts";
5
4
  import {
6
5
  Self,
7
6
  type Bindings,
@@ -13,8 +12,11 @@ import {
13
12
  type DurableObjectNamespace,
14
13
  } from "./durable-object-namespace.ts";
15
14
  import { createAssetConfig, type AssetUploadResult } from "./worker-assets.ts";
16
- import type { SingleStepMigration } from "./worker-migration.ts";
17
- import type { AssetsConfig, Worker, WorkerProps } from "./worker.ts";
15
+ import type {
16
+ MultiStepMigration,
17
+ SingleStepMigration,
18
+ } from "./worker-migration.ts";
19
+ import type { AssetsConfig, WorkerProps } from "./worker.ts";
18
20
 
19
21
  /**
20
22
  * Metadata returned by Cloudflare API for a worker script
@@ -193,17 +195,21 @@ export interface WorkerMetadata {
193
195
  }[];
194
196
  }
195
197
 
196
- export async function prepareWorkerMetadata<B extends Bindings>(
197
- ctx: Context<Worker<B>>,
198
- oldBindings: WorkerBindingSpec[] | undefined,
199
- oldTags: string[] | undefined,
198
+ export async function prepareWorkerMetadata(
199
+ api: CloudflareApi,
200
200
  props: WorkerProps & {
201
201
  compatibilityDate: string;
202
202
  compatibilityFlags: string[];
203
203
  workerName: string;
204
+ migrationTag?: string;
205
+ assetUploadResult?: AssetUploadResult;
204
206
  },
205
- assetUploadResult?: AssetUploadResult,
206
207
  ): Promise<WorkerMetadata> {
208
+ const oldSettings = await getWorkerSettings(api, props.workerName);
209
+ const oldTags: string[] | undefined =
210
+ oldSettings?.default_environment?.script?.tags;
211
+ const oldBindings = oldSettings?.bindings;
212
+
207
213
  // we use Cloudflare Worker tags to store a mapping between Alchemy's stable identifier and the binding name
208
214
  // e.g.
209
215
  // {
@@ -212,7 +218,7 @@ export async function prepareWorkerMetadata<B extends Bindings>(
212
218
  // will be stored as alchemy:do:stable-id:BINDING_NAME
213
219
  // TODO(sam): should we base64 encode to ensure no `:` collision risk?
214
220
  const bindingNameToStableId = Object.fromEntries(
215
- oldTags?.flatMap((tag) => {
221
+ oldTags?.flatMap((tag: string) => {
216
222
  // alchemy:do:{stableId}:{bindingName}
217
223
  if (tag.startsWith("alchemy:do:")) {
218
224
  const [, , stableId, bindingName] = tag.split(":");
@@ -222,6 +228,14 @@ export async function prepareWorkerMetadata<B extends Bindings>(
222
228
  }) ?? [],
223
229
  );
224
230
 
231
+ const oldMigrationTag = oldTags?.flatMap((tag: string) => {
232
+ if (tag.startsWith("alchemy:migration-tag:")) {
233
+ return [tag.slice("alchemy:migration-tag:".length)];
234
+ }
235
+ return [];
236
+ })[0];
237
+ const newMigrationTag = bumpMigrationTagVersion(oldMigrationTag);
238
+
225
239
  const deletedClasses = oldBindings?.flatMap((oldBinding) => {
226
240
  if (
227
241
  oldBinding.type === "durable_object_namespace" &&
@@ -284,7 +298,6 @@ export async function prepareWorkerMetadata<B extends Bindings>(
284
298
  },
285
299
  // TODO(sam): base64 encode instead? 0 collision risk vs readability.
286
300
  tags: [
287
- `alchemy:id:${slugify(ctx.fqn)}`,
288
301
  // encode a mapping table of Durable Object stable ID -> binding name
289
302
  // we use this to reliably compute class migrations based on server-side state
290
303
  ...Object.entries(props.bindings ?? {}).flatMap(
@@ -294,19 +307,21 @@ export async function prepareWorkerMetadata<B extends Bindings>(
294
307
  [`alchemy:do:${binding.id}:${bindingName}`]
295
308
  : [],
296
309
  ),
310
+ // encode the migraiton tag if there is one so we can avoid the failed PutWorker after adoption
311
+ ...(newMigrationTag ? [`alchemy:migration-tag:${newMigrationTag}`] : []),
297
312
  ],
298
313
  migrations: {
299
- new_classes: props.migrations?.new_classes ?? [],
300
- deleted_classes: [
301
- ...(deletedClasses ?? []),
302
- ...(props.migrations?.deleted_classes ?? []),
303
- ],
304
- renamed_classes: props.migrations?.renamed_classes ?? [],
305
- transferred_classes: props.migrations?.transferred_classes ?? [],
306
- new_sqlite_classes: props.migrations?.new_sqlite_classes ?? [],
314
+ old_tag: oldMigrationTag,
315
+ new_tag: newMigrationTag,
316
+ new_classes: [],
317
+ deleted_classes: [...(deletedClasses ?? [])],
318
+ renamed_classes: [],
319
+ transferred_classes: [],
320
+ new_sqlite_classes: [],
307
321
  },
308
322
  };
309
323
 
324
+ const assetUploadResult = props.assetUploadResult;
310
325
  // If we have asset upload results, add them to the metadata
311
326
  if (assetUploadResult) {
312
327
  meta.assets = {
@@ -577,3 +592,70 @@ export async function prepareWorkerMetadata<B extends Bindings>(
577
592
  }
578
593
  return meta;
579
594
  }
595
+
596
+ export function bumpMigrationTagVersion(tag?: string) {
597
+ if (tag) {
598
+ if (!tag.match(/^v\d+$/)) {
599
+ throw new Error(`Invalid tag format: ${tag}. Expected format: v<number>`);
600
+ }
601
+ return `v${Number.parseInt(tag.slice(1)) + 1}`;
602
+ }
603
+ return undefined;
604
+ }
605
+
606
+ interface WorkerSettings {
607
+ bindings: WorkerBindingSpec[];
608
+ compatibility_date: string;
609
+ compatibility_flags: string[];
610
+ migrations: SingleStepMigration | MultiStepMigration;
611
+ [key: string]: any;
612
+ }
613
+
614
+ async function getWorkerSettings(
615
+ api: CloudflareApi,
616
+ workerName: string,
617
+ ): Promise<WorkerSettings | undefined> {
618
+ // Fetch the bindings for a worker by calling the Cloudflare API endpoint:
619
+ // GET /accounts/:account_id/workers/scripts/:script_name/bindings
620
+ // See: https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/script_and_version_settings/methods/get/
621
+ const response = await api.get(
622
+ `/accounts/${api.accountId}/workers/scripts/${workerName}/settings`,
623
+ );
624
+ if (response.status === 404) {
625
+ return undefined;
626
+ }
627
+ if (!response.ok) {
628
+ throw new Error(
629
+ `Error getting worker bindings: ${response.status} ${response.statusText}`,
630
+ );
631
+ }
632
+ // The result is an object with a "result" property containing the bindings array
633
+ const { result, success, errors } = (await response.json()) as {
634
+ result: {
635
+ bindings: WorkerBindingSpec[];
636
+ compatibility_date: string;
637
+ compatibility_flags: string[];
638
+ migrations: SingleStepMigration | MultiStepMigration;
639
+ [key: string]: any;
640
+ };
641
+ success: boolean;
642
+ errors: Array<{
643
+ code: number;
644
+ message: string;
645
+ documentation_url: string;
646
+ [key: string]: any;
647
+ }>;
648
+ messages: Array<{
649
+ code: number;
650
+ message: string;
651
+ documentation_url: string;
652
+ [key: string]: any;
653
+ }>;
654
+ };
655
+ if (!success) {
656
+ throw new Error(
657
+ `Error getting worker bindings: ${response.status} ${response.statusText}\nErrors:\n${errors.map((e) => `- [${e.code}] ${e.message} (${e.documentation_url})`).join("\n")}`,
658
+ );
659
+ }
660
+ return result;
661
+ }