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.
@@ -0,0 +1,147 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Layer from "effect/Layer";
3
+ import * as Option from "effect/Option";
4
+ import * as ServiceMap from "effect/ServiceMap";
5
+
6
+ /**
7
+ * Per-resource in-memory artifacts shared across a single `Plan.make -> apply`
8
+ * execution.
9
+ *
10
+ * The engine scopes this service by resource `FQN` before invoking lifecycle
11
+ * handlers, so providers should treat it as a resource-local bag for expensive,
12
+ * deterministic intermediate results that can be reused across phases.
13
+ *
14
+ * Expected usage:
15
+ *
16
+ * - `diff` computes an expensive artifact once and stores it
17
+ * - `create` / `update` reads the same artifact and skips recomputing it
18
+ * - artifacts are ephemeral and must never be required for correctness on a
19
+ * later deploy because the bag is reset between runs
20
+ *
21
+ * Example:
22
+ *
23
+ * ```ts
24
+ * const artifacts = yield* Artifacts;
25
+ * const cached = yield* artifacts.get<PreparedBundle>("bundle");
26
+ * if (cached) return cached;
27
+ *
28
+ * const bundle = yield* prepareBundle();
29
+ * yield* artifacts.set("bundle", bundle);
30
+ * return bundle;
31
+ * ```
32
+ */
33
+ export class Artifacts extends ServiceMap.Service<
34
+ Artifacts,
35
+ {
36
+ /**
37
+ * Get an artifact by key from the current resource's bag.
38
+ */
39
+ get<T>(key: string): Effect.Effect<T | undefined>;
40
+ /**
41
+ * Store an artifact by key in the current resource's bag.
42
+ */
43
+ set<T>(key: string, value: T): Effect.Effect<void>;
44
+ /**
45
+ * Delete an artifact by key from the current resource's bag.
46
+ */
47
+ delete(key: string): Effect.Effect<void>;
48
+ }
49
+ >()("Artifacts") {}
50
+
51
+ type ArtifactBag = Map<string, unknown>;
52
+
53
+ export class ArtifactStore extends ServiceMap.Service<
54
+ ArtifactStore,
55
+ Map<string, ArtifactBag>
56
+ >()("Artifacts/Store") {}
57
+
58
+ /**
59
+ * Create a fresh root store for one deploy/test run.
60
+ */
61
+ export const createArtifactStore = (): ArtifactStore["Service"] =>
62
+ new Map<string, ArtifactBag>();
63
+
64
+ const getOrCreateBag = (
65
+ store: Map<string, ArtifactBag>,
66
+ fqn: string,
67
+ ): ArtifactBag => {
68
+ const existing = store.get(fqn);
69
+ if (existing) {
70
+ return existing;
71
+ }
72
+ const bag = new Map<string, unknown>();
73
+ store.set(fqn, bag);
74
+ return bag;
75
+ };
76
+
77
+ export const makeScopedArtifacts = (
78
+ store: Map<string, ArtifactBag>,
79
+ fqn: string,
80
+ ): typeof Artifacts.Service => {
81
+ const bag = getOrCreateBag(store, fqn);
82
+ return {
83
+ get: <T>(key: string) => Effect.sync(() => bag.get(key) as T | undefined),
84
+ set: <T>(key: string, value: T) =>
85
+ Effect.sync(() => {
86
+ bag.set(key, value);
87
+ }),
88
+ delete: (key: string) =>
89
+ Effect.sync(() => {
90
+ bag.delete(key);
91
+ }),
92
+ };
93
+ };
94
+
95
+ export const scopedArtifacts = (
96
+ fqn: string,
97
+ ): Layer.Layer<Artifacts, never, ArtifactStore> =>
98
+ Layer.effect(
99
+ Artifacts,
100
+ ArtifactStore.asEffect().pipe(
101
+ Effect.map((store) => makeScopedArtifacts(store, fqn)),
102
+ ),
103
+ );
104
+
105
+ /**
106
+ * Run an effect with a fresh artifact root, replacing any existing store.
107
+ * Use this at top-level entrypoints that intentionally define a new deploy run.
108
+ */
109
+ export const provideFreshArtifactStore = <A, E, R>(
110
+ effect: Effect.Effect<A, E, R | ArtifactStore>,
111
+ ): Effect.Effect<A, E, R> =>
112
+ effect.pipe(
113
+ Effect.provideServiceEffect(
114
+ ArtifactStore,
115
+ Effect.sync(createArtifactStore),
116
+ ),
117
+ );
118
+
119
+ /**
120
+ * Ensure an artifact root exists, reusing the ambient store when one is already
121
+ * present. This lets nested helpers participate in the same run-scoped cache.
122
+ */
123
+ export const ensureArtifactStore = <A, E, R>(
124
+ effect: Effect.Effect<A, E, R | ArtifactStore>,
125
+ ): Effect.Effect<A, E, R> =>
126
+ Effect.serviceOption(ArtifactStore).pipe(
127
+ Effect.map(Option.getOrUndefined),
128
+ Effect.flatMap((existing) =>
129
+ effect.pipe(
130
+ Effect.provideService(ArtifactStore, existing ?? createArtifactStore()),
131
+ ),
132
+ ),
133
+ );
134
+
135
+ export const cached =
136
+ (id: string) =>
137
+ <A, Err = never, Req = never>(eff: Effect.Effect<A, Err, Req>) =>
138
+ Effect.gen(function* () {
139
+ const artifacts = yield* Artifacts;
140
+ const cached = yield* artifacts.get<A>(id);
141
+ if (cached) {
142
+ return cached;
143
+ }
144
+ const result = yield* eff;
145
+ yield* artifacts.set(id, result);
146
+ return result;
147
+ });
@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream";
5
5
  import { ChildProcess } from "effect/unstable/process";
