alchemy 0.1.12 → 0.1.14

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