alchemy 0.1.12 → 0.1.13

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 (103) hide show
  1. package/lib/$.d.ts +12 -0
  2. package/lib/$.d.ts.map +1 -0
  3. package/lib/$.js +42 -0
  4. package/lib/$.js.map +1 -0
  5. package/lib/alchemize.d.ts.map +1 -1
  6. package/lib/alchemize.js +7 -0
  7. package/lib/alchemize.js.map +1 -1
  8. package/lib/apply.d.ts +2 -2
  9. package/lib/apply.d.ts.map +1 -1
  10. package/lib/apply.js +1 -1
  11. package/lib/apply.js.map +1 -1
  12. package/lib/cloudflare/api.d.ts +4 -0
  13. package/lib/cloudflare/api.d.ts.map +1 -1
  14. package/lib/cloudflare/api.js +6 -0
  15. package/lib/cloudflare/api.js.map +1 -1
  16. package/lib/cloudflare/bindings.d.ts +14 -4
  17. package/lib/cloudflare/bindings.d.ts.map +1 -1
  18. package/lib/cloudflare/bindings.js +0 -5
  19. package/lib/cloudflare/bindings.js.map +1 -1
  20. package/lib/cloudflare/durable-object-namespace.d.ts +23 -0
  21. package/lib/cloudflare/durable-object-namespace.d.ts.map +1 -0
  22. package/lib/cloudflare/durable-object-namespace.js +26 -0
  23. package/lib/cloudflare/durable-object-namespace.js.map +1 -0
  24. package/lib/cloudflare/index.d.ts +1 -0
  25. package/lib/cloudflare/index.d.ts.map +1 -1
  26. package/lib/cloudflare/index.js +1 -0
  27. package/lib/cloudflare/index.js.map +1 -1
  28. package/lib/cloudflare/kv-namespace.js +1 -1
  29. package/lib/cloudflare/kv-namespace.js.map +1 -1
  30. package/lib/cloudflare/static-site-router.d.ts +11 -2
  31. package/lib/cloudflare/static-site-router.d.ts.map +1 -1
  32. package/lib/cloudflare/static-site-router.js +98 -76
  33. package/lib/cloudflare/static-site-router.js.map +1 -1
  34. package/lib/cloudflare/static-site.d.ts +15 -15
  35. package/lib/cloudflare/static-site.d.ts.map +1 -1
  36. package/lib/cloudflare/static-site.js +41 -25
  37. package/lib/cloudflare/static-site.js.map +1 -1
  38. package/lib/cloudflare/worker-metadata.d.ts +130 -0
  39. package/lib/cloudflare/worker-metadata.d.ts.map +1 -0
  40. package/lib/cloudflare/worker-metadata.js +2 -0
  41. package/lib/cloudflare/worker-metadata.js.map +1 -0
  42. package/lib/cloudflare/worker-migration.d.ts +110 -0
  43. package/lib/cloudflare/worker-migration.d.ts.map +1 -0
  44. package/lib/cloudflare/worker-migration.js +11 -0
  45. package/lib/cloudflare/worker-migration.js.map +1 -0
  46. package/lib/cloudflare/worker.d.ts +29 -11
  47. package/lib/cloudflare/worker.d.ts.map +1 -1
  48. package/lib/cloudflare/worker.js +316 -322
  49. package/lib/cloudflare/worker.js.map +1 -1
  50. package/lib/destroy.d.ts +1 -1
  51. package/lib/destroy.d.ts.map +1 -1
  52. package/lib/destroy.js +2 -1
  53. package/lib/destroy.js.map +1 -1
  54. package/lib/esbuild.d.ts +1 -1
  55. package/lib/esbuild.d.ts.map +1 -1
  56. package/lib/esbuild.js +3 -1
  57. package/lib/esbuild.js.map +1 -1
  58. package/lib/global.d.ts +0 -2
  59. package/lib/global.d.ts.map +1 -1
  60. package/lib/global.js +0 -2
  61. package/lib/global.js.map +1 -1
  62. package/lib/index.d.ts +2 -0
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +2 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/output.d.ts +4 -3
  67. package/lib/output.d.ts.map +1 -1
  68. package/lib/output.js +10 -3
  69. package/lib/output.js.map +1 -1
  70. package/lib/print.d.ts +7 -0
  71. package/lib/print.d.ts.map +1 -0
  72. package/lib/print.js +10 -0
  73. package/lib/print.js.map +1 -0
  74. package/lib/resource.d.ts +8 -4
  75. package/lib/resource.d.ts.map +1 -1
  76. package/lib/resource.js +125 -91
  77. package/lib/resource.js.map +1 -1
  78. package/lib/scope.d.ts +3 -0
  79. package/lib/scope.d.ts.map +1 -1
  80. package/lib/scope.js +4 -12
  81. package/lib/scope.js.map +1 -1
  82. package/package.json +3 -2
  83. package/src/$.ts +69 -0
  84. package/src/alchemize.ts +12 -0
  85. package/src/apply.ts +4 -4
  86. package/src/cloudflare/api.ts +6 -0
  87. package/src/cloudflare/bindings.ts +37 -26
  88. package/src/cloudflare/durable-object-namespace.ts +43 -0
  89. package/src/cloudflare/index.ts +1 -0
  90. package/src/cloudflare/kv-namespace.ts +1 -1
  91. package/src/cloudflare/static-site-router.ts +124 -82
  92. package/src/cloudflare/static-site.ts +63 -47
  93. package/src/cloudflare/worker-metadata.ts +154 -0
  94. package/src/cloudflare/worker-migration.ts +145 -0
  95. package/src/cloudflare/worker.ts +526 -428
  96. package/src/destroy.ts +2 -2
  97. package/src/esbuild.ts +3 -0
  98. package/src/global.ts +0 -3
  99. package/src/index.ts +2 -1
  100. package/src/output.ts +24 -8
  101. package/src/print.ts +18 -0
  102. package/src/resource.ts +156 -115
  103. package/src/scope.ts +7 -12
