alchemy 0.2.0 → 0.2.2

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 (47) hide show
  1. package/README.md +506 -0
  2. package/lib/alchemy.js +10 -2
  3. package/lib/apply.js +86 -77
  4. package/lib/cloudflare/index.d.ts +2 -0
  5. package/lib/cloudflare/index.js +2 -0
  6. package/lib/cloudflare/static-site.js +9 -9
  7. package/lib/cloudflare/worker.js +1 -1
  8. package/lib/cloudflare/wrangler.json.d.ts +243 -0
  9. package/lib/cloudflare/wrangler.json.js +35 -0
  10. package/lib/cloudflare/zone-settings.d.ts +301 -0
  11. package/lib/cloudflare/zone-settings.js +1 -0
  12. package/lib/cloudflare/zone.d.ts +196 -0
  13. package/lib/cloudflare/zone.js +175 -0
  14. package/lib/destroy.js +1 -1
  15. package/lib/project/vite.d.ts +90 -0
  16. package/lib/project/vite.js +228 -0
  17. package/lib/scope.d.ts +2 -0
  18. package/lib/scope.js +20 -1
  19. package/lib/shadcn/component.d.ts +29 -0
  20. package/lib/shadcn/component.js +31 -0
  21. package/lib/shadcn/index.d.ts +1 -0
  22. package/lib/shadcn/index.js +1 -0
  23. package/lib/state.js +6 -1
  24. package/lib/util/rm.d.ts +1 -0
  25. package/lib/util/rm.js +11 -0
  26. package/lib/vite/index.d.ts +1 -0
  27. package/lib/vite/index.js +1 -0
  28. package/lib/vite/vite.d.ts +90 -0
  29. package/lib/vite/vite.js +228 -0
  30. package/package.json +5 -3
  31. package/src/alchemy.ts +9 -2
  32. package/src/apply.ts +100 -94
  33. package/src/cloudflare/index.ts +2 -0
  34. package/src/cloudflare/static-site.ts +11 -11
  35. package/src/cloudflare/worker.ts +1 -1
  36. package/src/cloudflare/wrangler.json.ts +309 -0
  37. package/src/cloudflare/zone-settings.ts +368 -0
  38. package/src/cloudflare/zone.ts +491 -0
  39. package/src/destroy.ts +1 -1
  40. package/src/project/vite.ts +406 -0
  41. package/src/scope.ts +25 -1
  42. package/src/shadcn/component.ts +80 -0
  43. package/src/shadcn/index.ts +1 -0
  44. package/src/state.ts +9 -1
  45. package/src/util/rm.ts +11 -0
  46. package/src/vite/index.ts +1 -0
  47. package/src/vite/vite.ts +406 -0