6
6
  import { isResolved } from "../Diff.ts";
7
7
  import { Resource } from "../Resource.ts";
8
- import { sha256, sha256Object } from "../Util/sha256.ts";
8
+ import { hashDirectory, type MemoOptions } from "./Memo.ts";
9
9
 
10
10
  export interface CommandProps {
11
11
  /**
@@ -20,16 +20,13 @@ export interface CommandProps {
20
20
  */
21
21
  cwd?: string;
22
22
  /**
23
- * Glob patterns to match input files for hashing.
24
- * When the hash of matched files changes, the build will re-run.
25
- * @example ["src/*.ts", "src/*.tsx", "package.json"]
23
+ * Controls which files are hashed to decide whether the build should re-run.
24
+ * By default every non-gitignored file in `cwd` is hashed, plus the nearest
25
+ * lockfile. Provide explicit globs to narrow the scope.
26
+ *
27
+ * @see {@link MemoOptions}
26
28
  */
27
- hash: string[];
28
- /**
29
- * Glob patterns to exclude from input hashing.
30
- * Defaults to node_modules and .git directories.
31
- */
32
- exclude?: string[];
29
+ memo?: MemoOptions;
33
30
  /**
34
31
  * The output path (file or directory) produced by the build.
35
32
  * This path is relative to the working directory.
@@ -67,8 +64,7 @@ export interface Command extends Resource<
67
64
  * const build = yield* Build("vite-build", {
68
65
  * command: "npm run build",
69
66
  * cwd: "./frontend",
70
- * include: ["src/*.ts", "src/*.tsx", "index.html", "package.json", "vite.config.ts"],
71
- * output: "dist",
67
+ * outdir: "dist",
72
68
  * });
73
69
  * yield* Console.log(build.path); // absolute path to dist directory
74
70
  * yield* Console.log(build.hash); // hash of input files
@@ -80,7 +76,6 @@ export interface Command extends Resource<
80
76
  * const build = yield* Build("production-build", {
81
77
  * command: "npm run build",
82
78
  * cwd: "./app",
83
- * include: ["src/*", "package.json"],
84
79
  * output: "dist",
85
80
  * env: {
86
81
  * NODE_ENV: "production",
@@ -88,6 +83,16 @@ export interface Command extends Resource<
88
83
  * },
89
84
  * });
90
85
  * ```
86
+ *
87
+ * @section Customizing Memoization
88
+ * @example Customize Memoization
89
+ * ```typescript
90
+ * const build = yield* Build("custom-build", {
91
+ * command: "npm run build",
92
+ * cwd: "./app",
93
+ * output: "dist",
94
+ * memo: { include: ["src/*", "package.json"], exclude: ["node_modules", "dist"] },
95
+ * });
91
96
  */
92
97
  export const Command = Resource<Command>("Build.Command");
93
98
 
@@ -97,26 +102,6 @@ export const CommandProvider = () =>
97
102
  const fs = yield* FileSystem.FileSystem;
98
103
  const pathModule = yield* Path.Path;
99
104
 
100
- const computeInputHash = (props: CommandProps) =>
101
- Effect.gen(function* () {
102
- const cwd = props.cwd ? pathModule.resolve(props.cwd) : process.cwd();
103
- const files = yield* listBuildFiles({
104
- cwd,
105
- include: props.hash,
106
- exclude: props.exclude ?? defaultBuildExclude,
107
- });
108
- const fileHashes = yield* hashBuildFiles({
109
- cwd,
110
- files,
111
- });
112
- const hash = yield* sha256Object({
113
- command: props.command,
114
- env: props.env,
115
- files: fileHashes,
116
- });
117
- return hash;
118
- });
119
-
120
105
  const runBuild = (props: CommandProps) =>
121
106
  Effect.gen(function* () {
122
107
  const cwd = props.cwd ? pathModule.resolve(props.cwd) : process.cwd();
@@ -139,7 +124,7 @@ export const CommandProvider = () =>
139
124
  if (!output) {
140
125
  return undefined;
141
126
  }
142
- const newHash = yield* computeInputHash(news);
127
+ const newHash = yield* hashDirectory(news);
143
128
  if (newHash !== output.hash) {
144
129
  return { action: "update" as const };
145
130
  }
@@ -156,7 +141,7 @@ export const CommandProvider = () =>
156
141
  return output;
157
142
  }),
158
143
  create: Effect.fnUntraced(function* ({ news, session }) {
159
- const hash = yield* computeInputHash(news);
144
+ const hash = yield* hashDirectory(news);
160
145
  const outputPath = getOutputPath(news);
161
146
 
162
147
  yield* session.note(`Running build: ${news.command}`);
@@ -177,7 +162,7 @@ export const CommandProvider = () =>
177
162
  };
178
163
  }),