@@ -3,362 +3,356 @@ import { apply } from "../apply";
3
3
  import { Bundle } from "../esbuild";
4
4
  import { Resource } from "../resource";
5
5
  import { createCloudflareApi } from "./api";
6
- export class Worker extends Resource("cloudflare::Worker", async (ctx, props) => {
6
+ import { isDurableObjectNamespace } from "./durable-object-namespace";
7
+ export class Worker extends Resource("cloudflare::Worker", {
8
+ alwaysUpdate: true,
9
+ }, async (ctx, props) => {
7
10
  // Create Cloudflare API client with automatic account discovery
8
11
  const api = await createCloudflareApi();
9
- // Validate that a name is provided
10
- if (!props.name) {
11
- throw new Error("Worker name is required - must be explicitly specified");
12
- }
13
12
  // Use the provided name
14
13
  const workerName = props.name;
15
14
  // Validate input - we need either script, entryPoint, or bundle
16
- if (!props.script && !props.entrypoint && !props.bundle) {
17
- throw new Error("One of script, entryPoint, or bundle must be provided");
18
- }
19
- // For create operations, check if a worker with the same name already exists
20
- if (ctx.event === "create") {
21
- const checkExistsResponse = await api.get(`/accounts/${api.accountId}/workers/scripts/${workerName}`);
22
- if (checkExistsResponse.ok) {
23
- throw new Error(`Worker with name '${workerName}' already exists. Please use a unique name.`);
24
- }
25
- else if (checkExistsResponse.status !== 404) {
26
- // If it's neither a success nor a 404, it's an unexpected error
27
- const errorData = await checkExistsResponse.json().catch(() => ({
28
- errors: [{ message: checkExistsResponse.statusText }],
29
- }));
30
- throw new Error(`Error checking if worker exists: ${errorData.errors?.[0]?.message || checkExistsResponse.statusText}`);
31
- }
32
- // If it's a 404, that means the worker doesn't exist yet, which is what we want
15
+ if (!props.script && !props.entrypoint) {
16
+ throw new Error("One of script or entryPoint must be provided");
33
17
  }
34
18
  if (ctx.event === "delete") {
35
- // Delete worker
36
- const deleteResponse = await api.delete(`/accounts/${api.accountId}/workers/scripts/${workerName}`);
37
- // Check for success (2xx status code)
38
- if (!deleteResponse.ok && deleteResponse.status !== 404) {
39
- const errorData = await deleteResponse
40
- .json()
41
- .catch(() => ({ errors: [{ message: deleteResponse.statusText }] }));
42
- console.error("Error deleting worker:", errorData.errors?.[0]?.message || deleteResponse.statusText);
19
+ return deleteWorker(ctx, api, workerName);
20
+ }
21
+ else if (ctx.event === "create") {
22
+ await assertWorkerDoesNotExist(ctx, api, workerName);
23
+ }
24
+ const oldBindings = await ctx.get("bindings");
25
+ const scriptMetadata = await prepareWorkerMetadata(ctx, oldBindings, props);
26
+ // Get the script content - either from props.script, or by bundling
27
+ const scriptContent = props.script ?? (await bundleWorkerScript(ctx, props));
28
+ // Upload the worker script
29
+ await putWorker(ctx, api, workerName, scriptContent, scriptMetadata);
30
+ // TODO: it is less than ideal that this can fail, resulting in state problem
31
+ await ctx.set("bindings", props.bindings);
32
+ // Handle routes if requested
33
+ await setupRoutes(api, workerName, props.routes || []);
34
+ // Handle worker URL if requested
35
+ const workerUrl = await configureURL(ctx, api, workerName, props.url ?? false);
36
+ // Get current timestamp
37
+ const now = Date.now();
38
+ // Construct the output
39
+ const output = {
40
+ id: workerName,
41
+ name: workerName,
42
+ script: scriptContent,
43
+ format: props.format || "esm", // Include format in the output
44
+ routes: props.routes || [],
45
+ bindings: props.bindings || [],
46
+ env: props.env,
47
+ observability: scriptMetadata.observability,
48
+ createdAt: now,
49
+ updatedAt: now,
50
+ url: workerUrl,
51
+ };
52
+ return output;
53
+ }) {
54
+ }
55
+ async function deleteWorker(ctx, api, workerName) {
56
+ // Delete worker
57
+ const deleteResponse = await api.delete(`/accounts/${api.accountId}/workers/scripts/${workerName}`);
58
+ // Check for success (2xx status code)
59
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
60
+ const errorData = await deleteResponse
61
+ .json()
62
+ .catch(() => ({ errors: [{ message: deleteResponse.statusText }] }));
63
+ console.error("Error deleting worker:", errorData.errors?.[0]?.message || deleteResponse.statusText);
64
+ }
65
+ // Also remove any associated routes if they exist
66
+ if (ctx.output?.routes && ctx.output.routes.length > 0 && api.zoneId) {
67
+ // First get existing routes to find their IDs
68
+ const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
69
+ if (!routesResponse.ok) {
70
+ throw new Error(`Could not fetch routes for cleanup: ${routesResponse.status} ${routesResponse.statusText}`);
43
71
  }
44
- // Also remove any associated routes if they exist
45
- if (ctx.output?.routes && ctx.output.routes.length > 0 && api.zoneId) {
46
- // First get existing routes to find their IDs
47
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
48
- if (routesResponse.ok) {
49
- const routesData = await routesResponse.json();
50
- const existingRoutes = routesData.result || [];
51
- for (const route of existingRoutes) {
52
- if (ctx.output.routes.includes(route.pattern)) {
53
- // Delete the route
54
- const routeDeleteResponse = await api.delete(`/zones/${api.zoneId}/workers/routes/${route.id}`);
55
- if (!routeDeleteResponse.ok) {
56
- console.warn(`Failed to delete route ${route.pattern}: ${routeDeleteResponse.status} ${routeDeleteResponse.statusText}`);
57
- }
58
- }
72
+ const routesData = await routesResponse.json();
73
+ const existingRoutes = routesData.result || [];
74
+ for (const route of existingRoutes) {
75
+ if (ctx.output.routes.includes(route.pattern)) {
76
+ // Delete the route
77
+ const routeDeleteResponse = await api.delete(`/zones/${api.zoneId}/workers/routes/${route.id}`);
78
+ if (!routeDeleteResponse.ok) {
79
+ console.warn(`Failed to delete route ${route.pattern}: ${routeDeleteResponse.status} ${routeDeleteResponse.statusText}`);
59
80
  }
60
81
  }
61
- else {
62
- console.warn("Could not fetch routes for cleanup:", routesResponse.status, routesResponse.statusText);
63
- }
64
- }
65
- // Disable the URL if it was enabled
66
- if (ctx.output?.url) {
67
- try {
68
- await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, JSON.stringify({ enabled: false }), {
69
- headers: { "Content-Type": "application/json" },
70
- });
71
- }
72
- catch (error) {
73
- console.warn("Failed to disable worker URL during deletion:", error);
74
- }
75
82
  }
76
- // Return minimal output for deleted state
77
- return {
78
- ...props,
79
- id: "",
80
- createdAt: 0,
81
- updatedAt: 0,
82
- url: undefined,
83
- };
84
83
  }
85
- else {
86
- // Get the script content - either from props.script, or by bundling
87
- let scriptContent;
88
- if (props.script) {
89
- // Use the provided script
90
- scriptContent = props.script;
84
+ // Disable the URL if it was enabled
85
+ if (ctx.output?.url) {
86
+ try {
87
+ await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, JSON.stringify({ enabled: false }), {
88
+ headers: { "Content-Type": "application/json" },
89
+ });
91
90
  }
92
- else if (props.bundle) {
93
- // Use a pre-configured Bundle resource
94
- const bundleOutput = await apply(props.bundle);
95
- scriptContent = await fs.readFile(bundleOutput.path, "utf-8");
91
+ catch (error) {
92
+ console.warn("Failed to disable worker URL during deletion:", error);
96
93
  }
97
- else if (props.entrypoint) {
98
- // Create and use a Bundle resource with worker-optimized configuration
99
- const defaultBundleOptions = {
100
- format: props.format === "cjs" ? "cjs" : "esm", // Use the specified format or default to ESM
101
- target: "es2020",
102
- platform: "browser",
103
- minify: true,
104
- options: {
105
- keepNames: true, // Important for Durable Object classes
106
- },
107
- };
108
- // Merge with user-provided options
109
- const bundleOptions = {
110
- ...defaultBundleOptions,
111
- ...(props.bundleOptions || {}),
112
- };
113
- // Create the bundle
114
- const bundle = new Bundle("bundle", {
115
- entryPoint: props.entrypoint,
116
- ...bundleOptions,
94
+ }
95
+ // Return minimal output for deleted state
96
+ return;
97
+ }
98
+ async function putWorker(ctx, api, workerName, scriptContent, scriptMetadata) {
99
+ const scriptName = scriptMetadata.main_module ?? scriptMetadata.body_part;
100
+ // Create FormData for the upload
101
+ const formData = new FormData();
102
+ // Add the actual script content as a named file part
103
+ formData.append(scriptName, new Blob([scriptContent], {
104
+ type: scriptMetadata.main_module
105
+ ? "application/javascript+module"
106
+ : "application/javascript",
107
+ }), scriptName);
108
+ console.log("Script Metadata:", JSON.stringify(scriptMetadata, null, 2));
109
+ // Add metadata as JSON
110
+ formData.append("metadata", new Blob([JSON.stringify(scriptMetadata)], {
111
+ type: "application/json",
112
+ }));
113
+ // Upload worker script with bindings
114
+ const uploadResponse = await api.put(`/accounts/${api.accountId}/workers/scripts/${workerName}`, formData, {
115
+ headers: {
116
+ "Content-Type": "multipart/form-data",
117
+ },
118
+ });
119
+ // Check if the upload was successful
120
+ if (!uploadResponse.ok) {
121
+ const errorData = await uploadResponse
122
+ .json()
123
+ .catch(() => ({ errors: [{ message: uploadResponse.statusText }] }));
124
+ throw new Error(`Error uploading worker script '${workerName}': ${errorData.errors?.[0]?.message || uploadResponse.statusText}`);
125
+ }
126
+ return formData;
127
+ }
128
+ async function prepareWorkerMetadata(ctx, oldBindings, props) {
129
+ // Prepare metadata with bindings
130
+ const meta = {
131
+ bindings: [],
132
+ observability: {
133
+ enabled: props.observability?.enabled !== false,
134
+ },
135
+ tags: [`alchemy:id:${ctx.resourceFQN}`],
136
+ migrations: {
137
+ new_classes: props.migrations?.new_classes ?? [],
138
+ deleted_classes: props.migrations?.deleted_classes ?? [],
139
+ renamed_classes: props.migrations?.renamed_classes ?? [],
140
+ transferred_classes: props.migrations?.transferred_classes ?? [],
141
+ new_sqlite_classes: props.migrations?.new_sqlite_classes ?? [],
142
+ },
143
+ };
144
+ // Convert bindings to the format expected by the API
145
+ for (const binding of props.bindings ?? []) {
146
+ // Create a copy of the binding to avoid modifying the original
147
+ const bindingData = { ...binding };
148
+ if (isDurableObjectNamespace(bindingData)) {
149
+ const stableId = bindingData.id;
150
+ const className = bindingData.className;
151
+ meta.bindings.push({
152
+ type: "durable_object_namespace",
153
+ name: bindingData.bindingName,
154
+ class_name: className,
155
+ script_name: bindingData.scriptName,
156
+ environment: bindingData.environment,
157
+ namespace_id: bindingData.namespaceId,
117
158
  });
118
- // Get the bundled content
119
- const bundleOutput = await apply(bundle);
120
- try {
121
- scriptContent = await fs.readFile(bundleOutput.path, "utf-8");
159
+ const oldBinding = oldBindings
160
+ ?.filter(isDurableObjectNamespace)
161
+ ?.find((b) => b.id === stableId);
162
+ if (!oldBinding) {
163
+ if (bindingData.sqlite) {
164
+ meta.migrations.new_sqlite_classes.push(className);
165
+ }
166
+ else {
167
+ meta.migrations.new_classes.push(className);
168
+ }
122
169
  }
123
- catch (error) {
124
- console.error("Error reading bundle:", error);
125
- throw new Error("Error reading bundle");
170
+ else if (oldBinding.className !== className) {
171
+ meta.migrations.renamed_classes.push({
172
+ from: oldBinding.className,
173
+ to: className,
174
+ });
126
175
  }
127
176
  }
128
177
  else {
129
- throw new Error("No script source provided"); // Shouldn't reach here due to earlier validation
130
- }
131
- // Prepare metadata with bindings
132
- const metadata = {
133
- bindings: [],
134
- };
135
- // Convert bindings to the format expected by the API
136
- if (props.bindings && props.bindings.length > 0) {
137
- for (const binding of props.bindings) {
138
- const bindingData = {
139
- name: binding.name,
140
- type: binding.type,
141
- };
142
- // Map binding properties to the API format based on binding type
143
- switch (binding.type) {
144
- case "kv_namespace":
145
- bindingData.namespace_id = binding.namespace_id;
146
- break;
147
- case "durable_object_namespace":
148
- bindingData.class_name = binding.class_name;
149
- if (binding.namespace_id) {
150
- bindingData.namespace_id = binding.namespace_id;
151
- }
152
- if (binding.script_name) {
153
- bindingData.script_name = binding.script_name;
154
- }
155
- if (binding.environment) {
156
- bindingData.environment = binding.environment;
157
- }
158
- break;
159
- case "secret_text":
160
- bindingData.text = binding.text;
161
- break;
162
- case "r2_bucket":
163
- bindingData.bucket_name = binding.bucket_name;
164
- break;
165
- case "analytics_engine":
166
- bindingData.dataset = binding.dataset;
167
- break;
168
- case "service":
169
- bindingData.service = binding.service;
170
- if (binding.environment) {
171
- bindingData.environment = binding.environment;
172
- }
173
- if (binding.namespace) {
174
- bindingData.namespace = binding.namespace;
175
- }
176
- break;
177
- case "d1":
178
- bindingData.id = binding.id;
179
- break;
180
- case "queue":
181
- bindingData.queue_name = binding.queue_name;
182
- break;
183
- case "json":
184
- bindingData.json = binding.json;
185
- break;
186
- case "hyperdrive":
187
- bindingData.id = binding.id;
188
- break;
189
- case "vectorize":
190
- bindingData.index_name = binding.index_name;
191
- break;
192
- case "dispatch_namespace":
193
- bindingData.namespace = binding.namespace;
194
- if (binding.outbound) {
195
- bindingData.outbound = binding.outbound;
196
- }
197
- break;
198
- case "mtls_certificate":
199
- bindingData.certificate_id = binding.certificate_id;
200
- break;
201
- case "plain_text":
202
- bindingData.text = binding.text;
203
- break;
204
- case "wasm_module":
205
- bindingData.module = binding.module;
206
- break;
207
- case "tail_consumer":
208
- bindingData.service = binding.service;
209
- break;
210
- }
211
- metadata.bindings.push(bindingData);
212
- }
178
+ // For all other binding types, use as-is
179
+ meta.bindings.push(bindingData);
213
180
  }
214
- // Create FormData for the upload
215
- const formData = new FormData();
216
- // Determine if we're using ESM or service worker format
217
- const isEsModule = props.format !== "cjs"; // Default to ESM unless CJS is specified
218
- const scriptName = isEsModule ? "worker.js" : "script";
219
- const contentType = isEsModule
220
- ? "application/javascript+module"
221
- : "application/javascript";
222
- // Prepare metadata
223
- const updatedMetadata = {
224
- ...metadata,
225
- };
226
- // Add Durable Object migrations if there are any DO bindings
227
- const durableObjectBindings = props.bindings?.filter((binding) => binding.type === "durable_object_namespace" &&
228
- // Only include self-referencing DOs (ones defined in this script)
229
- binding.script_name === props.name);
230
- if (durableObjectBindings && durableObjectBindings.length > 0) {
231
- // Add migrations metadata for Durable Objects
232
- updatedMetadata.migrations = {
233
- new_classes: durableObjectBindings.map((binding) => binding.class_name),
234
- deleted_classes: [],
235
- renamed_classes: [],
236
- // Version 2 migrations
237
- new_sqlite_classes: [],
238
- updated_sqlite_classes: [],
239
- renamed_sqlite_classes: [],
240
- };
181
+ }
182
+ // Convert env variables to plain_text bindings
183
+ if (props.env) {
184
+ for (const [key, value] of Object.entries(props.env)) {
185
+ meta.bindings.push({
186
+ name: key,
187
+ type: "plain_text",
188
+ text: value,
189
+ });
241
190
  }
242
- if (isEsModule) {
243
- // For ES modules format
244
- updatedMetadata.main_module = scriptName;
191
+ }
192
+ // Determine if we're using ESM or service worker format
193
+ const isEsModule = props.format !== "cjs"; // Default to ESM unless CJS is specified
194
+ const scriptName = isEsModule ? "worker.js" : "script";
195
+ if (isEsModule) {
196
+ // For ES modules format
197
+ meta.main_module = scriptName;
198
+ }
199
+ else {
200
+ // For service worker format (CJS)
201
+ meta.body_part = scriptName;
202
+ }
203
+ return meta;
204
+ }
205
+ async function assertWorkerDoesNotExist(ctx, api, workerName) {
206
+ const response = await api.get(`/accounts/${api.accountId}/workers/scripts/${workerName}`);
207
+ if (response.status === 404) {
208
+ return true;
209
+ }
210
+ if (response.status === 200) {
211
+ const metadata = await getWorkerScriptMetadata(api, workerName);
212
+ if (!metadata) {
213
+ throw new Error(`Worker exists but failed to fetch metadata: ${response.status} ${response.statusText}`);
245
214
  }
246
- else {
247
- // For service worker format (CJS)
248
- updatedMetadata.body_part = scriptName;
215
+ if (metadata.default_environment?.script.tags.includes(`alchemy:id:${ctx.resourceFQN}`)) {
216
+ return true;
249
217
  }
250
- // Add the actual script content as a named file part
251
- formData.append(scriptName, new Blob([scriptContent], { type: contentType }), scriptName);
252
- // Add metadata as JSON
253
- formData.append("metadata", new Blob([JSON.stringify(updatedMetadata)], {
254
- type: "application/json",
255
- }));
256
- // Upload worker script with bindings
257
- const uploadResponse = await api.put(`/accounts/${api.accountId}/workers/scripts/${workerName}`, formData);
258
- // Check if the upload was successful
259
- if (!uploadResponse.ok) {
260
- const errorData = await uploadResponse
261
- .json()
262
- .catch(() => ({ errors: [{ message: uploadResponse.statusText }] }));
263
- throw new Error(`Error uploading worker script: ${errorData.errors?.[0]?.message || uploadResponse.statusText}`);
218
+ throw new Error(`Worker with name '${workerName}' already exists. Please use a unique name.`);
219
+ }
220
+ else {
221
+ throw new Error(`Error checking if worker exists: ${response.status} ${response.statusText}`);
222
+ }
223
+ }
224
+ async function bundleWorkerScript(ctx, props) {
225
+ // Get the script content - either from props.script, or by bundling
226
+ // Create and use a Bundle resource with worker-optimized configuration
227
+ const defaultBundleOptions = {
228
+ format: props.format === "cjs" ? "cjs" : "esm", // Use the specified format or default to ESM
229
+ target: "es2020",
230
+ platform: "browser",
231
+ minify: true,
232
+ options: {
233
+ keepNames: true, // Important for Durable Object classes
234
+ },
235
+ };
236
+ // Merge with user-provided options
237
+ const bundleOptions = {
238
+ ...defaultBundleOptions,
239
+ ...(props.bundle || {}),
240
+ };
241
+ // Create the bundle
242
+ const bundle = new Bundle("bundle", {
243
+ entryPoint: props.entrypoint,
244
+ ...bundleOptions,
245
+ external: [...(bundleOptions.external ?? []), "cloudflare:workers"],
246
+ });
247
+ // Get the bundled content
248
+ const bundleOutput = await apply(bundle, {
249
+ quiet: ctx.quiet,
250
+ });
251
+ try {
252
+ return await fs.readFile(bundleOutput.path, "utf-8");
253
+ }
254
+ catch (error) {
255
+ console.error("Error reading bundle:", error);
256
+ throw new Error("Error reading bundle");
257
+ }
258
+ }
259
+ async function setupRoutes(api, workerName, routes) {
260
+ // Set up routes if provided
261
+ if (routes && routes.length > 0 && api.zoneId) {
262
+ // First get existing routes
263
+ const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
264
+ if (!routesResponse.ok) {
265
+ throw new Error(`Could not fetch routes: ${routesResponse.status} ${routesResponse.statusText}`);
264
266
  }
265
- // Set up routes if provided
266
- if (props.routes && props.routes.length > 0 && api.zoneId) {
267
- // First get existing routes
268
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
269
- if (routesResponse.ok) {
270
- const routesData = await routesResponse.json();
271
- const existingRoutes = routesData.result || [];
272
- // For each desired route
273
- for (const pattern of props.routes) {
274
- const existingRoute = existingRoutes.find((r) => r.pattern === pattern);
275
- if (existingRoute) {
276
- // Update if script name doesn't match
277
- if (existingRoute.script !== workerName) {
278
- const updateRouteResponse = await api.put(`/zones/${api.zoneId}/workers/routes/${existingRoute.id}`, {
279
- pattern,
280
- script: workerName,
281
- });
282
- if (!updateRouteResponse.ok) {
283
- console.warn(`Failed to update route ${pattern}: ${updateRouteResponse.status} ${updateRouteResponse.statusText}`);
284
- }
285
- }
286
- }
287
- else {
288
- // Create new route
289
- const createRouteResponse = await api.post(`/zones/${api.zoneId}/workers/routes`, {
290
- pattern,
291
- script: workerName,
292
- });
293
- if (!createRouteResponse.ok) {
294
- console.warn(`Failed to create route ${pattern}: ${createRouteResponse.status} ${createRouteResponse.statusText}`);
295
- }
267
+ const routesData = await routesResponse.json();
268
+ const existingRoutes = routesData.result || [];
269
+ // For each desired route
270
+ for (const pattern of routes) {
271
+ const existingRoute = existingRoutes.find((r) => r.pattern === pattern);
272
+ if (existingRoute) {
273
+ // Update if script name doesn't match
274
+ if (existingRoute.script !== workerName) {
275
+ const updateRouteResponse = await api.put(`/zones/${api.zoneId}/workers/routes/${existingRoute.id}`, {
276
+ pattern,
277
+ script: workerName,
278
+ });
279
+ if (!updateRouteResponse.ok) {
280
+ console.warn(`Failed to update route ${pattern}: ${updateRouteResponse.status} ${updateRouteResponse.statusText}`);
296
281
  }
297
282
  }
298
283
  }
299
284
  else {
300
- console.warn("Could not fetch routes:", routesResponse.status, routesResponse.statusText);
301
- }
302
- }
303
- // Handle worker URL if requested
304
- let workerUrl;
305
- if (props.url) {
306
- try {
307
- // Enable the workers.dev subdomain for this worker
308
- await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, { enabled: true, previews_enabled: true }, {
309
- headers: { "Content-Type": "application/json" },
285
+ // Create new route
286
+ const createRouteResponse = await api.post(`/zones/${api.zoneId}/workers/routes`, {
287
+ pattern,
288
+ script: workerName,
310
289
  });
311
- // Get the account's workers.dev subdomain
312
- const subdomainResponse = await api.get(`/accounts/${api.accountId}/workers/subdomain`);
313
- if (subdomainResponse.ok) {
314
- const subdomainData = await subdomainResponse.json();
315
- const subdomain = subdomainData.result?.subdomain;
316
- if (subdomain) {
317
- workerUrl = `${workerName}.${subdomain}.workers.dev`;
318
- // Add a delay when the subdomain is first created.
319
- // This is to prevent an issue where a negative cache-hit
320
- // causes the subdomain to be unavailable for 30 seconds.
321
- if (ctx.event === "create" || !ctx.output?.url) {
322
- await new Promise((resolve) => setTimeout(resolve, 3000));
323
- }
324
- }
290
+ if (!createRouteResponse.ok) {
291
+ throw new Error(`Failed to create route ${pattern}: ${createRouteResponse.status} ${createRouteResponse.statusText}`);
325
292
  }
326
- else {
327
- console.warn("Could not fetch workers.dev subdomain:", subdomainResponse.status, subdomainResponse.statusText);
328
- }
329
- }
330
- catch (error) {
331
- console.warn("Failed to enable worker URL:", error);
332
293
  }
333
294
  }
334
- else if (props.url === false && ctx.output?.url) {
335
- // Explicitly disable URL if it was previously enabled
336
- try {
337
- await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, JSON.stringify({ enabled: false }), {
338
- headers: { "Content-Type": "application/json" },
339
- });
340
- }
341
- catch (error) {
342
- console.warn("Failed to disable worker URL:", error);
295
+ }
296
+ }
297
+ async function configureURL(ctx, api, workerName, url) {
298
+ let workerUrl;
299
+ if (url) {
300
+ // Enable the workers.dev subdomain for this worker
301
+ await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, { enabled: true, previews_enabled: true }, {
302
+ headers: { "Content-Type": "application/json" },
303
+ });
304
+ // Get the account's workers.dev subdomain
305
+ const subdomainResponse = await api.get(`/accounts/${api.accountId}/workers/subdomain`);
306
+ if (!subdomainResponse.ok) {
307
+ throw new Error(`Could not fetch workers.dev subdomain: ${subdomainResponse.status} ${subdomainResponse.statusText}`);
308
+ }
309
+ const subdomainData = await subdomainResponse.json();
310
+ const subdomain = subdomainData.result?.subdomain;
311
+ if (subdomain) {
312
+ workerUrl = `https://${workerName}.${subdomain}.workers.dev`;
313
+ // Add a delay when the subdomain is first created.
314
+ // This is to prevent an issue where a negative cache-hit
315
+ // causes the subdomain to be unavailable for 30 seconds.
316
+ if (ctx.event === "create" || !ctx.output?.url) {
317
+ await new Promise((resolve) => setTimeout(resolve, 3000));
343
318
  }
344
319
  }
345
- // Get current timestamp
346
- const now = Date.now();
347
- // Construct the output
348
- const output = {
349
- id: workerName,
350
- name: workerName,
351
- script: scriptContent,
352
- format: props.format || "esm", // Include format in the output
353
- routes: props.routes || [],
354
- bindings: props.bindings || [],
355
- production: props.production !== false,
356
- createdAt: now,
357
- updatedAt: now,
358
- url: workerUrl,
359
- };
360
- return output;
361
320
  }
362
- }) {
321
+ else if (url === false && ctx.output?.url) {
322
+ // Explicitly disable URL if it was previously enabled
323
+ const response = await api.post(`/accounts/${api.accountId}/workers/scripts/${workerName}/subdomain`, JSON.stringify({ enabled: false }), {
324
+ headers: { "Content-Type": "application/json" },
325
+ });
326
+ if (!response.ok) {
327
+ throw new Error(`Failed to disable worker URL: ${response.status} ${response.statusText}`);
328
+ }
329
+ }
330
+ return workerUrl;
331
+ }
332
+ async function getWorkerScriptMetadata(api, workerName) {
333
+ const response = await api.get(`/accounts/${api.accountId}/workers/services/${workerName}`);
334
+ if (response.status === 404) {
335
+ return undefined;
336
+ }
337
+ if (!response.ok) {
338
+ throw new Error(`Error getting worker script metadata: ${response.status} ${response.statusText}`);
339
+ }
340
+ return (await response.json()).result;
341
+ }
342
+ async function getWorkerBindings(api, workerName, environment = "production") {
343
+ const response = await api.get(`/accounts/${api.accountId}/workers/services/${workerName}/environments/${environment}/bindings`, {
344
+ headers: {
345
+ Authorization: `Bearer ${process.env.CLOUDFLARE_API_TOKEN}`,
346
+ "Content-Type": "application/json",
347
+ },
348
+ });
349
+ if (response.status === 404) {
350
+ return undefined;
351
+ }
352
+ if (!response.ok) {
353
+ throw new Error(`Failed to fetch bindings: ${response.status} ${response.statusText}`);
354
+ }
355
+ const data = await response.json();
356
+ return data.result;
363
357
  }
364
358
  //# sourceMappingURL=worker.js.map