alchemy-effect 0.6.3 → 0.7.0

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.
@@ -1,5 +1,6 @@
1
1
  import type * as cf from "@cloudflare/workers-types";
2
- import cloudflare from "@distilled.cloud/cloudflare-rolldown-plugin";
2
+ import cloudflareRolldown from "@distilled.cloud/cloudflare-rolldown-plugin";
3
+ import cloudflareVite from "@distilled.cloud/cloudflare-vite-plugin";
3
4
  import * as workers from "@distilled.cloud/cloudflare/workers";
4
5
  import type * as Cause from "effect/Cause";
5
6
  import * as Effect from "effect/Effect";
@@ -8,12 +9,16 @@ import * as Layer from "effect/Layer";
8
9
  import * as Option from "effect/Option";
9
10
  import * as Path from "effect/Path";
10
11
  import * as Queue from "effect/Queue";
12
+ import * as Redacted from "effect/Redacted";
11
13
  import * as Schedule from "effect/Schedule";
12
14
  import * as ServiceMap from "effect/ServiceMap";
13
15
  import * as Stream from "effect/Stream";
14
16
  import * as Socket from "effect/unstable/socket/Socket";
15
17
  import type * as rolldown from "rolldown";
18
+ import type * as vite from "vite";
19
+ import * as Artifacts from "../../Artifacts.ts";
16
20
  import * as Binding from "../../Binding.ts";
21
+ import { hashDirectory, type MemoOptions } from "../../Build/Memo.ts";
17
22
  import * as Bundle from "../../Bundle/Bundle.ts";
18
23
  import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
19
24
  import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
@@ -33,7 +38,6 @@ import { Resource, type ResourceBinding } from "../../Resource.ts";
33
38
  import { Self } from "../../Self.ts";
34
39
  import * as Serverless from "../../Serverless/index.ts";
35
40
  import { Stack } from "../../Stack.ts";
36
- import { sha256 } from "../../Util/index.ts";
37
41
  import { Account } from "../Account.ts";
38
42
  import { CloudflareLogs } from "../Logs.ts";
39
43
  import type { AssetsConfig, AssetsProps } from "./Assets.ts";
@@ -42,6 +46,7 @@ import cloudflare_workers from "./cloudflare_workers.ts";
42
46
  import { isDurableObjectExport } from "./DurableObject.ts";
43
47
  import { fromCloudflareFetcher } from "./Fetcher.ts";
44
48
  import { workersHttpHandler } from "./HttpServer.ts";
49
+ import { Request } from "./Request.ts";
45
50
  import { makeRpcStub } from "./Rpc.ts";
46
51
  import { isWorkflowExport } from "./Workflow.ts";
47
52
 
@@ -163,6 +168,11 @@ export interface WorkerProps extends PlatformProps {
163
168
  enabled?: boolean;
164
169
  previewsEnabled?: boolean;
165
170
  };
171
+ /** @internal used by Cloudflare.Vite resource */
172
+ vite?: {
173
+ rootDir?: string;
174
+ memo?: MemoOptions;
175
+ };
166
176
  logpush?: boolean;
167
177
  observability?: WorkerObservability;
168
178
  tags?: string[];
@@ -181,7 +191,7 @@ export interface WorkerExecutionContext extends Serverless.FunctionContext {
181
191
  export(name: string, value: any): Effect.Effect<void>;
182
192
  }
183
193
 
184
- export type WorkerServices = Worker | WorkerEnvironment;
194
+ export type WorkerServices = Worker | WorkerEnvironment | Request;
185
195
 
186
196
  export type WorkerShape = Main<WorkerServices>;
187
197
 
@@ -197,7 +207,8 @@ export interface Worker extends Resource<
197
207
  accountId: string;
198
208
  hash?: {
199
209
  assets: string | undefined;
200
- bundle: string;
210
+ bundle: string | undefined;
211
+ input: string | undefined;
201
212
  };
202
213
  },
203
214
  {
@@ -249,11 +260,31 @@ export const Worker: Platform<
249
260
  ? Effect.succeed(value)
250
261
  : Effect.die(`Environment variable '${key}' not found`),
251
262
  ),
263
+ Effect.map((json) => {
264
+ try {
265
+ const value = JSON.parse(json);
266
+ if (!Redacted.isRedacted(value)) {
267
+ return Redacted.make(value.value);
268
+ }
269
+ return value;
270
+ } catch {
271
+ return json;
272
+ }
273
+ }),
252
274
  ) as any,
253
275
  set: (id: string, output: Output.Output) =>