179
164
  update: Effect.fnUntraced(function* ({ news, session }) {
180
- const hash = yield* computeInputHash(news);
165
+ const hash = yield* hashDirectory(news);
181
166
  const outputPath = getOutputPath(news);
182
167
 
183
168
  yield* session.note(`Rebuilding: ${news.command}`);
@@ -208,69 +193,6 @@ export const CommandProvider = () =>
208
193
  }),
209
194
  );
210
195
 
211
- export const defaultBuildExclude = ["**/node_modules/**", "**/.git/**"];
212
-
213
- export interface BuildFileGlobOptions {
214
- cwd: string;
215
- include: ReadonlyArray<string>;
216
- exclude?: ReadonlyArray<string>;
217
- }
218
-
219
- export const listBuildFiles = Effect.fnUntraced(function* ({
220
- cwd,
221
- include,
222
- exclude = defaultBuildExclude,
223
- }: BuildFileGlobOptions) {
224
- const mod = yield* Effect.promise(() => import("fast-glob"));
225
- const fg = mod.default ?? mod;
226
- const files = yield* Effect.promise(() =>
227
- fg.glob(Array.from(include), {
228
- cwd,
229
- ignore: Array.from(exclude),
230
- onlyFiles: true,
231
- dot: true,
232
- }),
233
- );
234
- files.sort();
235
- return files.map((file) => file.replaceAll("\\", "/"));
236
- });
237
-
238
- export interface HashBuildFilesOptions {
239
- cwd: string;
240
- files: ReadonlyArray<string>;
241
- }
242
-
243
- export const hashBuildFiles = Effect.fnUntraced(function* ({
244
- cwd,
245
- files,
246
- }: HashBuildFilesOptions) {
247
- const fs = yield* FileSystem.FileSystem;
248
- const pathModule = yield* Path.Path;
249
- const parts = yield* Effect.all(
250
- files.map((file) =>
251
- fs.readFile(pathModule.join(cwd, file)).pipe(
252
- Effect.flatMap((content) =>
253
- sha256(content).pipe(Effect.map((hash) => `${file}:${hash}`)),
254
- ),
255
- Effect.catch(() => Effect.succeed(undefined)),
256
- ),
257
- ),
258
- { concurrency: 10 },
259
- );
260
- return parts.filter((part): part is string => part !== undefined);
261
- });
262
-
263
- export const hashBuildDirectory = Effect.fnUntraced(function* (
264
- directory: string,
265
- ) {
266
- const files = yield* listBuildFiles({
267
- cwd: directory,
268
- include: ["**/*"],
269
- exclude: [],
270
- });
271
- return yield* hashBuildFiles({ cwd: directory, files });
272
- });
273
-
274
196
  export interface RunBuildCommandOptions {
275
197
  command: string;
276
198
  cwd?: string;
@@ -0,0 +1,187 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import type { PlatformError } from "effect/PlatformError";
5
+ import fg from "fast-glob";
6
+ import { gitignoreRulesToGlobs } from "../Util/gitignore-rules-to-globs.ts";
7
+ import { sha256, sha256Object } from "../Util/sha256.ts";
8
+
9
+ /**
10
+ * Controls which files are included in the content hash that determines
11
+ * whether a build needs to re-run.
12
+ *
13
+ * By default (no options), every non-gitignored file in the working directory
14
+ * is hashed, plus the nearest package-manager lockfile. Provide explicit
15
+ * `include`/`exclude` globs to narrow the scope when the default is too broad.
16
+ */
17
+ export interface MemoOptions {
18
+ /**
19
+ * Glob patterns of files to hash. Paths are relative to the working directory.
20
+ *
21
+ * @default ["**\/*"] (all files, filtered by `exclude`)
22
+ * @example ["src/**", "package.json", "tsconfig.json"]
23
+ */
24
+ include?: string[];
25
+ /**
26
+ * Glob patterns to exclude from hashing. Paths are relative to the working directory.
27
+ *
28
+ * @default gitignore rules collected from the working directory up to the repo root
29
+ */
30
+ exclude?: string[];
31
+ /**
32
+ * Whether to include the nearest package-manager lockfile (`bun.lock`,
33
+ * `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) in the hash,
34
+ * even when it lives above the working directory (e.g. monorepo root).
35
+ *
36
+ * @default true when both `include` and `exclude` are unset; false otherwise
37
+ */
38
+ lockfile?: boolean;
39
+ }
40
+
41
+ interface ResolvedMemoOptions {
42
+ cwd: string;
43
+ include: string[];
44
+ exclude: string[];
45
+ lockfile: boolean;
46
+ }
47
+
48
+ /**
49
+ * Internal service that resolves memo options, lists matching files, and
50
+ * produces a single SHA-256 content hash. Constructed as an Effect so it
51
+ * can access the platform `FileSystem` and `Path` services.
52
+ */
53
+ const Memo = Effect.gen(function* () {
54
+ const fs = yield* FileSystem.FileSystem;
55
+ const path = yield* Path.Path;
56
+
57
+ const findUp = Effect.fnUntraced(function* (
58
+ cwd: string,
59
+ filenames: string[],
60
+ ): Effect.fn.Return<string | undefined, PlatformError> {
61
+ const [file] = yield* Effect.filter(
62
+ filenames.map((filename) => path.join(cwd, filename)),
63
+ fs.exists,
64
+ { concurrency: "unbounded" },
65
+ );
66
+ if (file) {
67
+ return file;
68
+ }
69
+ const parent = path.dirname(cwd);
70
+ if (parent === cwd) {
71
+ return undefined;
72
+ }
73
+ return yield* findUp(parent, filenames);
74
+ });
75
+
76
+ const readGitIgnoreRules = Effect.fnUntraced(function* (
77
+ cwd: string,
78
+ ): Effect.fn.Return<string[], PlatformError> {
79
+ const rules = yield* fs.readFileString(path.join(cwd, ".gitignore")).pipe(
80
+ Effect.map((file) => file.split("\n")),
81
+ Effect.catchIf(
82
+ (error) =>
83
+ error._tag === "PlatformError" && error.reason._tag === "NotFound",
84
+ () => Effect.succeed([]),
85
+ ),
86
+ );
87
+ const parent = path.dirname(cwd);
88
+ if (parent === cwd || (yield* fs.exists(path.join(cwd, ".git")))) {
89
+ return rules;
90
+ }
91
+ return [...(yield* readGitIgnoreRules(parent)), ...rules];
92
+ });
93
+
94
+ const resolveMemoOptions = Effect.fnUntraced(function* (
95
+ cwd: string | undefined,
96
+ options: MemoOptions,
97
+ ): Effect.fn.Return<ResolvedMemoOptions, PlatformError> {
98
+ const resolvedCwd = cwd ? path.resolve(cwd) : process.cwd();
99
+ return {
100
+ cwd: resolvedCwd,
101
+ include: options.include ?? ["**/*"],
102
+ exclude:
103
+ options.exclude ??
104
+ (yield* readGitIgnoreRules(resolvedCwd).pipe(
105
+ Effect.map(gitignoreRulesToGlobs),
106
+ Effect.map((globs) => ["**/.git/**", ...globs]),
107
+ )),
108
+ lockfile: options.lockfile ?? !(options.exclude || options.include),
109
+ };
110
+ });
111
+
112
+ const listFiles = Effect.fnUntraced(function* (
113
+ options: ResolvedMemoOptions,
114
+ ): Effect.fn.Return<string[], PlatformError> {
115
+ const [files, lockfile] = yield* Effect.all(
116
+ [
117
+ Effect.promise(() =>
118
+ fg.glob(options.include, {
119
+ cwd: options.cwd,
120
+ ignore: options.exclude,
121
+ onlyFiles: true,
122
+ dot: true,
123
+ }),
124
+ ),
125
+ options.lockfile
126
+ ? findUp(options.cwd, [
127
+ "bun.lock",
128
+ "bun.lockb",
129
+ "package-lock.json",
130
+ "pnpm-lock.yaml",
131
+ "yarn.lock",
132
+ ]).pipe(
133
+ Effect.map((lockfile) =>
134
+ lockfile ? path.relative(options.cwd, lockfile) : undefined,
135
+ ),
136
+ )
137
+ : Effect.succeed(undefined),
138
+ ],
139
+ { concurrency: "unbounded" },
140
+ );
141
+ if (lockfile && !files.includes(lockfile)) {
142
+ files.push(lockfile);
143
+ }
144
+ return files.sort();
145
+ });
146
+
147
+ const hashFiles = Effect.fnUntraced(function* (
148
+ cwd: string,
149
+ files: string[],
150
+ ): Effect.fn.Return<string, PlatformError> {
151
+ const hashes = yield* Effect.forEach(
152
+ files,
153
+ (file) =>
154
+ fs.readFile(path.join(cwd, file)).pipe(
155
+ Effect.flatMap(sha256),
156
+ Effect.map((hash) => `${file}:${hash}`),
157
+ ),
158
+ { concurrency: "unbounded" },
159
+ );
160
+ return yield* sha256Object(hashes);
161
+ });
162
+
163
+ return {
164
+ resolveMemoOptions,
165
+ listFiles,
166
+ hashFiles,
167
+ };
168
+ });
169
+
170
+ /**
171
+ * Produces a deterministic SHA-256 hash of all files matched by the given
172
+ * memo options. The hash changes if and only if the content of the matched
173
+ * files changes, making it suitable for cache-busting build outputs.
174
+ */
175
+ export const hashDirectory = Effect.fn(function* (props: {
176
+ cwd?: string;
177
+ memo?: MemoOptions;
178
+ }): Effect.fn.Return<string, PlatformError, FileSystem.FileSystem | Path.Path> {
179
+ const service = yield* Memo;
180
+ const resolvedOptions = yield* service.resolveMemoOptions(
181
+ props.cwd,
182
+ props.memo ?? {},
183
+ );
184
+ const files = yield* service.listFiles(resolvedOptions);
185
+ const hash = yield* service.hashFiles(resolvedOptions.cwd, files);
186
+ return hash;
187
+ });
@@ -110,24 +110,9 @@ export const watch = (
110
110
  if (result._tag === "Failure") {
111
111
  return Result.fail(result.failure);
112
112
  }
113
- const files = Object.values(result.success);
114
- // These are sanity checks - with rolldown, the first file is always an entry chunk.
115
- if (!files[0] || files[0].type !== "chunk" || !files[0].isEntry) {
116
- return Result.fail(
117
- new BundleError({
118
- message: "Invalid bundle output",
119
- }),
120
- );
121
- }
122
- return yield* Effect.forEach(
123
- files as [
124
- rolldown.OutputChunk,
125
- ...(rolldown.OutputChunk | rolldown.OutputAsset)[],
126
- ],
127
- bundleFileFromOutputChunk,
128
- ).pipe(
129
- Effect.flatMap(bundleOutputFromFiles),
113
+ return yield* bundleOutputFromRolldownOutputBundle(result.success).pipe(
130
114
  Effect.map(Result.succeed),
115
+ Effect.catch((error) => Effect.succeed(Result.fail(error))),
131
116
  );
132
117
  }),
133
118
  ),
