alchemy 2.0.0-beta.35 → 2.0.0-beta.36
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/bin/exec.js +27 -0
- package/bin/exec.js.map +1 -1
- package/lib/Cli/main.d.ts.map +1 -1
- package/lib/Cli/main.js +2 -0
- package/lib/Cli/main.js.map +1 -1
- package/lib/Cloudflare/Images/Images.d.ts +103 -0
- package/lib/Cloudflare/Images/Images.d.ts.map +1 -0
- package/lib/Cloudflare/Images/Images.js +82 -0
- package/lib/Cloudflare/Images/Images.js.map +1 -0
- package/lib/Cloudflare/Images/ImagesBinding.d.ts +73 -0
- package/lib/Cloudflare/Images/ImagesBinding.d.ts.map +1 -0
- package/lib/Cloudflare/Images/ImagesBinding.js +83 -0
- package/lib/Cloudflare/Images/ImagesBinding.js.map +1 -0
- package/lib/Cloudflare/Images/index.d.ts +3 -0
- package/lib/Cloudflare/Images/index.d.ts.map +1 -0
- package/lib/Cloudflare/Images/index.js +3 -0
- package/lib/Cloudflare/Images/index.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +3 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.d.ts +135 -0
- package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
- package/lib/Cloudflare/R2/R2Bucket.js +130 -7
- package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts +1 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +34 -23
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/index.d.ts +1 -0
- package/lib/Cloudflare/index.d.ts.map +1 -1
- package/lib/Cloudflare/index.js +1 -0
- package/lib/Cloudflare/index.js.map +1 -1
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +2 -0
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Util/LocalhostDns.d.ts +19 -0
- package/lib/Util/LocalhostDns.d.ts.map +1 -0
- package/lib/Util/LocalhostDns.js +55 -0
- package/lib/Util/LocalhostDns.js.map +1 -0
- package/lib/Util/PlatformServices.d.ts.map +1 -1
- package/lib/Util/PlatformServices.js +2 -0
- package/lib/Util/PlatformServices.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/Cli/main.ts +3 -0
- package/src/Cloudflare/Images/Images.ts +121 -0
- package/src/Cloudflare/Images/ImagesBinding.ts +192 -0
- package/src/Cloudflare/Images/index.ts +2 -0
- package/src/Cloudflare/Providers.ts +3 -0
- package/src/Cloudflare/R2/R2Bucket.ts +240 -7
- package/src/Cloudflare/Workers/InferEnv.ts +7 -3
- package/src/Cloudflare/Workers/Worker.ts +47 -28
- package/src/Cloudflare/index.ts +1 -0
- package/src/Test/Vitest.ts +3 -0
- package/src/Util/LocalhostDns.ts +58 -0
- package/src/Util/PlatformServices.ts +2 -0
|
@@ -42,6 +42,60 @@ export type R2BucketCustomDomain = {
|
|
|
42
42
|
minTLS?: "1.0" | "1.1" | "1.2" | "1.3";
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
export type R2BucketLifecycleCondition =
|
|
46
|
+
| {
|
|
47
|
+
type: "Age";
|
|
48
|
+
/**
|
|
49
|
+
* Maximum age of an object, in seconds, before the rule's action applies.
|
|
50
|
+
*/
|
|
51
|
+
maxAge: number;
|
|
52
|
+
}
|
|
53
|
+
| {
|
|
54
|
+
type: "Date";
|
|
55
|
+
/**
|
|
56
|
+
* Absolute date (ISO 8601) at which the rule's action applies.
|
|
57
|
+
*/
|
|
58
|
+
date: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type R2BucketLifecycleRule = {
|
|
62
|
+
/**
|
|
63
|
+
* Unique identifier for the rule within the bucket.
|
|
64
|
+
*/
|
|
65
|
+
id: string;
|
|
66
|
+
/**
|
|
67
|
+
* Whether the rule is enabled.
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
enabled?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Object key prefix the rule applies to. Use `""` (or omit) to match all
|
|
73
|
+
* objects in the bucket.
|
|
74
|
+
* @default ""
|
|
75
|
+
*/
|
|
76
|
+
prefix?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Abort incomplete multipart uploads after the configured age.
|
|
79
|
+
*/
|
|
80
|
+
abortMultipartUploadsTransition?: {
|
|
81
|
+
condition?: { type: "Age"; maxAge: number };
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Delete matching objects after the configured age or on a specific date.
|
|
85
|
+
*/
|
|
86
|
+
deleteObjectsTransition?: {
|
|
87
|
+
condition?: R2BucketLifecycleCondition;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Transition matching objects to a different storage class. Cloudflare R2
|
|
91
|
+
* only supports transitioning to `InfrequentAccess` today.
|
|
92
|
+
*/
|
|
93
|
+
storageClassTransitions?: {
|
|
94
|
+
condition: R2BucketLifecycleCondition;
|
|
95
|
+
storageClass: "InfrequentAccess";
|
|
96
|
+
}[];
|
|
97
|
+
};
|
|
98
|
+
|
|
45
99
|
export type R2BucketProps = {
|
|
46
100
|
/**
|
|
47
101
|
* Name of the bucket. If omitted, a unique name will be generated.
|
|
@@ -67,6 +121,12 @@ export type R2BucketProps = {
|
|
|
67
121
|
* to remove all custom domains.
|
|
68
122
|
*/
|
|
69
123
|
domains?: R2BucketCustomDomain[];
|
|
124
|
+
/**
|
|
125
|
+
* Object lifecycle rules applied to the bucket. Pass an empty array (or
|
|
126
|
+
* omit) to clear all lifecycle rules. See the Cloudflare R2 docs for
|
|
127
|
+
* supported transitions.
|
|
128
|
+
*/
|
|
129
|
+
lifecycleRules?: R2BucketLifecycleRule[];
|
|
70
130
|
};
|
|
71
131
|
|
|
72
132
|
export type R2Bucket = Resource<
|
|
@@ -79,6 +139,7 @@ export type R2Bucket = Resource<
|
|
|
79
139
|
location: R2Bucket.Location | undefined;
|
|
80
140
|
accountId: string;
|
|
81
141
|
domains: R2Bucket.CustomDomain[];
|
|
142
|
+
lifecycleRules: R2Bucket.LifecycleRule[];
|
|
82
143
|
},
|
|
83
144
|
never,
|
|
84
145
|
Cloudflare.Providers
|
|
@@ -175,6 +236,63 @@ export type R2Bucket = Resource<
|
|
|
175
236
|
* ],
|
|
176
237
|
* });
|
|
177
238
|
* ```
|
|
239
|
+
*
|
|
240
|
+
* @section Object Lifecycle Rules
|
|
241
|
+
*
|
|
242
|
+
* Configure lifecycle rules to automatically delete objects, abort
|
|
243
|
+
* incomplete multipart uploads, or transition objects to InfrequentAccess
|
|
244
|
+
* storage. Pass an empty array (or omit) to clear all rules. See the
|
|
245
|
+
* [Cloudflare R2 docs](https://developers.cloudflare.com/r2/buckets/object-lifecycles/)
|
|
246
|
+
* for details and limits (max 1000 rules per bucket).
|
|
247
|
+
*
|
|
248
|
+
* @example Delete objects 30 days after upload
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
|
|
251
|
+
* lifecycleRules: [
|
|
252
|
+
* {
|
|
253
|
+
* id: "expire-old-objects",
|
|
254
|
+
* deleteObjectsTransition: {
|
|
255
|
+
* condition: { type: "Age", maxAge: 60 * 60 * 24 * 30 },
|
|
256
|
+
* },
|
|
257
|
+
* },
|
|
258
|
+
* ],
|
|
259
|
+
* });
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* @example Transition to InfrequentAccess after 60 days, delete after 365
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
|
|
265
|
+
* lifecycleRules: [
|
|
266
|
+
* {
|
|
267
|
+
* id: "archive-then-delete",
|
|
268
|
+
* prefix: "logs/",
|
|
269
|
+
* storageClassTransitions: [
|
|
270
|
+
* {
|
|
271
|
+
* condition: { type: "Age", maxAge: 60 * 60 * 24 * 60 },
|
|
272
|
+
* storageClass: "InfrequentAccess",
|
|
273
|
+
* },
|
|
274
|
+
* ],
|
|
275
|
+
* deleteObjectsTransition: {
|
|
276
|
+
* condition: { type: "Age", maxAge: 60 * 60 * 24 * 365 },
|
|
277
|
+
* },
|
|
278
|
+
* },
|
|
279
|
+
* ],
|
|
280
|
+
* });
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @example Abort incomplete multipart uploads after 7 days
|
|
284
|
+
* ```typescript
|
|
285
|
+
* const bucket = yield* Cloudflare.R2Bucket("MyBucket", {
|
|
286
|
+
* lifecycleRules: [
|
|
287
|
+
* {
|
|
288
|
+
* id: "abort-stale-uploads",
|
|
289
|
+
* abortMultipartUploadsTransition: {
|
|
290
|
+
* condition: { type: "Age", maxAge: 60 * 60 * 24 * 7 },
|
|
291
|
+
* },
|
|
292
|
+
* },
|
|
293
|
+
* ],
|
|
294
|
+
* });
|
|
295
|
+
* ```
|
|
178
296
|
*/
|
|
179
297
|
export const R2Bucket = Resource<R2Bucket>("Cloudflare.R2Bucket")({
|
|
180
298
|
bind: R2BucketBinding.bind,
|
|
@@ -184,6 +302,23 @@ export declare namespace R2Bucket {
|
|
|
184
302
|
export type StorageClass = "Standard" | "InfrequentAccess";
|
|
185
303
|
export type Jurisdiction = "default" | "eu" | "fedramp";
|
|
186
304
|
export type Location = "apac" | "eeur" | "enam" | "weur" | "wnam" | "oc";
|
|
305
|
+
export type LifecycleRule = {
|
|
306
|
+
id: string;
|
|
307
|
+
enabled: boolean;
|
|
308
|
+
prefix: string;
|
|
309
|
+
abortMultipartUploadsTransition:
|
|
310
|
+
| { condition: { type: "Age"; maxAge: number } | undefined }
|
|
311
|
+
| undefined;
|
|
312
|
+
deleteObjectsTransition:
|
|
313
|
+
| { condition: R2BucketLifecycleCondition | undefined }
|
|
314
|
+
| undefined;
|
|
315
|
+
storageClassTransitions:
|
|
316
|
+
| {
|
|
317
|
+
condition: R2BucketLifecycleCondition;
|
|
318
|
+
storageClass: "InfrequentAccess";
|
|
319
|
+
}[]
|
|
320
|
+
| undefined;
|
|
321
|
+
};
|
|
187
322
|
export type CustomDomain = {
|
|
188
323
|
domain: string;
|
|
189
324
|
zoneId: string | undefined;
|
|
@@ -224,6 +359,8 @@ export const R2BucketProvider = () =>
|
|
|
224
359
|
const createBucketDomainCustom = yield* r2.createBucketDomainCustom;
|
|
225
360
|
const updateBucketDomainCustom = yield* r2.updateBucketDomainCustom;
|
|
226
361
|
const deleteBucketDomainCustom = yield* r2.deleteBucketDomainCustom;
|
|
362
|
+
const getBucketLifecycle = yield* r2.getBucketLifecycle;
|
|
363
|
+
const putBucketLifecycle = yield* r2.putBucketLifecycle;
|
|
227
364
|
|
|
228
365
|
const createBucketName = (id: string, name: string | undefined) =>
|
|
229
366
|
Effect.gen(function* () {
|
|
@@ -252,7 +389,7 @@ export const R2BucketProvider = () =>
|
|
|
252
389
|
}).pipe(
|
|
253
390
|
Effect.retry({
|
|
254
391
|
while: isNoSuchBucket,
|
|
255
|
-
schedule:
|
|
392
|
+
schedule: r2BucketEndpointConsistencySchedule,
|
|
256
393
|
}),
|
|
257
394
|
Effect.map((response) =>
|
|
258
395
|
response.domains.map(toCustomDomainAttributes),
|
|
@@ -351,7 +488,7 @@ export const R2BucketProvider = () =>
|
|
|
351
488
|
}).pipe(
|
|
352
489
|
Effect.retry({
|
|
353
490
|
while: isNoSuchBucket,
|
|
354
|
-
schedule:
|
|
491
|
+
schedule: r2BucketEndpointConsistencySchedule,
|
|
355
492
|
}),
|
|
356
493
|
);
|
|
357
494
|
return toCustomDomainAttributes({ ...created, zoneId });
|
|
@@ -368,7 +505,7 @@ export const R2BucketProvider = () =>
|
|
|
368
505
|
}).pipe(
|
|
369
506
|
Effect.retry({
|
|
370
507
|
while: isNoSuchBucket,
|
|
371
|
-
schedule:
|
|
508
|
+
schedule: r2BucketEndpointConsistencySchedule,
|
|
372
509
|
}),
|
|
373
510
|
);
|
|
374
511
|
return toCustomDomainAttributes({
|
|
@@ -383,6 +520,45 @@ export const R2BucketProvider = () =>
|
|
|
383
520
|
return applied.sort((a, b) => a.domain.localeCompare(b.domain));
|
|
384
521
|
});
|
|
385
522
|
|
|
523
|
+
const reconcileLifecycleRules = (
|
|
524
|
+
bucketName: string,
|
|
525
|
+
jurisdiction: R2Bucket.Jurisdiction,
|
|
526
|
+
desired: R2BucketLifecycleRule[],
|
|
527
|
+
) =>
|
|
528
|
+
Effect.gen(function* () {
|
|
529
|
+
const observed = yield* getBucketLifecycle({
|
|
530
|
+
accountId,
|
|
531
|
+
bucketName,
|
|
532
|
+
jurisdiction,
|
|
533
|
+
}).pipe(
|
|
534
|
+
Effect.retry({
|
|
535
|
+
while: isNoSuchBucket,
|
|
536
|
+
schedule: r2BucketEndpointConsistencySchedule,
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const observedRules = (observed.rules ?? []).map(toLifecycleRule);
|
|
541
|
+
const desiredRules = desired.map(normalizeLifecycleRule);
|
|
542
|
+
|
|
543
|
+
if (deepEqual(observedRules, desiredRules)) {
|
|
544
|
+
return desiredRules;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
yield* putBucketLifecycle({
|
|
548
|
+
accountId,
|
|
549
|
+
bucketName,
|
|
550
|
+
jurisdiction,
|
|
551
|
+
rules: desired.map(toLifecyclePutPayload),
|
|
552
|
+
}).pipe(
|
|
553
|
+
Effect.retry({
|
|
554
|
+
while: isNoSuchBucket,
|
|
555
|
+
schedule: r2BucketEndpointConsistencySchedule,
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return desiredRules;
|
|
560
|
+
});
|
|
561
|
+
|
|
386
562
|
return {
|
|
387
563
|
stables: ["bucketName", "accountId"],
|
|
388
564
|
diff: Effect.fn(function* ({ id, olds = {}, news = {}, output }) {
|
|
@@ -412,6 +588,9 @@ export const R2BucketProvider = () =>
|
|
|
412
588
|
if (!deepEqual(olds.domains, news.domains)) {
|
|
413
589
|
return { action: "update" } as const;
|
|
414
590
|
}
|
|
591
|
+
if (!deepEqual(olds.lifecycleRules, news.lifecycleRules)) {
|
|
592
|
+
return { action: "update" } as const;
|
|
593
|
+
}
|
|
415
594
|
}),
|
|
416
595
|
reconcile: Effect.fn(function* ({ id, news = {}, output }) {
|
|
417
596
|
const name = yield* createBucketName(id, news.name);
|
|
@@ -481,9 +660,16 @@ export const R2BucketProvider = () =>
|
|
|
481
660
|
output?.domains ?? [],
|
|
482
661
|
);
|
|
483
662
|
|
|
663
|
+
const lifecycleRules = yield* reconcileLifecycleRules(
|
|
664
|
+
attrs.bucketName,
|
|
665
|
+
attrs.jurisdiction,
|
|
666
|
+
news.lifecycleRules ?? [],
|
|
667
|
+
);
|
|
668
|
+
|
|
484
669
|
return {
|
|
485
670
|
...attrs,
|
|
486
671
|
domains,
|
|
672
|
+
lifecycleRules,
|
|
487
673
|
};
|
|
488
674
|
}),
|
|
489
675
|
delete: Effect.fn(function* ({ output }) {
|
|
@@ -519,6 +705,7 @@ export const R2BucketProvider = () =>
|
|
|
519
705
|
location: normalizeLocation(bucket.location),
|
|
520
706
|
accountId: acct,
|
|
521
707
|
domains: output?.domains ?? [],
|
|
708
|
+
lifecycleRules: output?.lifecycleRules ?? [],
|
|
522
709
|
})),
|
|
523
710
|
Effect.catchTag("NoSuchBucket", () => Effect.succeed(undefined)),
|
|
524
711
|
);
|
|
@@ -527,10 +714,11 @@ export const R2BucketProvider = () =>
|
|
|
527
714
|
}),
|
|
528
715
|
);
|
|
529
716
|
|
|
530
|
-
// R2 can make a newly-created bucket visible to `getBucket` before
|
|
531
|
-
//
|
|
532
|
-
// not-found
|
|
533
|
-
|
|
717
|
+
// R2 can make a newly-created bucket visible to `getBucket` before its
|
|
718
|
+
// sub-resource endpoints (custom domains, lifecycle) accept it. Retry only
|
|
719
|
+
// that narrow `NoSuchBucket` lag here; not-found sub-resources are still
|
|
720
|
+
// treated as terminal for idempotent deletes.
|
|
721
|
+
const r2BucketEndpointConsistencySchedule = Schedule.exponential(100).pipe(
|
|
534
722
|
Schedule.both(Schedule.recurs(5)),
|
|
535
723
|
);
|
|
536
724
|
|
|
@@ -573,6 +761,51 @@ const isMissingCustomDomainOrBucket = (error: unknown): boolean =>
|
|
|
573
761
|
((error as { _tag: unknown })._tag === "DomainNotFound" ||
|
|
574
762
|
(error as { _tag: unknown })._tag === "NoSuchBucket")));
|
|
575
763
|
|
|
764
|
+
type LifecycleRuleResponse = NonNullable<
|
|
765
|
+
r2.GetBucketLifecycleResponse["rules"]
|
|
766
|
+
>[number];
|
|
767
|
+
|
|
768
|
+
const toLifecycleRule = (
|
|
769
|
+
rule: LifecycleRuleResponse,
|
|
770
|
+
): R2Bucket.LifecycleRule => ({
|
|
771
|
+
id: rule.id,
|
|
772
|
+
enabled: rule.enabled,
|
|
773
|
+
prefix: rule.conditions.prefix ?? "",
|
|
774
|
+
abortMultipartUploadsTransition: rule.abortMultipartUploadsTransition
|
|
775
|
+
? { condition: rule.abortMultipartUploadsTransition.condition ?? undefined }
|
|
776
|
+
: undefined,
|
|
777
|
+
deleteObjectsTransition: rule.deleteObjectsTransition
|
|
778
|
+
? { condition: rule.deleteObjectsTransition.condition ?? undefined }
|
|
779
|
+
: undefined,
|
|
780
|
+
storageClassTransitions: rule.storageClassTransitions ?? undefined,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const normalizeLifecycleRule = (
|
|
784
|
+
rule: R2BucketLifecycleRule,
|
|
785
|
+
): R2Bucket.LifecycleRule => ({
|
|
786
|
+
id: rule.id,
|
|
787
|
+
enabled: rule.enabled ?? true,
|
|
788
|
+
prefix: rule.prefix ?? "",
|
|
789
|
+
abortMultipartUploadsTransition: rule.abortMultipartUploadsTransition
|
|
790
|
+
? { condition: rule.abortMultipartUploadsTransition.condition }
|
|
791
|
+
: undefined,
|
|
792
|
+
deleteObjectsTransition: rule.deleteObjectsTransition
|
|
793
|
+
? { condition: rule.deleteObjectsTransition.condition }
|
|
794
|
+
: undefined,
|
|
795
|
+
storageClassTransitions: rule.storageClassTransitions,
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const toLifecyclePutPayload = (
|
|
799
|
+
rule: R2BucketLifecycleRule,
|
|
800
|
+
): NonNullable<r2.PutBucketLifecycleRequest["rules"]>[number] => ({
|
|
801
|
+
id: rule.id,
|
|
802
|
+
enabled: rule.enabled ?? true,
|
|
803
|
+
conditions: { prefix: rule.prefix ?? "" },
|
|
804
|
+
abortMultipartUploadsTransition: rule.abortMultipartUploadsTransition,
|
|
805
|
+
deleteObjectsTransition: rule.deleteObjectsTransition,
|
|
806
|
+
storageClassTransitions: rule.storageClassTransitions,
|
|
807
|
+
});
|
|
808
|
+
|
|
576
809
|
const isNoSuchBucket = (error: unknown): boolean =>
|
|
577
810
|
typeof error === "object" &&
|
|
578
811
|
error !== null &&
|
|
@@ -27,6 +27,10 @@ type GetBindingType<T> = T extends Cloudflare.Assets
|
|
|
27
27
|
? Ai
|
|
28
28
|
: T extends Cloudflare.Artifacts
|
|
29
29
|
? Artifacts
|
|
30
|
-
: T extends Cloudflare.
|
|
31
|
-
?
|
|
32
|
-
:
|
|
30
|
+
: T extends Cloudflare.Images
|
|
31
|
+
? ImagesBinding
|
|
32
|
+
: T extends Cloudflare.Hyperdrive
|
|
33
|
+
? Hyperdrive
|
|
34
|
+
: T extends Cloudflare.DurableObjectNamespaceLike
|
|
35
|
+
? DurableObjectNamespace<Exclude<T["Shape"], undefined>>
|
|
36
|
+
: never;
|
|
@@ -6,7 +6,6 @@ import * as Context from "effect/Context";
|
|
|
6
6
|
import * as Data from "effect/Data";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Exit from "effect/Exit";
|
|
9
|
-
import * as FileSystem from "effect/FileSystem";
|
|
10
9
|
import * as Layer from "effect/Layer";
|
|
11
10
|
import * as Option from "effect/Option";
|
|
12
11
|
import * as Path from "effect/Path";
|
|
@@ -46,7 +45,14 @@ import {
|
|
|
46
45
|
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
47
46
|
import { D1Database } from "../D1/D1Database.ts";
|
|
48
47
|
import { fromCloudflareFetcher } from "../Fetcher.ts";
|
|
49
|
-
import type {
|
|
48
|
+
import type {
|
|
49
|
+
Hyperdrive,
|
|
50
|
+
HyperdriveDevOrigin,
|
|
51
|
+
} from "../Hyperdrive/Hyperdrive.ts";
|
|
52
|
+
import {
|
|
53
|
+
isImages as isImagesBinding,
|
|
54
|
+
type Images as ImagesBinding,
|
|
55
|
+
} from "../Images/Images.ts";
|
|
50
56
|
import type { KVNamespace } from "../KV/KVNamespace.ts";
|
|
51
57
|
import { SidecarLive } from "../Local/Sidecar.ts";
|
|
52
58
|
import { CloudflareLogs } from "../Logs.ts";
|
|
@@ -182,6 +188,8 @@ export type WorkerBindingResource =
|
|
|
182
188
|
| CloudflareQueue
|
|
183
189
|
| AiGateway
|
|
184
190
|
| ArtifactsBinding
|
|
191
|
+
| ImagesBinding
|
|
192
|
+
| Hyperdrive
|
|
185
193
|
| DurableObjectNamespaceLike<any>;
|
|
186
194
|
|
|
187
195
|
export type WorkerBindings = {
|
|
@@ -734,48 +742,61 @@ export const Worker: Platform<
|
|
|
734
742
|
name: bindingName,
|
|
735
743
|
namespace: binding.namespace,
|
|
736
744
|
} as any)
|
|
737
|
-
:
|
|
745
|
+
: isImagesBinding(binding)
|
|
738
746
|
? {
|
|
739
|
-
type: "
|
|
747
|
+
type: "images",
|
|
740
748
|
name: bindingName,
|
|
741
|
-
className: binding.className ?? binding.name,
|
|
742
749
|
}
|
|
743
|
-
: binding
|
|
750
|
+
: isDurableObjectNamespaceLike(binding)
|
|
744
751
|
? {
|
|
745
|
-
type: "
|
|
746
|
-
id: binding.databaseId,
|
|
752
|
+
type: "durable_object_namespace",
|
|
747
753
|
name: bindingName,
|
|
754
|
+
className: binding.className ?? binding.name,
|
|
748
755
|
}
|
|
749
|
-
: binding.Type === "Cloudflare.
|
|
756
|
+
: binding.Type === "Cloudflare.D1Database"
|
|
750
757
|
? {
|
|
751
|
-
type: "
|
|
758
|
+
type: "d1",
|
|
759
|
+
id: binding.databaseId,
|
|
752
760
|
name: bindingName,
|
|
753
|
-
bucketName: binding.bucketName,
|
|
754
|
-
jurisdiction: binding.jurisdiction.pipe(
|
|
755
|
-
Output.map((jurisdiction) =>
|
|
756
|
-
jurisdiction === "default" ? undefined : jurisdiction,
|
|
757
|
-
),
|
|
758
|
-
),
|
|
759
761
|
}
|
|
760
|
-
: binding.Type === "Cloudflare.
|
|
762
|
+
: binding.Type === "Cloudflare.R2Bucket"
|
|
761
763
|
? {
|
|
762
|
-
type: "
|
|
764
|
+
type: "r2_bucket",
|
|
763
765
|
name: bindingName,
|
|
764
|
-
|
|
766
|
+
bucketName: binding.bucketName,
|
|
767
|
+
jurisdiction: binding.jurisdiction.pipe(
|
|
768
|
+
Output.map((jurisdiction) =>
|
|
769
|
+
jurisdiction === "default"
|
|
770
|
+
? undefined
|
|
771
|
+
: jurisdiction,
|
|
772
|
+
),
|
|
773
|
+
),
|
|
765
774
|
}
|
|
766
|
-
: binding.Type === "Cloudflare.
|
|
775
|
+
: binding.Type === "Cloudflare.KVNamespace"
|
|
767
776
|
? {
|
|
768
|
-
type: "
|
|
777
|
+
type: "kv_namespace",
|
|
769
778
|
name: bindingName,
|
|
770
|
-
|
|
779
|
+
namespaceId: binding.namespaceId,
|
|
771
780
|
}
|
|
772
|
-
: binding.Type === "Cloudflare.
|
|
781
|
+
: binding.Type === "Cloudflare.Queue"
|
|
773
782
|
? {
|
|
774
|
-
type: "
|
|
783
|
+
type: "queue",
|
|
775
784
|
name: bindingName,
|
|
785
|
+
queueName: binding.queueName,
|
|
776
786
|
}
|
|
777
|
-
:
|
|
778
|
-
|
|
787
|
+
: binding.Type === "Cloudflare.AiGateway"
|
|
788
|
+
? {
|
|
789
|
+
type: "ai",
|
|
790
|
+
name: bindingName,
|
|
791
|
+
}
|
|
792
|
+
: binding.Type === "Cloudflare.Hyperdrive"
|
|
793
|
+
? {
|
|
794
|
+
type: "hyperdrive",
|
|
795
|
+
name: bindingName,
|
|
796
|
+
id: binding.hyperdriveId,
|
|
797
|
+
}
|
|
798
|
+
: // TODO(sam): handle others
|
|
799
|
+
undefined;
|
|
779
800
|
|
|
780
801
|
if (bindingMeta) {
|
|
781
802
|
yield* resource.bind`${bindingName}`({
|
|
@@ -1059,11 +1080,9 @@ export const LiveWorkerProvider = () =>
|
|
|
1059
1080
|
Provider.effect(
|
|
1060
1081
|
Worker,
|
|
1061
1082
|
Effect.gen(function* () {
|
|
1062
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1063
1083
|
const path = yield* Path.Path;
|
|
1064
1084
|
|
|
1065
1085
|
const { accountId } = yield* CloudflareEnvironment;
|
|
1066
|
-
const virtualEntryPlugin = yield* Bundle.virtualEntryPlugin;
|
|
1067
1086
|
const bundler = yield* WorkerBundle;
|
|
1068
1087
|
const stack = yield* Stack;
|
|
1069
1088
|
|
package/src/Cloudflare/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from "./Container/index.ts";
|
|
|
6
6
|
export * from "./D1/index.ts";
|
|
7
7
|
export * from "./EdgeSession.ts";
|
|
8
8
|
export * from "./Hyperdrive/index.ts";
|
|
9
|
+
export * from "./Images/index.ts";
|
|
9
10
|
export * from "./KV/index.ts";
|
|
10
11
|
export * from "./Providers.ts";
|
|
11
12
|
export * from "./Queue/index.ts";
|
package/src/Test/Vitest.ts
CHANGED
|
@@ -10,8 +10,11 @@ import {
|
|
|
10
10
|
import type { AlchemyContext } from "../AlchemyContext.ts";
|
|
11
11
|
import type { CompiledStack } from "../Stack.ts";
|
|
12
12
|
import type { Stage } from "../Stage.ts";
|
|
13
|
+
import { installLocalhostDns } from "../Util/LocalhostDns.ts";
|
|
13
14
|
import * as Core from "./Core.ts";
|
|
14
15
|
|
|
16
|
+
installLocalhostDns();
|
|
17
|
+
|
|
15
18
|
export type MakeOptions<ROut = any> = Core.MakeOptions<ROut>;
|
|
16
19
|
export type ScratchStack = Core.ScratchStack;
|
|
17
20
|
export type TestEffect<A, R = never> = Core.TestEffect<A, R>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-level DNS shim that makes `*.localhost` resolve to 127.0.0.1.
|
|
3
|
+
*
|
|
4
|
+
* Background: macOS's `getaddrinfo` does not honor RFC 6761 for arbitrary
|
|
5
|
+
* `*.localhost` names; only the bare `localhost` is mapped to loopback.
|
|
6
|
+
* workerd and browsers handle it themselves, but Node's undici-based fetch
|
|
7
|
+
* (and Bun's libcurl-based fetch on some platforms) fails with `ENOTFOUND`
|
|
8
|
+
* when calling URLs returned by the local Cloudflare worker proxy
|
|
9
|
+
* (e.g. `http://my-worker.localhost:1337`).
|
|
10
|
+
*
|
|
11
|
+
* The proxy in `@distilled.cloud/cloudflare-runtime` dispatches by the
|
|
12
|
+
* URL hostname, so we cannot rewrite the URL client-side to `127.0.0.1`
|
|
13
|
+
* (and `Host` is a forbidden fetch header). The fix is to make the host
|
|
14
|
+
* resolvable at the network layer.
|
|
15
|
+
*
|
|
16
|
+
* Calling `installLocalhostDns()` is idempotent.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createRequire } from "node:module";
|
|
20
|
+
|
|
21
|
+
let installed = false;
|
|
22
|
+
|
|
23
|
+
const isLocalhost = (hostname: string): boolean =>
|
|
24
|
+
hostname === "localhost" ||
|
|
25
|
+
hostname === "[::1]" ||
|
|
26
|
+
hostname.endsWith(".localhost");
|
|
27
|
+
|
|
28
|
+
export const installLocalhostDns = (): void => {
|
|
29
|
+
if (installed) return;
|
|
30
|
+
installed = true;
|
|
31
|
+
// Bun resolves `*.localhost` natively via its libcurl-based fetch on
|
|
32
|
+
// Darwin and Linux, so the shim is a no-op there. If a future Bun
|
|
33
|
+
// release changes this, install via Bun's undici-compatible global
|
|
34
|
+
// dispatcher here.
|
|
35
|
+
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") return;
|
|
36
|
+
try {
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const undici: typeof import("undici") = require("undici");
|
|
39
|
+
const dns: typeof import("node:dns") = require("node:dns");
|
|
40
|
+
undici.setGlobalDispatcher(
|
|
41
|
+
new undici.Agent({
|
|
42
|
+
connect: {
|
|
43
|
+
lookup: (hostname, options, cb: any) => {
|
|
44
|
+
if (isLocalhost(hostname)) {
|
|
45
|
+
if (options && (options as { all?: boolean }).all) {
|
|
46
|
+
return cb(null, [{ address: "127.0.0.1", family: 4 }]);
|
|
47
|
+
}
|
|
48
|
+
return cb(null, "127.0.0.1", 4);
|
|
49
|
+
}
|
|
50
|
+
return dns.lookup(hostname, options, cb);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
// undici not available; nothing to do.
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -9,6 +9,7 @@ import type { HttpServer } from "effect/unstable/http/HttpServer";
|
|
|
9
9
|
import type { ServeError } from "effect/unstable/http/HttpServerError";
|
|
10
10
|
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
11
11
|
import type { WebSocketConstructor } from "effect/unstable/socket/Socket";
|
|
12
|
+
import { installLocalhostDns } from "./LocalhostDns.ts";
|
|
12
13
|
|
|
13
14
|
const isBun = typeof Bun !== "undefined";
|
|
14
15
|
|
|
@@ -52,6 +53,7 @@ export const runMain = <E, A>(
|
|
|
52
53
|
readonly teardown?: Teardown | undefined;
|
|
53
54
|
},
|
|
54
55
|
): void => {
|
|
56
|
+
installLocalhostDns();
|
|
55
57
|
if (isBun) {
|
|
56
58
|
void import("@effect/platform-bun/BunRuntime").then((BunRuntime) =>
|
|
57
59
|
BunRuntime.runMain(effect, options),
|