254
276
  Effect.sync(() => {
255
277
  const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
256
- env[key] = output.pipe(Output.map((value) => JSON.stringify(value)));
278
+ env[key] = output.pipe(
279
+ Output.map((value) =>
280
+ Redacted.isRedacted(value)
281
+ ? JSON.stringify({
282
+ _tag: "Redacted",
283
+ value: Redacted.value(value),
284
+ })
285
+ : JSON.stringify(value),
286
+ ),
287
+ );
257
288
  return key;
258
289
  }),
259
290
  serve: <Req = never>(handler: HttpEffect<Req>) =>
@@ -389,6 +420,10 @@ export const WorkerProvider = () =>
389
420
  const listScripts = yield* workers.listScripts;
390
421
  const putScript = yield* workers.putScript;
391
422
  const telemetry = yield* CloudflareLogs;
423
+ const defaultCompatibilityDate = yield* Effect.promise(() =>
424
+ // @ts-expect-error no types for workerd
425
+ import("workerd").then((m) => m.compatibilityDate as string),
426
+ );
392
427
 
393
428
  const getAccountSubdomain = (accountId: string) =>
394
429
  getSubdomain({
@@ -436,44 +471,34 @@ export const WorkerProvider = () =>
436
471
  "path" in assets &&
437
472
  "hash" in assets
438
473
  ) {
439
- const path = assets.path as string;
440
- const hash = assets.hash as string;
441
474
  const result = yield* read({
442
- directory: path,
475
+ directory: assets.path as string,
443
476
  config: assets.config,
444
477
  });
445
478
  return {
446
479
  ...result,
447
- hash,
480
+ hash: assets.hash as string,
448
481
  };
449
482
  }
450
483
 
451
484
  // Handle string path or AssetsProps
452
- const result = yield* read(
485
+ return yield* read(
453
486
  typeof assets === "string" ? { directory: assets } : assets,
454
487
  );
455
- return {
456
- ...result,
457
- hash: yield* sha256(JSON.stringify(result)),
458
- };
459
488
  });
460
489
 
