alchemy 0.41.2 → 0.42.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/bin/alchemy.mjs +99 -44
  3. package/lib/build-date.d.ts +1 -1
  4. package/lib/build-date.js +1 -1
  5. package/lib/cloudflare/container.d.ts +6 -0
  6. package/lib/cloudflare/container.d.ts.map +1 -1
  7. package/lib/cloudflare/container.js +32 -25
  8. package/lib/cloudflare/container.js.map +1 -1
  9. package/lib/cloudflare/index.d.ts +2 -0
  10. package/lib/cloudflare/index.d.ts.map +1 -1
  11. package/lib/cloudflare/index.js +2 -0
  12. package/lib/cloudflare/index.js.map +1 -1
  13. package/lib/cloudflare/orange.d.ts +37 -0
  14. package/lib/cloudflare/orange.d.ts.map +1 -0
  15. package/lib/cloudflare/orange.js +48 -0
  16. package/lib/cloudflare/orange.js.map +1 -0
  17. package/lib/cloudflare/redirect-rule.d.ts +168 -0
  18. package/lib/cloudflare/redirect-rule.d.ts.map +1 -0
  19. package/lib/cloudflare/redirect-rule.js +380 -0
  20. package/lib/cloudflare/redirect-rule.js.map +1 -0
  21. package/lib/cloudflare/website.js +2 -2
  22. package/lib/cloudflare/website.js.map +1 -1
  23. package/lib/cloudflare/worker/miniflare-worker-options.d.ts.map +1 -1
  24. package/lib/cloudflare/worker/miniflare-worker-options.js +11 -12
  25. package/lib/cloudflare/worker/miniflare-worker-options.js.map +1 -1
  26. package/lib/cloudflare/worker.d.ts +1 -1
  27. package/lib/fs/file-system-state-store.d.ts.map +1 -1
  28. package/lib/fs/file-system-state-store.js +8 -1
  29. package/lib/fs/file-system-state-store.js.map +1 -1
  30. package/lib/scope.js +1 -1
  31. package/lib/scope.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/build-date.ts +1 -1
  34. package/src/cloudflare/container.ts +44 -30
  35. package/src/cloudflare/index.ts +2 -0
  36. package/src/cloudflare/orange.ts +61 -0
  37. package/src/cloudflare/redirect-rule.ts +642 -0
  38. package/src/cloudflare/website.ts +2 -2
  39. package/src/cloudflare/worker/miniflare-worker-options.ts +14 -12
  40. package/src/fs/file-system-state-store.ts +8 -1
  41. package/src/scope.ts +1 -1
  42. package/templates/astro/package.json +1 -1
  43. package/templates/nuxt/package.json +1 -1
  44. package/templates/react-router/package.json +1 -1
  45. package/templates/rwsdk/package.json +1 -1
  46. package/templates/sveltekit/package.json +1 -1
  47. package/templates/tanstack-start/package.json +1 -1
  48. package/templates/typescript/package.json +1 -1
  49. package/templates/vite/package.json +1 -1