@@ -173,6 +158,27 @@ export const virtualEntryPlugin = Effect.gen(function* () {
173
158
  };
174
159
  });
175
160
 
161
+ export function bundleOutputFromRolldownOutputBundle(
162
+ bundle: rolldown.OutputBundle,
163
+ ): Effect.Effect<BundleOutput, BundleError> {
164
+ const files = Object.values(bundle);
165
+ // These are sanity checks - with rolldown, the first file is always an entry chunk.
166
+ if (!files[0] || files[0].type !== "chunk" || !files[0].isEntry) {
167
+ return Effect.fail(
168
+ new BundleError({
169
+ message: "Invalid bundle output",
170
+ }),
171
+ );
172
+ }
173
+ return Effect.forEach(
174
+ files as [
175
+ rolldown.OutputChunk,
176
+ ...(rolldown.OutputChunk | rolldown.OutputAsset)[],
177
+ ],
178
+ bundleFileFromOutputChunk,
179
+ ).pipe(Effect.flatMap(bundleOutputFromFiles));
180
+ }
181
+
176
182
  function bundleErrorFromUnknown(error: unknown): BundleError {
177
183
  const message = error instanceof Error ? error.message : String(error);
178
184
  return new BundleError({
@@ -0,0 +1,65 @@
1
+ import type { MemoOptions } from "../../Build/Memo.ts";
2
+ import { Worker, type WorkerProps } from "../Workers/Worker.ts";
3
+
4
+ export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
5
+ /**
6
+ * Root directory passed to Vite's `root` option.
7
+ * Defaults to the current working directory (`process.cwd()`).
8
+ */
9
+ rootDir?: string;
10
+ /**
11
+ * Controls which files are hashed to decide whether a rebuild is needed.
12
+ * By default every non-gitignored file in `cwd` is hashed, plus the nearest
13
+ * lockfile. Provide explicit globs to narrow the scope.
14
+ *
15
+ * @see {@link MemoOptions}
16
+ */
17
+ memo?: MemoOptions;
18
+ }
19
+
20
+ /**
21
+ * A Cloudflare Worker deployed from a Vite project.
22
+ *
23
+ * `Vite` uses the Cloudflare Vite plugin to build both the server bundle and
24
+ * client assets in a single `vite build` invocation — no manual `main`
25
+ * entrypoint, build command, output directory, or Wrangler configuration
26
+ * required.
27
+ *
28
+ * Input files are content-hashed (respecting `.gitignore` by default) so
29
+ * unchanged projects skip the build and deploy entirely.
30
+ *
31
+ * @section Deploying a Static Site
32
+ * @example Basic Static Site
33
+ * ```typescript
34
+ * const site = yield* Cloudflare.Vite("Website");
35
+ * ```
36
+ *
37
+ * @section Deploying a TanStack Start App
38
+ * @example TanStack Start with SSR
39
+ * ```typescript
40
+ * const app = yield* Cloudflare.Vite("TanStackStart", {
41
+ * compatibility: {
42
+ * flags: ["nodejs_compat"],
43
+ * },
44
+ * });
45
+ * ```
46
+ *
47
+ * @section Custom Rebuild Scope
48
+ * @example Narrow the Memo Scope
49
+ * ```typescript
50
+ * const site = yield* Cloudflare.Vite("Docs", {
51
+ * memo: {
52
+ * include: ["src/**", "content/**", "package.json"],
53
+ * },
54
+ * });
55
+ * ```
56
+ */
57
+ export const Vite = (id: string, props: ViteProps = {}) =>
58
+ Worker(id, {
59
+ ...props,
60
+ main: undefined!,
61
+ vite: {
62
+ rootDir: props.rootDir,
63
+ memo: props.memo,
64
+ },
65
+ });
@@ -1,2 +1,2 @@
1
1
  export * from "./StaticSite.ts";
2
- export * from "./TanstackStart.ts";
2
+ export * from "./Vite.ts";
@@ -7,7 +7,7 @@ import * as Path from "effect/Path";
7
7
  import type { PlatformError } from "effect/PlatformError";
8
8
  import * as ServiceMap from "effect/ServiceMap";
9
9
  import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
10
- import { sha256 } from "../../Util/index.ts";
10
+ import { sha256, sha256Object } from "../../Util/index.ts";
11
11
 
12
12
  const MAX_ASSET_SIZE = 1024 * 1024 * 25; // 25MB
13
13
  const MAX_ASSET_COUNT = 20_000;
@@ -23,6 +23,7 @@ export interface AssetReadResult {
23
23
  manifest: Record<string, { hash: string; size: number }>;
24
24
  _headers: string | undefined;
25
25
  _redirects: string | undefined;
26
+ hash: string;
26
27
  }
27
28
 
28
29
  export interface AssetsProps {
@@ -190,7 +191,7 @@ export const AssetsProvider = () =>
190
191
  });
191
192
  }),
192
193
  );
193
- return {
194
+ const result = {
194
195
  directory: props.directory,
195
196
  config: props.config,
196
197
  manifest: Object.fromEntries(
@@ -201,6 +202,10 @@ export const AssetsProvider = () =>
201
202
  _headers,
202
203
  _redirects,
203
204
  };
205
+ return {
206
+ ...result,
207
+ hash: yield* sha256Object(result),
208
+ };
204
209
  }),
205
210
  upload: Effect.fnUntraced(function* (
206
211
  accountId: string,