461
- const prepareBundle = Effect.fnUntraced(function* (
462
- id: string,
463
- props: WorkerProps,
464
- ) {
465
- const realMain = yield* fs.realPath(props.main);
466
- const buildBundle = Effect.fnUntraced(function* (
467
- entry: string,
468
- plugins?: rolldown.RolldownPluginOption,
469
- ) {
470
- const { files, hash } = yield* Bundle.build(
490
+ const prepareBundle = Effect.fnUntraced(function* (props: WorkerProps) {
491
+ const main = yield* fs.realPath(props.main);
492
+ const cwd = yield* findCwdForBundle(main);
493
+ const buildBundle = (plugins?: rolldown.RolldownPluginOption) =>
494
+ Bundle.build(
471
495
  {
472
- input: entry,
473
- cwd: yield* findCwdForBundle(entry),
496
+ input: main,
497
+ cwd,
474
498
  plugins: [
475
- cloudflare({
476
- compatibilityDate: props.compatibility?.date ?? "2026-03-10",
499
+ cloudflareRolldown({
500
+ compatibilityDate:
501
+ props.compatibility?.date ?? defaultCompatibilityDate,
477
502
  compatibilityFlags: props.compatibility?.flags,
478
503
  }),
479
504
  plugins,
@@ -490,20 +515,10 @@ export const WorkerProvider = () =>
490
515
  keepNames: true,
491
516
  },
492
517
  );
493
- return {
494
- files: files.map(
495
- (file) =>
496
- new File([file.content as BlobPart], file.path, {
497
- type: contentTypeFromExtension(path.extname(file.path)),
498
- }),
499
- ),
500
- mainModule: files[0].path,
501
- hash,
502
- };
503
- });
504
518
 
505
519
  if (props.isExternal) {
506
- return yield* buildBundle(realMain);
520
+ const bundle = yield* buildBundle();
521
+ return bundle;
507
522
  }
508
523
 
509
524
  const exportMap = (props.exports ?? {}) as Record<string, unknown>;
@@ -574,7 +589,10 @@ const exportsEffect = tag.asEffect().pipe(
574
589
  Layer.provideMerge(
575
590
  Layer.succeed(
576
591
  ConfigProvider.ConfigProvider,
577
- ConfigProvider.fromUnknown(env),
592
+ ConfigProvider.orElse(
593
+ ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
594
+ ConfigProvider.fromUnknown(env),
595
+ ),
578
596
  )
579
597
  ),
580
598
  Layer.provideMerge(
@@ -631,9 +649,111 @@ ${[
631
649
  ].join("\n")}
632
650
  `;
633
651
 
634
- return yield* buildBundle(realMain, virtualEntryPlugin(script));
652
+ return yield* buildBundle(virtualEntryPlugin(script));
635
653
  });
636
654
 
655
+ const viteBuild = Effect.fnUntraced(function* (props: WorkerProps) {
656
+ const vite = yield* Effect.promise(() => import("vite"));
657
+ let assetsDirectory: string | undefined;
658
+ let serverBundle: vite.Rolldown.OutputBundle | undefined;
659
+
660
+ yield* Effect.promise(async () => {
661
+ const builder = await vite.createBuilder(
662
+ {
663
+ root: props.vite?.rootDir,
664
+ plugins: [
665
+ cloudflareVite({
666
+ compatibilityDate:
667
+ props.compatibility?.date ?? defaultCompatibilityDate,
668
+ compatibilityFlags: props.compatibility?.flags,
669
+ }),
670
+ {
671
+ name: "output:ssr",
672
+ applyToEnvironment(environment) {
673
+ return environment.name === "ssr";
674
+ },
675
+ generateBundle(_outputOptions, bundle) {
676
+ serverBundle = bundle;
677
+ },
678
+ },
679
+ {
680
+ name: "output:client",
681
+ applyToEnvironment(environment) {
682
+ return environment.name === "client";
683
+ },
684
+ generateBundle(outputOptions) {
685
+ assetsDirectory = outputOptions.dir;
686
+ },
687
+ },
688
+ ],
689
+ },
690
+ // This is the `useLegacyBuilder` option. The Vite CLI implementation uses `null` here.
691
+ // Originally we used `undefined` here, but this caused the static site build to fail.
692
+ // https://github.com/vitejs/vite/blob/a07a4bd052ac75f916391c999c408ad5f2867e61/packages/vite/src/node/cli.ts#L367
693
+ null,
694
+ );
695
+ await builder.buildApp();
696
+ });
697
+ if (!assetsDirectory && !serverBundle) {
698
+ return yield* Effect.die(
699
+ new Error("Vite build produced neither server nor client output"),
700
+ );
701
+ }
702
+ const [assets, bundle] = yield* Effect.all(
703
+ [
704
+ assetsDirectory
705
+ ? read({
706
+ directory: assetsDirectory,
707
+ config:
708
+ typeof props.assets === "object" && "config" in props.assets
709
+ ? props.assets.config
710
+ : undefined,
711
+ })
712
+ : Effect.succeed(undefined),
713
+ serverBundle
714
+ ? Bundle.bundleOutputFromRolldownOutputBundle(serverBundle)
715
+ : Effect.succeed(undefined),
716
+ ],
717
+ { concurrency: "unbounded" },
718
+ );
719
+ return { assets, bundle };
720
+ });
721
+
722
+ const prepareAssetsAndBundle = (props: WorkerProps) =>
723
+ Effect.gen(function* () {
724
+ if (props.vite) {
725
+ const [{ assets, bundle }, input] = yield* Effect.all(
726
+ [viteBuild(props), hashDirectory(props.vite)],
727
+ { concurrency: "unbounded" },
728
+ );
729
+ return { assets, bundle, input };
730
+ }
731
+ const [assets, bundle] = yield* Effect.all(
732
+ [prepareAssets(props.assets), prepareBundle(props)],
733
+ { concurrency: "unbounded" },
734
+ );
735
+ return { assets, bundle };
736
+ }).pipe(
737
+ Effect.map(({ assets, bundle, input }) => ({
738
+ assets,
739
+ bundle: {
740
+ main: bundle?.files[0].path,
741
+ files: bundle?.files.map(
742
+ (file) =>
743
+ new File([file.content as BlobPart], file.path, {
744
+ type: contentTypeFromExtension(path.extname(file.path)),
745
+ }),
746
+ ),
747
+ },
748
+ hash: {
749
+ assets: assets?.hash,
750
+ bundle: bundle?.hash,
751
+ input,
752
+ } satisfies Worker["Attributes"]["hash"],
753
+ })),
754
+ Artifacts.cached("build"),
755
+ );
756
+
637
757
  const putWorker = Effect.fnUntraced(function* (
638
758
  id: string,
639
759
  news: WorkerProps,
@@ -647,10 +767,7 @@ ${[
647
767
  yield* Effect.logInfo(
648
768
  `Cloudflare Worker ${olds ? "update" : "create"}: preparing bundle for ${name}`,
649
769
  );
650
- const [assets, bundle] = yield* Effect.all([
651
- prepareAssets(news.assets),
652
- prepareBundle(id, news),
653
- ]);
770
+ const { assets, bundle, hash } = yield* prepareAssetsAndBundle(news);
654
771
  const metadataBindings = bindings.flatMap((b) => b.data.bindings);
655
772
  let metadataAssets:
656
773
  | workers.PutScriptRequest["metadata"]["assets"]
@@ -867,7 +984,7 @@ ${[
867
984
  keepBindings: undefined,
868
985
  limits: news.limits,
869
986
  logpush: news.logpush,
870
- mainModule: bundle.mainModule,
987
+ mainModule: bundle.main,
871
988
  migrations,
872
989
  observability: news.observability ?? {
873
990
  enabled: true,
@@ -936,13 +1053,33 @@ ${[
936
1053
  : undefined,
937
1054
  tags: metadata.tags,
938
1055
  accountId,
939
- hash: {
940
- assets: assets?.hash,
941
- bundle: bundle.hash,
942
- },
1056
+ hash,
943
1057
  } satisfies Worker["Attributes"];
944
1058
  });
945
1059
 
1060
+ const hasChanged = Effect.fnUntraced(function* (
1061
+ props: WorkerProps,
1062
+ output: Worker["Attributes"],
1063
+ ) {
1064
+ if (props.vite) {
1065
+ const input = yield* hashDirectory(props.vite);
1066
+ return input !== output.hash?.input;
1067
+ }
1068
+ const [assetsHash, bundleHash] = yield* Effect.all(
1069
+ [
1070
+ "assets" in output && output.hash?.assets
1071
+ ? Effect.succeed(output.hash.assets)
1072
+ : prepareAssets(props.assets).pipe(Effect.map((a) => a?.hash)),
1073
+ prepareBundle(props).pipe(Effect.map((b) => b.hash)),
1074
+ ],
1075
+ { concurrency: "unbounded" },
1076
+ );
1077
+ return (
1078
+ assetsHash !== output.hash?.assets ||
1079
+ bundleHash !== output.hash?.bundle
1080
+ );
1081
+ });
1082
+
946
1083
  return Worker.provider.of({
947
1084
  stables: ["workerId", "workerName"],
948
1085
  diff: Effect.fnUntraced(function* ({ id, news, olds, output }) {
@@ -960,14 +1097,7 @@ ${[
960
1097
  if (!output) {
961
1098
  return;
962
1099
  }
963
- const [assets, bundle] = yield* Effect.all([
964
- prepareAssets(news.assets),
965
- prepareBundle(id, news),
966
- ]);
967
- if (
968
- assets?.hash !== output.hash?.assets ||
969
- bundle.hash !== output.hash?.bundle
970
- ) {
1100
+ if (yield* hasChanged(news, output)) {
971
1101
  return {
972
1102
  action: "update",
973
1103
  stables:
package/src/Destroy.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import { apply } from "./Apply.ts";
3
+ import { provideFreshArtifactStore } from "./Artifacts.ts";
3
4
  import * as Plan from "./Plan.ts";
4
5
  import { Stack } from "./Stack.ts";
5
6
 
@@ -11,5 +12,5 @@ export const destroy = () =>
11
12
  resources: {},
12
13
  bindings: {},
13
14
  output: {},
14
- }),
15
- ).pipe(Effect.flatMap(apply));
15
+ }).pipe(Effect.flatMap(apply), provideFreshArtifactStore),
16
+ );
package/src/Output.ts CHANGED
@@ -53,6 +53,7 @@ export interface Output<A = any, Req = any> extends Pipeable {
53
53
  >;
54
54
  bind(id: string): Effect.Effect<Effect.Effect<A>, never, ExecutionContext>;
55
55
  asEffect(): Effect.Effect<Accessor<A>, never, Req>;
56
+ as<T>(): Output<T, Req>;
56
57
  }
57
58
 
58
59
  export interface Accessor<A> extends Effect.Effect<A> {}
@@ -91,6 +92,9 @@ export abstract class BaseExpr<A = any, Req = any> implements Output<A, Req> {
91
92
  declare readonly req: Req;
92
93
  // we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy-effect module imports
93
94
  constructor() {}
95
+ as<T>(): Output<T, Req> {
96
+ return this as any;
97
+ }
94
98
 
95
99
  [Symbol.iterator](): Iterator<
96
100
  Yieldable<any, void, never, Req>,