@@ -0,0 +1,642 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import {
4
+ createCloudflareApi,
5
+ type CloudflareApi,
6
+ type CloudflareApiOptions,
7
+ } from "./api.ts";
8
+ import type { CloudflareResponse } from "./response.ts";
9
+ import type { Zone } from "./zone.ts";
10
+
11
+ /**
12
+ * Properties for creating or updating a RedirectRule
13
+ */
14
+ export interface RedirectRuleProps extends CloudflareApiOptions {
15
+ /**
16
+ * The zone where the redirect rule will be applied
17
+ * Can be a zone ID string or a Zone resource
18
+ */
19
+ zone: string | Zone;
20
+
21
+ /**
22
+ * For wildcard redirects: the URL pattern to match
23
+ * Example: "https://*.example.com/files/*"
24
+ * This is mutually exclusive with `expression`
25
+ */
26
+ requestUrl?: string;
27
+
28
+ /**
29
+ * For dynamic redirects: a Cloudflare Rules expression
30
+ * Example: 'http.request.uri.path matches "/autodiscover\\.(xml|src)$"'
31
+ * This is mutually exclusive with `requestUrl`
32
+ * @see https://developers.cloudflare.com/ruleset-engine/rules-language/expressions/
33
+ */
34
+ expression?: string;
35
+
36
+ /**
37
+ * The target URL for the redirect
38
+ * Can include placeholders like ${1}, ${2} for wildcard matches
39
+ * Example: "https://example.com/${1}/files/${2}"
40
+ */
41
+ targetUrl: string;
42
+
43
+ /**
44
+ * HTTP status code for the redirect
45
+ * @default 301
46
+ */
47
+ statusCode?: 301 | 302 | 303 | 307 | 308;
48
+
49
+ /**
50
+ * Whether to preserve query string parameters
51
+ * @default true
52
+ */
53
+ preserveQueryString?: boolean;
54
+ }
55
+
56
+ /**
57
+ * Cloudflare Ruleset response format
58
+ */
59
+ interface CloudflareRuleset {
60
+ id: string;
61
+ name: string;
62
+ description?: string;
63
+ kind: string;
64
+ version: string;
65
+ rules: CloudflareRule[];
66
+ last_updated: string;
67
+ phase: string;
68
+ }
69
+
70
+ /**
71
+ * Cloudflare Rule response format
72
+ */
73
+ interface CloudflareRule {
74
+ id: string;
75
+ version: string;
76
+ action: string;
77
+ expression: string;
78
+ description?: string;
79
+ last_updated: string;
80
+ ref: string;
81
+ enabled: boolean;
82
+ action_parameters?: {
83
+ from_value?: {
84
+ status_code?: number;
85
+ target_url?: {
86
+ value?: string;
87
+ expression?: string;
88
+ };
89
+ preserve_query_string?: boolean;
90
+ };
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Output returned after RedirectRule creation/update
96
+ */
97
+ export interface RedirectRule extends Resource<"cloudflare::RedirectRule"> {
98
+ /**
99
+ * The ID of the redirect rule
100
+ */
101
+ ruleId: string;
102
+
103
+ /**
104
+ * The ID of the ruleset containing this rule
105
+ */
106
+ rulesetId: string;
107
+
108
+ /**
109
+ * The zone ID where the rule is applied
110
+ */
111
+ zoneId: string;
112
+
113
+ /**
114
+ * The request URL pattern (for wildcard redirects)
115
+ */
116
+ requestUrl?: string;
117
+
118
+ /**
119
+ * The expression (for dynamic redirects)
120
+ */
121
+ expression?: string;
122
+
123
+ /**
124
+ * The target URL for the redirect
125
+ */
126
+ targetUrl: string;
127
+
128
+ /**
129
+ * HTTP status code for the redirect
130
+ */
131
+ statusCode: number;
132
+
133
+ /**
134
+ * Whether query string parameters are preserved
135
+ */
136
+ preserveQueryString: boolean;
137
+
138
+ /**
139
+ * Whether the rule is enabled
140
+ */
141
+ enabled: boolean;
142
+
143
+ /**
144
+ * Time when the rule was last updated
145
+ */
146
+ lastUpdated: string;
147
+ }
148
+
149
+ /**
150
+ * A Cloudflare Redirect Rule enables URL redirects and rewrites using Cloudflare's Rules engine.
151
+ * Supports wildcard redirects, static redirects, and dynamic redirects with expressions.
152
+ *
153
+ * @example
154
+ * ## Wildcard Redirect
155
+ *
156
+ * Redirect from a wildcard pattern to a target URL with placeholders.
157
+ *
158
+ * ```ts
159
+ * const wildcardRedirect = await RedirectRule("my-wildcard-redirect", {
160
+ * zone: "example.com",
161
+ * requestUrl: "https://*.example.com/files/*",
162
+ * targetUrl: "https://example.com/${1}/files/${2}",
163
+ * statusCode: 301,
164
+ * preserveQueryString: true
165
+ * });
166
+ * ```
167
+ *
168
+ * @example
169
+ * ## Static Redirect
170
+ *
171
+ * Simple redirect from any request to a static target URL.
172
+ *
173
+ * ```ts
174
+ * const staticRedirect = await RedirectRule("my-static-redirect", {
175
+ * zone: "example.com",
176
+ * targetUrl: "https://example.com/",
177
+ * statusCode: 301,
178
+ * preserveQueryString: true
179
+ * });
180
+ * ```
181
+ *
182
+ * @example
183
+ * ## Dynamic Redirect with Expression
184
+ *
185
+ * Complex redirect using Cloudflare's Rules language for advanced matching.
186
+ *
187
+ * ```ts
188
+ * const dynamicRedirect = await RedirectRule("my-dynamic-redirect", {
189
+ * zone: "example.com",
190
+ * expression: 'http.request.uri.path matches "/autodiscover\\.(xml|src)$"',
191
+ * targetUrl: "https://example.com/not-found",
192
+ * statusCode: 301,
193
+ * preserveQueryString: true
194
+ * });
195
+ * ```
196
+ *
197
+ * @see https://developers.cloudflare.com/rules/url-forwarding/single-redirects/
198
+ */
199
+ export const RedirectRule = Resource(
200
+ "cloudflare::RedirectRule",
201
+ async function (
202
+ this: Context<RedirectRule>,
203
+ _id: string,
204
+ props: RedirectRuleProps,
205
+ ): Promise<RedirectRule> {
206
+ // Create Cloudflare API client
207
+ const api = await createCloudflareApi(props);
208
+
209
+ // Get zone ID
210
+ const zoneId = typeof props.zone === "string" ? props.zone : props.zone.id;
211
+
212
+ if (this.phase === "delete") {
213
+ if (this.output?.ruleId && this.output?.rulesetId) {
214
+ // Let delete errors propagate instead of swallowing them
215
+ await deleteRedirectRule(
216
+ api,
217
+ zoneId,
218
+ this.output.rulesetId,
219
+ this.output.ruleId,
220
+ );
221
+ }
222
+ return this.destroy();
223
+ }
224
+
225
+ // Validate props
226
+ if (props.requestUrl && props.expression) {
227
+ throw new Error(
228
+ "Cannot specify both requestUrl and expression. Use requestUrl for wildcard redirects or expression for dynamic redirects.",
229
+ );
230
+ }
231
+
232
+ const statusCode = props.statusCode ?? 301;
233
+ const preserveQueryString = props.preserveQueryString ?? true;
234
+
235
+ // Build the rule expression
236
+ let ruleExpression: string;
237
+ if (props.requestUrl) {
238
+ // Convert wildcard URL to Cloudflare expression
239
+ ruleExpression = convertWildcardUrlToExpression(props.requestUrl);
240
+ } else if (props.expression) {
241
+ ruleExpression = props.expression;
242
+ } else {
243
+ // Static redirect - match all requests
244
+ ruleExpression = "true";
245
+ }
246
+
247
+ if (
248
+ this.phase === "update" &&
249
+ this.output?.ruleId &&
250
+ this.output?.rulesetId
251
+ ) {
252
+ // Update existing rule
253
+ const updatedRule = await updateRedirectRule(
254
+ api,
255
+ zoneId,
256
+ this.output.rulesetId,
257
+ this.output.ruleId,
258
+ {
259
+ expression: ruleExpression,
260
+ targetUrl: props.targetUrl,
261
+ statusCode,
262
+ preserveQueryString,
263
+ },
264
+ );
265
+
266
+ return this({
267
+ ruleId: updatedRule.id,
268
+ rulesetId: this.output.rulesetId,
269
+ zoneId,
270
+ requestUrl: props.requestUrl,
271
+ expression: props.expression,
272
+ targetUrl: props.targetUrl,
273
+ statusCode,
274
+ preserveQueryString,
275
+ enabled: updatedRule.enabled ?? true,
276
+ lastUpdated: updatedRule.last_updated,
277
+ });
278
+ }
279
+
280
+ // Get or create the redirect ruleset for this zone
281
+ const rulesetId = await getOrCreateRedirectRuleset(api, zoneId);
282
+
283
+ // Create the rule
284
+ const createdRule = await createRedirectRule(api, zoneId, rulesetId, {
285
+ expression: ruleExpression,
286
+ targetUrl: props.targetUrl,
287
+ statusCode,
288
+ preserveQueryString,
289
+ });
290
+
291
+ return this({
292
+ ruleId: createdRule.id,
293
+ rulesetId,
294
+ zoneId,
295
+ requestUrl: props.requestUrl,
296
+ expression: props.expression,
297
+ targetUrl: props.targetUrl,
298
+ statusCode,
299
+ preserveQueryString,
300
+ enabled: createdRule.enabled ?? true,
301
+ lastUpdated: createdRule.last_updated,
302
+ });
303
+ },
304
+ );
305
+
306
+ /**
307
+ * Get existing redirect ruleset for a zone
308
+ */
309
+ async function getRedirectRuleset(
310
+ api: CloudflareApi,
311
+ zoneId: string,
312
+ ): Promise<string | null> {
313
+ const response = await api.get(`/zones/${zoneId}/rulesets`);
314
+
315
+ if (!response.ok) {
316
+ return null;
317
+ }
318
+
319
+ const result = (await response.json()) as CloudflareResponse<
320
+ CloudflareRuleset[]
321
+ >;
322
+ const redirectRuleset = result.result.find(
323
+ (ruleset) => ruleset.phase === "http_request_dynamic_redirect",
324
+ );
325
+
326
+ return redirectRuleset?.id || null;
327
+ }
328
+
329
+ /**
330
+ * Create a new redirect ruleset for a zone
331
+ */
332
+ async function createRedirectRuleset(
333
+ api: CloudflareApi,
334
+ zoneId: string,
335
+ ): Promise<string> {
336
+ const response = await api.post(`/zones/${zoneId}/rulesets`, {
337
+ name: "Zone-level redirect ruleset",
338
+ description: "Redirect rules for the zone",
339
+ kind: "zone",
340
+ phase: "http_request_dynamic_redirect",
341
+ });
342
+
343
+ if (!response.ok) {
344
+ throw new Error(
345
+ `Failed to create redirect ruleset: ${response.statusText}`,
346
+ );
347
+ }
348
+
349
+ const result =
350
+ (await response.json()) as CloudflareResponse<CloudflareRuleset>;
351
+ return result.result.id;
352
+ }
353
+
354
+ /**
355
+ * Get or create the redirect ruleset for a zone
356
+ */
357
+ async function getOrCreateRedirectRuleset(
358
+ api: CloudflareApi,
359
+ zoneId: string,
360
+ ): Promise<string> {
361
+ const existingRulesetId = await getRedirectRuleset(api, zoneId);
362
+ if (existingRulesetId) {
363
+ return existingRulesetId;
364
+ }
365
+
366
+ return await createRedirectRuleset(api, zoneId);
367
+ }
368
+
369
+ /**
370
+ * Create a new redirect rule by updating the ruleset
371
+ */
372
+ async function createRedirectRule(
373
+ api: CloudflareApi,
374
+ zoneId: string,
375
+ rulesetId: string,
376
+ ruleData: {
377
+ expression: string;
378
+ targetUrl: string;
379
+ statusCode: number;
380
+ preserveQueryString: boolean;
381
+ },
382
+ ): Promise<CloudflareRule> {
383
+ // Get current ruleset
384
+ const ruleset = await getRuleset(api, zoneId, rulesetId);
385
+ if (!ruleset) {
386
+ throw new Error(`Ruleset ${rulesetId} not found`);
387
+ }
388
+
389
+ // Create new rule object
390
+ const newRule = {
391
+ action: "redirect" as const,
392
+ expression: ruleData.expression,
393
+ action_parameters: {
394
+ from_value: {
395
+ status_code: ruleData.statusCode,
396
+ target_url: {
397
+ value: ruleData.targetUrl,
398
+ },
399
+ preserve_query_string: ruleData.preserveQueryString,
400
+ },
401
+ },
402
+ enabled: true,
403
+ };
404
+
405
+ // Update ruleset with new rule
406
+ const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
407
+ name: ruleset.name,
408
+ description: ruleset.description,
409
+ kind: ruleset.kind,
410
+ phase: ruleset.phase,
411
+ rules: [...ruleset.rules, newRule],
412
+ });
413
+
414
+ if (!response.ok) {
415
+ const errorBody = await response.text();
416
+ throw new Error(
417
+ `Failed to create redirect rule: ${response.statusText}\nResponse: ${errorBody}`,
418
+ );
419
+ }
420
+
421
+ const result =
422
+ (await response.json()) as CloudflareResponse<CloudflareRuleset>;
423
+ // Return the last rule (the one we just added)
424
+ const createdRule = result.result.rules[result.result.rules.length - 1];
425
+ return createdRule;
426
+ }
427
+
428
+ /**
429
+ * Update an existing redirect rule by updating the ruleset
430
+ */
431
+ async function updateRedirectRule(
432
+ api: CloudflareApi,
433
+ zoneId: string,
434
+ rulesetId: string,
435
+ ruleId: string,
436
+ ruleData: {
437
+ expression: string;
438
+ targetUrl: string;
439
+ statusCode: number;
440
+ preserveQueryString: boolean;
441
+ },
442
+ ): Promise<CloudflareRule> {
443
+ // Get current ruleset
444
+ const ruleset = await getRuleset(api, zoneId, rulesetId);
445
+ if (!ruleset) {
446
+ throw new Error(`Ruleset ${rulesetId} not found`);
447
+ }
448
+
449
+ // Find and update the rule
450
+ const updatedRules = ruleset.rules.map((rule) => {
451
+ if (rule.id === ruleId) {
452
+ return {
453
+ ...rule,
454
+ action: "redirect" as const,
455
+ expression: ruleData.expression,
456
+ action_parameters: {
457
+ from_value: {
458
+ status_code: ruleData.statusCode,
459
+ target_url: {
460
+ value: ruleData.targetUrl,
461
+ },
462
+ preserve_query_string: ruleData.preserveQueryString,
463
+ },
464
+ },
465
+ enabled: true,
466
+ };
467
+ }
468
+ return rule;
469
+ });
470
+
471
+ // Update ruleset with modified rules
472
+ const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
473
+ name: ruleset.name,
474
+ description: ruleset.description,
475
+ kind: ruleset.kind,
476
+ phase: ruleset.phase,
477
+ rules: updatedRules,
478
+ });
479
+
480
+ if (!response.ok) {
481
+ throw new Error(`Failed to update redirect rule: ${response.statusText}`);
482
+ }
483
+
484
+ const result =
485
+ (await response.json()) as CloudflareResponse<CloudflareRuleset>;
486
+ // Find and return the updated rule
487
+ const updatedRule = result.result.rules.find((rule) => rule.id === ruleId);
488
+ if (!updatedRule) {
489
+ throw new Error(`Updated rule ${ruleId} not found in response`);
490
+ }
491
+ return updatedRule;
492
+ }
493
+
494
+ /**
495
+ * Get a ruleset with its rules
496
+ */
497
+ async function getRuleset(
498
+ api: CloudflareApi,
499
+ zoneId: string,
500
+ rulesetId: string,
501
+ ): Promise<CloudflareRuleset | null> {
502
+ const response = await api.get(`/zones/${zoneId}/rulesets/${rulesetId}`);
503
+
504
+ if (!response.ok) {
505
+ return null;
506
+ }
507
+
508
+ const result =
509
+ (await response.json()) as CloudflareResponse<CloudflareRuleset>;
510
+ return result.result;
511
+ }
512
+
513
+ /**
514
+ * Find a specific rule in a ruleset
515
+ */
516
+ export async function findRuleInRuleset(
517
+ api: CloudflareApi,
518
+ zoneId: string,
519
+ rulesetId: string,
520
+ ruleId: string,
521
+ ): Promise<CloudflareRule | null> {
522
+ const response = await api.get(`/zones/${zoneId}/rulesets/${rulesetId}`);
523
+
524
+ if (!response.ok) {
525
+ throw new Error(
526
+ `Failed to get ruleset: ${response.status} ${response.statusText}`,
527
+ );
528
+ }
529
+
530
+ const rulesetData =
531
+ (await response.json()) as CloudflareResponse<CloudflareRuleset>;
532
+ const rule = rulesetData.result.rules.find((r) => r.id === ruleId);
533
+
534
+ return rule || null;
535
+ }
536
+
537
+ /**
538
+ * Delete a redirect rule by updating the ruleset to exclude it
539
+ */
540
+ async function deleteRedirectRule(
541
+ api: CloudflareApi,
542
+ zoneId: string,
543
+ rulesetId: string,
544
+ ruleId: string,
545
+ ): Promise<void> {
546
+ const ruleset = await getRuleset(api, zoneId, rulesetId);
547
+
548
+ if (!ruleset) {
549
+ throw new Error(`Ruleset ${rulesetId} not found for deletion`);
550
+ }
551
+
552
+ // Filter out the rule to delete
553
+ const updatedRules = ruleset.rules.filter((rule) => rule.id !== ruleId);
554
+
555
+ // Update the ruleset with the filtered rules
556
+ const response = await api.put(`/zones/${zoneId}/rulesets/${rulesetId}`, {
557
+ name: ruleset.name,
558
+ description: ruleset.description,
559
+ kind: ruleset.kind,
560
+ phase: ruleset.phase,
561
+ rules: updatedRules,
562
+ });
563
+
564
+ if (!response.ok) {
565
+ throw new Error(`Failed to delete redirect rule: ${response.statusText}`);
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Convert a wildcard URL pattern to a Cloudflare Rules expression
571
+ * Uses operators available on Free plans (no regex matching)
572
+ */
573
+ function convertWildcardUrlToExpression(wildcardUrl: string): string {
574
+ // Parse the URL to extract components
575
+ const url = new URL(wildcardUrl);
576
+ const hostname = url.hostname;
577
+ const pathname = url.pathname;
578
+
579
+ let expression = "";
580
+
581
+ // Handle hostname wildcards
582
+ if (hostname.includes("*")) {
583
+ // For simple wildcard patterns, use contains or ends_with operators
584
+ if (hostname.startsWith("*")) {
585
+ // *.example.com -> http.host ends_with ".example.com"
586
+ const suffix = hostname.substring(1); // Remove the *
587
+ expression += `http.host ends_with "${suffix}"`;
588
+ } else if (hostname.endsWith("*")) {
589
+ // subdomain.* -> http.host starts_with "subdomain."
590
+ const prefix = hostname.substring(0, hostname.length - 1); // Remove the *
591
+ expression += `http.host starts_with "${prefix}"`;
592
+ } else {
593
+ // More complex wildcards - fallback to a broader match
594
+ const parts = hostname.split("*");
595
+ if (parts.length === 2) {
596
+ expression += `http.host starts_with "${parts[0]}" and http.host ends_with "${parts[1]}"`;
597
+ } else {
598
+ // Fallback to domain contains for complex patterns
599
+ const baseDomain = hostname.replace(/^\*\./, "").replace(/\.\*$/, "");
600
+ expression += `http.host contains "${baseDomain}"`;
601
+ }
602
+ }
603
+ } else {
604
+ expression += `http.host == "${hostname}"`;
605
+ }
606
+
607
+ // Handle pathname wildcards
608
+ if (pathname.includes("*")) {
609
+ if (pathname.endsWith("*")) {
610
+ // /files/* -> starts_with "/files/"
611
+ const prefix = pathname.substring(0, pathname.length - 1); // Remove the *
612
+ expression += ` and http.request.uri.path starts_with "${prefix}"`;
613
+ } else if (pathname.startsWith("*")) {
614
+ // *.html -> ends_with ".html"
615
+ const suffix = pathname.substring(1); // Remove the *
616
+ expression += ` and http.request.uri.path ends_with "${suffix}"`;
617
+ } else {
618
+ // More complex wildcards - use contains
619
+ const parts = pathname.split("*");
620
+ if (parts.length === 2 && parts[0] && parts[1]) {
621
+ expression += ` and http.request.uri.path starts_with "${parts[0]}" and http.request.uri.path ends_with "${parts[1]}"`;
622
+ } else {
623
+ // Fallback to contains for the non-wildcard part
624
+ const nonWildcardPart = parts.find((part) => part.length > 0) || "";
625
+ if (nonWildcardPart) {
626
+ expression += ` and http.request.uri.path contains "${nonWildcardPart}"`;
627
+ }
628
+ }
629
+ }
630
+ } else if (pathname !== "/") {
631
+ expression += ` and http.request.uri.path == "${pathname}"`;
632
+ }
633
+
634
+ // Handle protocol
635
+ if (url.protocol === "https:") {
636
+ expression += " and ssl";
637
+ } else if (url.protocol === "http:") {
638
+ expression += " and not ssl";
639
+ }
640
+
641
+ return expression;
642
+ }
@@ -183,8 +183,8 @@ export default {
183
183
  return new Response("Not Found", { status: 404 });
184
184
  },
185
185
  };`,
186
- url: true,
187
- adopt: true,
186
+ url: props.url ?? true,
187
+ adopt: props.adopt ?? true,
188
188
  dev: props.dev
189
189
  ? {
190
190
  command: props.dev.command,
@@ -225,11 +225,14 @@ export function buildMiniflareWorkerOptions({
225
225
  compatibilityDate,
226
226
  compatibilityFlags,
227
227
  unsafeDirectSockets: [{ entrypoint: undefined, proxy: true }],
228
- // containerEngine: {
229
- // localDocker: {
230
- // socketPath: "/var/run/docker.sock",
231
- // }
232
- // }
228
+ containerEngine: {
229
+ localDocker: {
230
+ socketPath:
231
+ process.platform === "win32"
232
+ ? "//./pipe/docker_engine"
233
+ : "unix:///var/run/docker.sock",
234
+ },
235
+ },
233
236
  };
234
237
  for (const [name, binding] of Object.entries(bindings ?? {})) {
235
238
  if (typeof binding === "string") {
@@ -519,19 +522,18 @@ export function buildMiniflareWorkerOptions({
519
522
  break;
520
523
  }
521
524
  case "container": {
525
+ if (binding.dev?.remote) {
526
+ throw new Error(
527
+ `Container bindings with remote: true are not supported for locally emulated workers. Worker "${name}" is locally emulated but is bound to container "${name}" with remote: true.`,
528
+ );
529
+ }
522
530
  (options.durableObjects ??= {})[name] = {
523
531
  className: binding.className,
524
532
  scriptName: binding.scriptName,
525
533
  useSQLite: binding.sqlite,
526
534
  container: {
527
- imageName: binding.image.name,
535
+ imageName: binding.image.imageRef,
528
536
  },
529
- // namespaceId: binding.namespaceId,
530
- // unsafeUniqueKey?: string | typeof kUnsafeEphemeralUniqueKey | undefined;
531
- // unsafePreventEviction?: boolean | undefined;
532
- // remoteProxyConnectionString: binding.local
533
- // ? undefined
534
- // : remoteProxyConnectionString,
535
537
  };
536
538
  if (!binding.scriptName || binding.scriptName === workerName) {
537
539
  options.unsafeDirectSockets!.push({
@@ -131,9 +131,16 @@ export class FileSystemStateStore implements StateStore {
131
131
  if (key.includes("/")) {
132
132
  //todo(michael): remove this next time we do a breaking change
133
133
  //* windows doesn't support ":" in file paths, but we already use ":"
134
- //* so now we use both to prevent breaking changes`
134
+ //* so now we use both to prevent breaking changes
135
135
  key = key.replaceAll("/", ALCHEMY_SEPERATOR_CHAR);
136
136
  }
137
+ //todo(michael): remove this next time we do a breaking change
138
+ //* windows doesn't support "*" in file paths, but we already use "*"
139
+ //* when making cloudflare routes containing "*"
140
+ //* so now we use "+" on windows but "*" on mac/linux to prevent breaking changes
141
+ if (process.platform === "win32") {
142
+ key = key.replaceAll("*", "+");
143
+ }
137
144
  return path.join(this.dir, `${key}.json`);
138
145
  }
139
146
  }