alchemy 0.2.0 → 0.2.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.
package/src/apply.ts CHANGED
@@ -21,110 +21,116 @@ export async function apply<Out extends Resource>(
21
21
  options?: ApplyOptions,
22
22
  ): Promise<Awaited<Out>> {
23
23
  const scope = resource.Scope;
24
- const quiet = props.quiet ?? scope.quiet;
25
- await scope.init();
26
- let state: State | undefined = (await scope.state.get(resource.ID))!;
27
- const provider: Provider = PROVIDERS.get(resource.Kind);
28
- if (provider === undefined) {
29
- throw new Error(`Provider "${resource.Kind}" not found`);
30
- }
31
- if (state === undefined) {
32
- state = {
33
- kind: resource.Kind,
34
- id: resource.ID,
35
- fqn: resource.FQN,
36
- seq: resource.Seq,
37
- status: "creating",
38
- data: {},
39
- output: {
40
- ID: resource.ID,
41
- FQN: resource.FQN,
42
- Kind: resource.Kind,
43
- Scope: scope,
44
- Seq: resource.Seq,
45
- },
46
- // deps: [...deps],
47
- props,
48
- };
49
- await scope.state.set(resource.ID, state);
50
- }
24
+ try {
25
+ const quiet = props.quiet ?? scope.quiet;
26
+ await scope.init();
27
+ let state: State | undefined = (await scope.state.get(resource.ID))!;
28
+ const provider: Provider = PROVIDERS.get(resource.Kind);
29
+ if (provider === undefined) {
30
+ throw new Error(`Provider "${resource.Kind}" not found`);
31
+ }
32
+ if (state === undefined) {
33
+ state = {
34
+ kind: resource.Kind,
35
+ id: resource.ID,
36
+ fqn: resource.FQN,
37
+ seq: resource.Seq,
38
+ status: "creating",
39
+ data: {},
40
+ output: {
41
+ ID: resource.ID,
42
+ FQN: resource.FQN,
43
+ Kind: resource.Kind,
44
+ Scope: scope,
45
+ Seq: resource.Seq,
46
+ },
47
+ // deps: [...deps],
48
+ props,
49
+ };
50
+ await scope.state.set(resource.ID, state);
51
+ }
51
52
 
52
- const alwaysUpdate =
53
- options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
53
+ const alwaysUpdate =
54
+ options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
54
55
 
55
- // Skip update if inputs haven't changed and resource is in a stable state
56
- if (state.status === "created" || state.status === "updated") {
57
- if (
58
- JSON.stringify(state.props) === JSON.stringify(serialize(scope, props)) &&
59
- alwaysUpdate !== true
60
- ) {
61
- if (!quiet) {
62
- console.log(`Skip: "${resource.FQN}" (no changes)`);
56
+ // Skip update if inputs haven't changed and resource is in a stable state
57
+ if (state.status === "created" || state.status === "updated") {
58
+ if (
59
+ JSON.stringify(state.props) ===
60
+ JSON.stringify(await serialize(scope, props)) &&
61
+ alwaysUpdate !== true
62
+ ) {
63
+ if (!quiet) {
64
+ console.log(`Skip: "${resource.FQN}" (no changes)`);
65
+ }
66
+ // if (resourceState.output !== undefined) {
67
+ // resource[Provide](resourceState.output);
68
+ // }
69
+ return state.output as Awaited<Out>;
63
70
  }
64
- // if (resourceState.output !== undefined) {
65
- // resource[Provide](resourceState.output);
66
- // }
67
- return state.output as Awaited<Out>;
68
71
  }
69
- }
70
72
 
71
- const phase = state.status === "creating" ? "create" : "update";
72
- state.status = phase === "create" ? "creating" : "updating";
73
- state.oldProps = state.props;
74
- state.props = props;
73
+ const phase = state.status === "creating" ? "create" : "update";
74
+ state.status = phase === "create" ? "creating" : "updating";
75
+ state.oldProps = state.props;
76
+ state.props = props;
75
77
 
76
- if (!quiet) {
77
- console.log(
78
- `${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`,
79
- );
80
- }
78
+ if (!quiet) {
79
+ console.log(
80
+ `${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`,
81
+ );
82
+ }
81
83
 
82
- await scope.state.set(resource.ID, state);
84
+ await scope.state.set(resource.ID, state);
83
85
 
84
- let isReplaced = false;
86
+ let isReplaced = false;
85
87
 
86
- const ctx = context({
87
- scope,
88
- phase,
89
- kind: resource.Kind,
90
- id: resource.ID,
91
- fqn: resource.FQN,
92
- seq: resource.Seq,
93
- state,
94
- replace: () => {
95
- if (isReplaced) {
96
- console.warn(
97
- `Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`,
98
- );
99
- return;
100
- }
101
- isReplaced = true;
102
- },
103
- });
88
+ const ctx = context({
89
+ scope,
90
+ phase,
91
+ kind: resource.Kind,
92
+ id: resource.ID,
93
+ fqn: resource.FQN,
94
+ seq: resource.Seq,
95
+ state,
96
+ replace: () => {
97
+ if (isReplaced) {
98
+ console.warn(
99
+ `Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`,
100
+ );
101
+ return;
102
+ }
103
+ isReplaced = true;
104
+ },
105
+ });
104
106
 
105
- const output = await alchemy.run(resource.ID, async () =>
106
- provider.handler.bind(ctx)(resource.ID, props),
107
- );
107
+ const output = await alchemy.run(resource.ID, async () => {
108
+ return provider.handler.bind(ctx)(resource.ID, props);
109
+ });
108
110
 
109
- if (!quiet) {
110
- console.log(
111
- `${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`,
112
- );
113
- }
111
+ if (!quiet) {
112
+ console.log(
113
+ `${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`,
114
+ );
115
+ }
114
116
 
115
- await scope.state.set(resource.ID, {
116
- kind: resource.Kind,
117
- id: resource.ID,
118
- fqn: resource.FQN,
119
- seq: resource.Seq,
120
- data: state.data,
121
- status: phase === "create" ? "created" : "updated",
122
- output,
123
- props,
124
- // deps: [...deps],
125
- });
126
- // if (output !== undefined) {
127
- // resource[Provide](output as Out);
128
- // }
129
- return output as any;
117
+ await scope.state.set(resource.ID, {
118
+ kind: resource.Kind,
119
+ id: resource.ID,
120
+ fqn: resource.FQN,
121
+ seq: resource.Seq,
122
+ data: state.data,
123
+ status: phase === "create" ? "created" : "updated",
124
+ output,
125
+ props,
126
+ // deps: [...deps],
127
+ });
128
+ // if (output !== undefined) {
129
+ // resource[Provide](output as Out);
130
+ // }
131
+ return output as any;
132
+ } catch (error) {
133
+ scope.fail();
134
+ throw error;
135
+ }
130
136
  }
@@ -5,3 +5,5 @@ export * from "./kv-namespace";
5
5
  export * from "./state";
6
6
  export * from "./static-site";
7
7
  export * from "./worker";
8
+ export * from "./wrangler.json";
9
+ export * from "./zone";
@@ -230,17 +230,6 @@ export const StaticSite = Resource(
230
230
  throw new Error("Directory is required for StaticSite");
231
231
  }
232
232
 
233
- try {
234
- const dirStat = await fs.stat(props.dir);
235
- if (!dirStat.isDirectory()) {
236
- throw new Error(`"${props.dir}" is not a directory`);
237
- }
238
- } catch (error: any) {
239
- throw new Error(
240
- `Directory "${props.dir}" does not exist: ${error.message}`,
241
- );
242
- }
243
-
244
233
  // Run build command if provided
245
234
  if (props.build?.command) {
246
235
  try {
@@ -266,6 +255,17 @@ export const StaticSite = Resource(
266
255
  }
267
256
  }
268
257
 
258
+ try {
259
+ const dirStat = await fs.stat(props.dir);
260
+ if (!dirStat.isDirectory()) {
261
+ throw new Error(`"${props.dir}" is not a directory`);
262
+ }
263
+ } catch (error: any) {
264
+ throw new Error(
265
+ `Directory "${props.dir}" does not exist: ${error.message}`,
266
+ );
267
+ }
268
+
269
269
  // Use the provided name
270
270
  const siteName = props.name;
271
271
  const indexPage = props.indexPage || "index.html";
@@ -528,7 +528,7 @@ async function assertWorkerDoesNotExist<B extends Bindings>(
528
528
  );
529
529
  } else {
530
530
  throw new Error(
531
- `Error checking if worker exists: ${response.status} ${response.statusText}`,
531
+ `Error checking if worker exists: ${response.status} ${response.statusText} ${await response.text()}`,
532
532
  );
533
533
  }
534
534
  }
@@ -0,0 +1,309 @@
1
+ import * as fs from "fs/promises";
2
+ import type { Context } from "../context";
3
+ import { Resource } from "../resource";
4
+
5
+ /**
6
+ * Properties for wrangler.json configuration file
7
+ */
8
+ export interface WranglerJsonProps {
9
+ /**
10
+ * The name of your worker
11
+ */
12
+ name: string;
13
+
14
+ /**
15
+ * The directory containing your worker entry point
16
+ * @default "src"
17
+ */
18
+ main?: string;
19
+
20
+ /**
21
+ * The entry point for your worker
22
+ * @default "index.ts" or "index.js"
23
+ */
24
+ entrypoint?: string;
25
+
26
+ /**
27
+ * The directory to store build artifacts
28
+ * @default "dist"
29
+ */
30
+ outdir?: string;
31
+
32
+ /**
33
+ * The directory to serve static assets from
34
+ */
35
+ assets?: string;
36
+
37
+ /**
38
+ * Minify the worker script
39
+ * @default true
40
+ */
41
+ minify?: boolean;
42
+
43
+ /**
44
+ * Node.js compatibility mode
45
+ * @default false
46
+ */
47
+ node_compat?: boolean;
48
+
49
+ /**
50
+ * First-party worker service bindings
51
+ */
52
+ services?: Array<{
53
+ /**
54
+ * Binding name
55
+ */
56
+ name: string;
57
+ /**
58
+ * Service environment
59
+ */
60
+ environment?: string;
61
+ }>;
62
+
63
+ /**
64
+ * Worker environment variables
65
+ */
66
+ vars?: Record<string, string>;
67
+
68
+ /**
69
+ * KV Namespace bindings
70
+ */
71
+ kv_namespaces?: Array<{
72
+ /**
73
+ * Binding name
74
+ */
75
+ binding: string;
76
+ /**
77
+ * KV namespace ID
78
+ */
79
+ id: string;
80
+ /**
81
+ * Preview KV namespace ID
82
+ */
83
+ preview_id?: string;
84
+ }>;
85
+
86
+ /**
87
+ * R2 bucket bindings
88
+ */
89
+ r2_buckets?: Array<{
90
+ /**
91
+ * Binding name
92
+ */
93
+ binding: string;
94
+ /**
95
+ * Bucket name
96
+ */
97
+ bucket_name: string;
98
+ /**
99
+ * Preview bucket name
100
+ */
101
+ preview_bucket_name?: string;
102
+ }>;
103
+
104
+ /**
105
+ * D1 database bindings
106
+ */
107
+ d1_databases?: Array<{
108
+ /**
109
+ * Binding name
110
+ */
111
+ binding: string;
112
+ /**
113
+ * Database name
114
+ */
115
+ database_name: string;
116
+ /**
117
+ * Database ID
118
+ */
119
+ database_id: string;
120
+ /**
121
+ * Preview database ID
122
+ */
123
+ preview_database_id?: string;
124
+ }>;
125
+
126
+ /**
127
+ * Durable Object bindings
128
+ */
129
+ durable_objects?: {
130
+ /**
131
+ * Durable Object bindings
132
+ */
133
+ bindings: Array<{
134
+ /**
135
+ * Binding name
136
+ */
137
+ name: string;
138
+ /**
139
+ * Class name
140
+ */
141
+ class_name: string;
142
+ /**
143
+ * Script name
144
+ */
145
+ script_name?: string;
146
+ /**
147
+ * Environment name
148
+ */
149
+ environment?: string;
150
+ }>;
151
+ };
152
+
153
+ /**
154
+ * Queue bindings
155
+ */
156
+ queues?: {
157
+ /**
158
+ * Producer bindings
159
+ */
160
+ producers?: Array<{
161
+ /**
162
+ * Binding name
163
+ */
164
+ binding: string;
165
+ /**
166
+ * Queue name
167
+ */
168
+ queue: string;
169
+ }>;
170
+ /**
171
+ * Consumer configuration
172
+ */
173
+ consumers?: Array<{
174
+ /**
175
+ * Queue name
176
+ */
177
+ queue: string;
178
+ /**
179
+ * Maximum batch size
180
+ */
181
+ max_batch_size?: number;
182
+ /**
183
+ * Maximum batch timeout
184
+ */
185
+ max_batch_timeout?: number;
186
+ /**
187
+ * Maximum retries
188
+ */
189
+ max_retries?: number;
190
+ /**
191
+ * Dead letter queue
192
+ */
193
+ dead_letter_queue?: string;
194
+ }>;
195
+ };
196
+
197
+ /**
198
+ * Analytics Engine bindings
199
+ */
200
+ analytics_engine_datasets?: Array<{
201
+ /**
202
+ * Binding name
203
+ */
204
+ binding: string;
205
+ /**
206
+ * Dataset name
207
+ */
208
+ dataset?: string;
209
+ }>;
210
+
211
+ /**
212
+ * Route configuration
213
+ */
214
+ routes?: Array<string>;
215
+
216
+ /**
217
+ * Triggers configuration
218
+ */
219
+ triggers?: {
220
+ /**
221
+ * Cron triggers
222
+ */
223
+ crons: Array<string>;
224
+ };
225
+
226
+ /**
227
+ * Worker compatibility date
228
+ */
229
+ compatibility_date?: string;
230
+
231
+ /**
232
+ * Worker compatibility flags
233
+ */
234
+ compatibility_flags?: Array<string>;
235
+
236
+ /**
237
+ * Worker usage model
238
+ * "bundled" - Includes resources like CPU and memory. Better for consistent workloads.
239
+ * "unbound" - Pay only for what you use. Better for sporadic workloads.
240
+ */
241
+ usage_model?: "bundled" | "unbound";
242
+
243
+ /**
244
+ * Path to the wrangler.json file
245
+ * @internal
246
+ */
247
+ path?: string;
248
+ }
249
+
250
+ /**
251
+ * Output returned after WranglerJson creation/update
252
+ */
253
+ export interface WranglerJson
254
+ extends Resource<"cloudflare::WranglerJson">,
255
+ WranglerJsonProps {
256
+ /**
257
+ * Time at which the file was created
258
+ */
259
+ createdAt: number;
260
+
261
+ /**
262
+ * Time at which the file was last updated
263
+ */
264
+ updatedAt: number;
265
+ }
266
+
267
+ /**
268
+ * Resource for managing wrangler.json configuration files
269
+ */
270
+ export const WranglerJson = Resource(
271
+ "cloudflare::WranglerJson",
272
+ async function (
273
+ this: Context<WranglerJson>,
274
+ id: string,
275
+ props: WranglerJsonProps,
276
+ ): Promise<WranglerJson> {
277
+ // Default path is wrangler.json in current directory
278
+ const filePath = props.path || "wrangler.json";
279
+
280
+ if (this.phase === "delete") {
281
+ try {
282
+ await fs.unlink(filePath);
283
+ } catch (error) {
284
+ // Ignore errors if file doesn't exist
285
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
286
+ throw error;
287
+ }
288
+ }
289
+ return this.destroy();
290
+ }
291
+
292
+ // Create or update the file
293
+ const config = {
294
+ ...props,
295
+ // Remove internal path property
296
+ path: undefined,
297
+ };
298
+
299
+ // Write the file
300
+ await fs.writeFile(filePath, JSON.stringify(config, null, 2));
301
+
302
+ // Return the resource
303
+ return this({
304
+ ...props,
305
+ createdAt: Date.now(),
306
+ updatedAt: Date.now(),
307
+ });
308
+ },
309
+ );