@@ -0,0 +1,491 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import { createCloudflareApi } from "./api";
4
+ import type {
5
+ AlwaysUseHTTPSValue,
6
+ AutomaticHTTPSRewritesValue,
7
+ BrotliValue,
8
+ CloudflareZoneSettingResponse,
9
+ DevelopmentModeValue,
10
+ EarlyHintsValue,
11
+ EmailObfuscationValue,
12
+ HTTP2Value,
13
+ HTTP3Value,
14
+ HotlinkProtectionValue,
15
+ IPv6Value,
16
+ MinTLSVersionValue,
17
+ SSLValue,
18
+ TLS13Value,
19
+ UpdateZoneSettingParams,
20
+ WebSocketsValue,
21
+ ZeroRTTValue,
22
+ } from "./zone-settings";
23
+
24
+ /**
25
+ * Properties for creating or updating a Zone
26
+ */
27
+ export interface ZoneProps {
28
+ /**
29
+ * The domain name for the zone
30
+ */
31
+ name: string;
32
+
33
+ /**
34
+ * Account ID to use for the zone
35
+ * If not provided, will use the default account
36
+ */
37
+ accountId?: string;
38
+
39
+ /**
40
+ * The type of zone to create
41
+ * "full" - Full zone implies that DNS is hosted with Cloudflare
42
+ * "partial" - Partial zone is typically a partner-hosted zone or a CNAME setup
43
+ * "secondary" - Secondary zone is a zone that mirrors the primary zone
44
+ * @default "full"
45
+ */
46
+ type?: "full" | "partial" | "secondary";
47
+
48
+ /**
49
+ * Whether to jump start the zone
50
+ * When enabled, Cloudflare will attempt to fetch existing DNS records
51
+ * @default true
52
+ */
53
+ jumpStart?: boolean;
54
+
55
+ /**
56
+ * Settings to apply to the zone
57
+ */
58
+ settings?: {
59
+ /**
60
+ * Enable SSL/TLS encryption for the zone
61
+ * "off" - SSL disabled
62
+ * "flexible" - Encrypts traffic between browser and Cloudflare
63
+ * "full" - Encrypts traffic between browser and server, allows self-signed certs
64
+ * "strict" - Encrypts traffic between browser and server, requires valid cert
65
+ */
66
+ ssl?: SSLValue;
67
+
68
+ /**
69
+ * Enable Always Use HTTPS
70
+ * Redirects all HTTP traffic to HTTPS
71
+ * @default "off"
72
+ */
73
+ alwaysUseHttps?: AlwaysUseHTTPSValue;
74
+
75
+ /**
76
+ * Enable Automatic HTTPS Rewrites
77
+ * Automatically rewrites HTTP URLs to HTTPS
78
+ * @default "off"
79
+ */
80
+ automaticHttpsRewrites?: AutomaticHTTPSRewritesValue;
81
+
82
+ /**
83
+ * Enable TLS 1.3
84
+ * Enables the latest version of TLS encryption
85
+ * @default "off"
86
+ */
87
+ tls13?: TLS13Value;
88
+
89
+ /**
90
+ * Enable Early Hints
91
+ * Speeds up page loads by serving Link headers
92
+ * @default "off"
93
+ */
94
+ earlyHints?: EarlyHintsValue;
95
+
96
+ /**
97
+ * Enable Email Obfuscation
98
+ * Obfuscates email addresses on the site
99
+ * @default "off"
100
+ */
101
+ emailObfuscation?: EmailObfuscationValue;
102
+
103
+ /**
104
+ * Enable Browser Cache TTL
105
+ * Sets the browser cache TTL in seconds
106
+ */
107
+ browserCacheTtl?: number;
108
+
109
+ /**
110
+ * Enable Development Mode
111
+ * Disables caching and enables real-time updates
112
+ * @default "off"
113
+ */
114
+ developmentMode?: DevelopmentModeValue;
115
+
116
+ /**
117
+ * Enable HTTP/2
118
+ * @default "on"
119
+ */
120
+ http2?: HTTP2Value;
121
+
122
+ /**
123
+ * Enable HTTP/3
124
+ * @default "on"
125
+ */
126
+ http3?: HTTP3Value;
127
+
128
+ /**
129
+ * Enable IPv6
130
+ * @default "on"
131
+ */
132
+ ipv6?: IPv6Value;
133
+
134
+ /**
135
+ * Enable WebSockets
136
+ * @default "on"
137
+ */
138
+ websockets?: WebSocketsValue;
139
+
140
+ /**
141
+ * Enable Zero-RTT
142
+ * @default "off"
143
+ */
144
+ zeroRtt?: ZeroRTTValue;
145
+
146
+ /**
147
+ * Enable Brotli compression
148
+ * @default "on"
149
+ */
150
+ brotli?: BrotliValue;
151
+
152
+ /**
153
+ * Enable Hotlink Protection
154
+ * @default "off"
155
+ */
156
+ hotlinkProtection?: HotlinkProtectionValue;
157
+
158
+ /**
159
+ * Minimum TLS Version
160
+ * @default "1.0"
161
+ */
162
+ minTlsVersion?: MinTLSVersionValue;
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Output returned after Zone creation/update
168
+ */
169
+ export interface Zone extends Resource<"cloudflare::Zone"> {
170
+ /**
171
+ * The ID of the zone
172
+ */
173
+ id: string;
174
+
175
+ /**
176
+ * The domain name for the zone
177
+ */
178
+ name: string;
179
+
180
+ /**
181
+ * The type of zone
182
+ */
183
+ type: "full" | "partial" | "secondary";
184
+
185
+ /**
186
+ * The status of the zone
187
+ */
188
+ status: string;
189
+
190
+ /**
191
+ * Whether the zone is paused
192
+ */
193
+ paused: boolean;
194
+
195
+ /**
196
+ * The account ID the zone belongs to
197
+ */
198
+ accountId: string;
199
+
200
+ /**
201
+ * The nameservers assigned to the zone
202
+ */
203
+ nameservers: string[];
204
+
205
+ /**
206
+ * The original nameservers for the zone
207
+ */
208
+ originalNameservers: string[] | null;
209
+
210
+ /**
211
+ * Time at which the zone was created
212
+ */
213
+ createdAt: number;
214
+
215
+ /**
216
+ * Time at which the zone was last modified
217
+ */
218
+ modifiedAt: number;
219
+
220
+ /**
221
+ * Time at which the zone was activated
222
+ */
223
+ activatedAt: number | null;
224
+
225
+ /**
226
+ * The zone's current settings
227
+ */
228
+ settings: {
229
+ ssl: SSLValue;
230
+ alwaysUseHttps: AlwaysUseHTTPSValue;
231
+ automaticHttpsRewrites: AutomaticHTTPSRewritesValue;
232
+ tls13: TLS13Value;
233
+ earlyHints: EarlyHintsValue;
234
+ emailObfuscation: EmailObfuscationValue;
235
+ browserCacheTtl: number;
236
+ developmentMode: DevelopmentModeValue;
237
+ http2: HTTP2Value;
238
+ http3: HTTP3Value;
239
+ ipv6: IPv6Value;
240
+ websockets: WebSocketsValue;
241
+ zeroRtt: ZeroRTTValue;
242
+ brotli: BrotliValue;
243
+ hotlinkProtection: HotlinkProtectionValue;
244
+ minTlsVersion: MinTLSVersionValue;
245
+ };
246
+ }
247
+
248
+ export const Zone = Resource(
249
+ "cloudflare::Zone",
250
+ async function (
251
+ this: Context<Zone>,
252
+ id: string,
253
+ props: ZoneProps,
254
+ ): Promise<Zone> {
255
+ // Create Cloudflare API client with automatic account discovery
256
+ const api = await createCloudflareApi({
257
+ accountId: props.accountId,
258
+ });
259
+
260
+ if (this.phase === "delete") {
261
+ if (this.output?.id) {
262
+ // Delete zone
263
+ const deleteResponse = await api.delete(`/zones/${this.output.id}`);
264
+
265
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
266
+ const errorData: any = await deleteResponse.json().catch(() => ({
267
+ errors: [{ message: deleteResponse.statusText }],
268
+ }));
269
+ throw new Error(
270
+ `Error deleting zone '${props.name}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
271
+ );
272
+ }
273
+ } else {
274
+ console.warn(`Zone '${props.name}' not found, skipping delete`);
275
+ }
276
+ return this.destroy();
277
+ }
278
+
279
+ if (this.phase === "update" && this.output?.id) {
280
+ // Get zone details to verify it exists
281
+ const response = await api.get(`/zones/${this.output.id}`);
282
+
283
+ if (!response.ok) {
284
+ throw new Error(
285
+ `Error getting zone '${props.name}': ${response.statusText}`,
286
+ );
287
+ }
288
+
289
+ const result = (await response.json()) as CloudflareZoneResponse;
290
+ const zoneData = result.result;
291
+
292
+ // Update zone settings if provided
293
+ if (props.settings) {
294
+ await updateZoneSettings(api, this.output.id, props.settings);
295
+ // Add a small delay to ensure settings are propagated
296
+ await new Promise((resolve) => setTimeout(resolve, 2000));
297
+ }
298
+
299
+ return this({
300
+ id: zoneData.id,
301
+ name: zoneData.name,
302
+ type: zoneData.type,
303
+ status: zoneData.status,
304
+ paused: zoneData.paused,
305
+ accountId: zoneData.account.id,
306
+ nameservers: zoneData.name_servers,
307
+ originalNameservers: zoneData.original_name_servers,
308
+ createdAt: new Date(zoneData.created_on).getTime(),
309
+ modifiedAt: new Date(zoneData.modified_on).getTime(),
310
+ activatedAt: zoneData.activated_on
311
+ ? new Date(zoneData.activated_on).getTime()
312
+ : null,
313
+ settings: await getZoneSettings(api, zoneData.id),
314
+ });
315
+ } else {
316
+ // Create new zone
317
+
318
+ const response = await api.post("/zones", {
319
+ name: props.name,
320
+ type: props.type || "full",
321
+ jump_start: props.jumpStart !== false,
322
+ account: {
323
+ id: api.accountId,
324
+ },
325
+ });
326
+
327
+ const body = await response.text();
328
+ if (!response.ok) {
329
+ if (response.status === 400 && body.includes("already exists")) {
330
+ // TODO: should we error?
331
+ } else {
332
+ throw new Error(
333
+ `Error creating zone '${props.name}': ${response.statusText}\n${body}`,
334
+ );
335
+ }
336
+ }
337
+
338
+ const zoneData = (JSON.parse(body) as CloudflareZoneResponse).result;
339
+
340
+ // Update zone settings if provided
341
+ if (props.settings) {
342
+ await updateZoneSettings(api, zoneData.id, props.settings);
343
+ // Add a small delay to ensure settings are propagated
344
+ await new Promise((resolve) => setTimeout(resolve, 2000));
345
+ }
346
+
347
+ return this({
348
+ id: zoneData.id,
349
+ name: zoneData.name,
350
+ type: zoneData.type,
351
+ status: zoneData.status,
352
+ paused: zoneData.paused,
353
+ accountId: zoneData.account.id,
354
+ nameservers: zoneData.name_servers,
355
+ originalNameservers: zoneData.original_name_servers,
356
+ createdAt: new Date(zoneData.created_on).getTime(),
357
+ modifiedAt: new Date(zoneData.modified_on).getTime(),
358
+ activatedAt: zoneData.activated_on
359
+ ? new Date(zoneData.activated_on).getTime()
360
+ : null,
361
+ settings: await getZoneSettings(api, zoneData.id),
362
+ });
363
+ }
364
+ },
365
+ );
366
+
367
+ /**
368
+ * Helper function to update zone settings
369
+ */
370
+ async function updateZoneSettings(
371
+ api: any,
372
+ zoneId: string,
373
+ settings: ZoneProps["settings"],
374
+ ): Promise<void> {
375
+ if (!settings) return;
376
+
377
+ const settingsMap = {
378
+ ssl: "ssl",
379
+ alwaysUseHttps: "always_use_https",
380
+ automaticHttpsRewrites: "automatic_https_rewrites",
381
+ tls13: "tls_1_3",
382
+ earlyHints: "early_hints",
383
+ emailObfuscation: "email_obfuscation",
384
+ browserCacheTtl: "browser_cache_ttl",
385
+ developmentMode: "development_mode",
386
+ http2: "http2",
387
+ http3: "http3",
388
+ ipv6: "ipv6",
389
+ websockets: "websockets",
390
+ zeroRtt: "0rtt",
391
+ brotli: "brotli",
392
+ hotlinkProtection: "hotlink_protection",
393
+ minTlsVersion: "min_tls_version",
394
+ };
395
+
396
+ await Promise.all(
397
+ Object.entries(settings)
398
+ .filter(([_, value]) => value !== undefined)
399
+ .map(async ([key, value]) => {
400
+ const settingId = settingsMap[key as keyof typeof settings];
401
+ if (!settingId) return;
402
+
403
+ const response = await api.patch(
404
+ `/zones/${zoneId}/settings/${settingId}`,
405
+ {
406
+ value,
407
+ } as UpdateZoneSettingParams,
408
+ );
409
+
410
+ if (!response.ok) {
411
+ const data = await response.text();
412
+ if (response.status === 400 && data.includes("already enabled")) {
413
+ console.warn(`Warning: Setting '${key}' already enabled`);
414
+ return;
415
+ }
416
+ throw new Error(
417
+ `Failed to update zone setting ${key}: ${response.statusText}`,
418
+ );
419
+ }
420
+ }),
421
+ );
422
+ }
423
+
424
+ /**
425
+ * Helper function to get current zone settings
426
+ */
427
+ async function getZoneSettings(
428
+ api: any,
429
+ zoneId: string,
430
+ ): Promise<Zone["settings"]> {
431
+ const settingsResponse = await api.get(`/zones/${zoneId}/settings`);
432
+
433
+ if (!settingsResponse.ok) {
434
+ throw new Error(
435
+ `Failed to fetch zone settings: ${settingsResponse.status} ${settingsResponse.statusText}`,
436
+ );
437
+ }
438
+
439
+ const result =
440
+ (await settingsResponse.json()) as CloudflareZoneSettingResponse;
441
+ const settingsData = result.result;
442
+
443
+ // Helper to get setting value with default
444
+ const getSetting = <T>(id: string, defaultValue: T): T => {
445
+ const setting = settingsData.find((s: any) => s.id === id);
446
+ return (setting?.value as T) ?? defaultValue;
447
+ };
448
+
449
+ return {
450
+ ssl: getSetting("ssl", "off"),
451
+ alwaysUseHttps: getSetting("always_use_https", "off"),
452
+ automaticHttpsRewrites: getSetting("automatic_https_rewrites", "off"),
453
+ tls13: getSetting("tls_1_3", "off"),
454
+ earlyHints: getSetting("early_hints", "off"),
455
+ emailObfuscation: getSetting("email_obfuscation", "off"),
456
+ browserCacheTtl: getSetting("browser_cache_ttl", 14400),
457
+ developmentMode: getSetting("development_mode", "off"),
458
+ http2: getSetting("http2", "on"),
459
+ http3: getSetting("http3", "on"),
460
+ ipv6: getSetting("ipv6", "on"),
461
+ websockets: getSetting("websockets", "on"),
462
+ zeroRtt: getSetting("0rtt", "off"),
463
+ brotli: getSetting("brotli", "on"),
464
+ hotlinkProtection: getSetting("hotlink_protection", "off"),
465
+ minTlsVersion: getSetting("min_tls_version", "1.0"),
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Cloudflare Zone API response
471
+ */
472
+ interface CloudflareZoneResponse {
473
+ result: {
474
+ id: string;
475
+ name: string;
476
+ type: "full" | "partial" | "secondary";
477
+ status: string;
478
+ paused: boolean;
479
+ account: {
480
+ id: string;
481
+ };
482
+ name_servers: string[];
483
+ original_name_servers: string[] | null;
484
+ created_on: string;
485
+ modified_on: string;
486
+ activated_on: string | null;
487
+ };
488
+ success: boolean;
489
+ errors: Array<{ code: number; message: string }>;
490
+ messages: string[];
491
+ }
package/src/destroy.ts CHANGED
@@ -92,7 +92,7 @@ export async function destroy<Type extends string>(
92
92
  // BUG: this does not restore persisted scope
93
93
  await alchemy.run(instance.ID, async (scope) => {
94
94
  nestedScope = scope;
95
- return Provider.handler.bind(ctx)(instance.ID, state.props);
95
+ return await Provider.handler.bind(ctx)(instance.ID, state.props);
96
96
  });
97
97
  } catch (err) {
98
98
  if (err instanceof DestroyedSignal) {