alchemy 2.0.0-beta.2 → 2.0.0-beta.4

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 (77) hide show
  1. package/bin/{alchemy-effect.js → alchemy.js} +171 -171
  2. package/bin/alchemy.js.map +1 -0
  3. package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
  4. package/lib/cli/index.d.ts.map +1 -1
  5. package/lib/cli/index.js +167 -167
  6. package/lib/cli/index.js.map +1 -1
  7. package/package.json +54 -45
  8. package/src/AWS/AGENTS.md +10 -10
  9. package/src/AWS/Assets.ts +1 -1
  10. package/src/AWS/AuthProvider.ts +392 -0
  11. package/src/AWS/Credentials.ts +0 -1
  12. package/src/AWS/DynamoDB/Table.ts +63 -10
  13. package/src/AWS/EC2/VpcEndpoint.ts +4 -4
  14. package/src/AWS/EC2/hosted.ts +1 -1
  15. package/src/AWS/ECS/Task.ts +1 -1
  16. package/src/AWS/Kinesis/Stream.ts +44 -8
  17. package/src/AWS/Lambda/Function.ts +218 -10
  18. package/src/AWS/S3/Bucket.ts +26 -19
  19. package/src/AWS/S3/BucketNotifications.ts +1 -1
  20. package/src/AWS/SNS/Topic.ts +47 -10
  21. package/src/AWS/SQS/Queue.ts +66 -0
  22. package/src/Auth/AuthProvider.ts +33 -0
  23. package/src/Auth/Credentials.ts +56 -0
  24. package/src/Auth/Env.ts +45 -0
  25. package/src/Auth/Profile.ts +72 -0
  26. package/src/Auth/index.ts +2 -0
  27. package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
  28. package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
  29. package/src/Cloudflare/Container/Container.ts +122 -0
  30. package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
  31. package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
  32. package/src/Cloudflare/Container/StartContainer.ts +1 -1
  33. package/src/Cloudflare/D1/D1Database.ts +23 -4
  34. package/src/Cloudflare/KV/KVNamespace.ts +25 -0
  35. package/src/Cloudflare/Providers.ts +1 -6
  36. package/src/Cloudflare/R2/R2Bucket.ts +45 -2
  37. package/src/Cloudflare/Website/StaticSite.ts +101 -15
  38. package/src/Cloudflare/Website/Vite.ts +86 -20
  39. package/src/Cloudflare/Workers/Assets.ts +170 -207
  40. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
  41. package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
  42. package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
  43. package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
  44. package/src/Cloudflare/Workers/InferEnv.ts +11 -7
  45. package/src/Cloudflare/Workers/Rpc.ts +13 -2
  46. package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
  47. package/src/Cloudflare/Workers/WebSocket.ts +1 -1
  48. package/src/Cloudflare/Workers/Worker.ts +572 -67
  49. package/src/Cloudflare/Workers/Workflow.ts +53 -11
  50. package/src/Cloudflare/Workers/index.ts +4 -1
  51. package/src/Construct.ts +2 -2
  52. package/src/GitHub/Comment.ts +224 -0
  53. package/src/GitHub/Secret.ts +257 -0
  54. package/src/GitHub/Variable.ts +166 -0
  55. package/src/GitHub/index.ts +3 -0
  56. package/src/Kubernetes/client.ts +2 -2
  57. package/src/Output.ts +1 -1
  58. package/src/Platform.ts +10 -5
  59. package/src/Provider.ts +1 -1
  60. package/src/Resource.ts +4 -4
  61. package/src/Test/Vitest.ts +4 -3
  62. package/src/Util/Clank.ts +77 -0
  63. package/src/Util/PlatformServices.ts +21 -0
  64. package/src/Util/dedent.ts +59 -0
  65. package/src/Util/index.ts +1 -0
  66. package/bin/alchemy-effect.js.map +0 -1
  67. package/src/Cloudflare/Workers/DurableObject.ts +0 -527
  68. package/src/Daemon/Client.ts +0 -116
  69. package/src/Daemon/Config.ts +0 -28
  70. package/src/Daemon/Errors.ts +0 -48
  71. package/src/Daemon/Lock.ts +0 -162
  72. package/src/Daemon/ProcessRegistry.ts +0 -284
  73. package/src/Daemon/RpcSchema.ts +0 -43
  74. package/src/Daemon/RpcServer.ts +0 -231
  75. package/src/Daemon/index.ts +0 -27
  76. package/src/Spawn.ts +0 -23
  77. /package/bin/{alchemy-effect.ts → alchemy.ts} +0 -0
@@ -3,10 +3,17 @@ import { Command, type CommandProps } from "../../Build/Command.ts";
3
3
  import type { InputProps } from "../../Input.ts";
4
4
  import * as Namespace from "../../Namespace.ts";
5
5
  import type { AssetsConfig } from "../Workers/Assets.ts";
6
- import { Worker, type WorkerProps } from "../Workers/Worker.ts";
6
+ import {
7
+ Worker,
8
+ type WorkerAssetsConfig,
9
+ type WorkerBindingProps,
10
+ type WorkerProps,
11
+ } from "../Workers/Worker.ts";
7
12
 
8
- export interface StaticSiteProps
9
- extends Omit<WorkerProps, "assets">, Omit<CommandProps, "env"> {
13
+ export interface StaticSiteProps<Bindings extends WorkerBindingProps = {}>
14
+ extends
15
+ Omit<WorkerProps<Bindings, WorkerAssetsConfig>, "assets">,
16
+ Omit<CommandProps, "env"> {
10
17
  /**
11
18
  * Optional configuration for static asset routing behavior.
12
19
  * Supports `runWorkerFirst`, `htmlHandling`, `notFoundHandling`, etc.
@@ -19,19 +26,98 @@ export interface StaticSiteProps
19
26
 
20
27
  export type StaticSite = ReturnType<typeof StaticSite>;
21
28
 
22
- export const StaticSite = (id: string, props: InputProps<StaticSiteProps>) =>
29
+ /**
30
+ * A Cloudflare Worker that serves static assets built by a shell command.
31
+ *
32
+ * `StaticSite` runs a build command (e.g. `npm run build`), content-hashes
33
+ * the output directory, and deploys the result as a Cloudflare Worker with
34
+ * static assets. Use this when your site has its own build step that
35
+ * produces a directory of files — Hugo, Zola, Eleventy, or any custom
36
+ * pipeline.
37
+ *
38
+ * For Vite-based projects, prefer `Cloudflare.Vite` which handles
39
+ * building automatically.
40
+ *
41
+ * @resource
42
+ *
43
+ * @section Basic Usage
44
+ * Point `command` at your build script and `outdir` at where it writes
45
+ * output. Alchemy runs the command, hashes the output, and deploys it.
46
+ *
47
+ * @example Deploying a Hugo site
48
+ * ```typescript
49
+ * const site = yield* Cloudflare.StaticSite("Blog", {
50
+ * command: "hugo --minify",
51
+ * outdir: "public",
52
+ * });
53
+ * ```
54
+ *
55
+ * @section Asset Configuration
56
+ * Use `assetsConfig` to control how Cloudflare handles routing for
57
+ * your static files — HTML handling, not-found behavior, etc.
58
+ *
59
+ * @example SPA-style routing
60
+ * ```typescript
61
+ * const site = yield* Cloudflare.StaticSite("App", {
62
+ * command: "npm run build",
63
+ * outdir: "dist",
64
+ * assetsConfig: {
65
+ * htmlHandling: "auto-trailing-slash",
66
+ * notFoundHandling: "single-page-application",
67
+ * },
68
+ * });
69
+ * ```
70
+ *
71
+ * @section Custom Rebuild Scope
72
+ * By default, all non-gitignored files are hashed to decide whether
73
+ * the build should re-run. Use `memo` to narrow the scope.
74
+ *
75
+ * @example Narrowing the memo scope
76
+ * ```typescript
77
+ * const site = yield* Cloudflare.StaticSite("Docs", {
78
+ * command: "npm run build",
79
+ * outdir: "dist",
80
+ * memo: {
81
+ * include: ["content/**", "templates/**", "config.toml"],
82
+ * },
83
+ * });
84
+ * ```
85
+ */
86
+ export const StaticSite = <
87
+ const Bindings extends WorkerBindingProps = {},
88
+ Req = never,
89
+ >(
90
+ id: string,
91
+ propsEff:
92
+ | InputProps<StaticSiteProps<Bindings>>
93
+ | Effect.Effect<InputProps<StaticSiteProps<Bindings>>, never, Req>,
94
+ ) =>
23
95
  Effect.gen(function* () {
24
- // TODO(sam): local dev/hmr support?
25
- const build = yield* Command("Build", props);
96
+ const props = Effect.isEffect(propsEff)
97
+ ? propsEff
98
+ : Effect.succeed(propsEff);
26
99
 
27
- const worker = yield* Worker("Worker", {
28
- ...props,
29
- assets: {
30
- path: build.outdir,
31
- hash: build.hash,
32
- config: props.assetsConfig,
33
- },
34
- });
100
+ // TODO(sam): local dev/hmr support?
101
+ const build = yield* Command(
102
+ "Build",
103
+ Effect.map(props, (props) => ({
104
+ command: props.command,
105
+ cwd: props.cwd,
106
+ memo: props.memo,
107
+ outdir: props.outdir,
108
+ env: props.env,
109
+ })),
110
+ );
35
111
 
36
- return worker;
112
+ return yield* Worker<Bindings, WorkerAssetsConfig, Req>(
113
+ "Worker",
114
+ Effect.map(props, (props) => ({
115
+ ...props,
116
+ assets: {
117
+ path: build.outdir,
118
+ hash: build.hash,
119
+ config: props.assetsConfig,
120
+ },
121
+ })),
122
+ );
37
123
  }).pipe(Namespace.push(id));
@@ -1,8 +1,16 @@
1
- import type { InputProps } from "../../Input.ts";
1
+ import * as Effect from "effect/Effect";
2
2
  import type { MemoOptions } from "../../Build/Memo.ts";
3
- import { Worker, type WorkerProps } from "../Workers/Worker.ts";
3
+ import type { InputProps } from "../../Input.ts";
4
+ import {
5
+ Worker,
6
+ type WorkerAssetsConfig,
7
+ type WorkerBindingProps,
8
+ type WorkerProps,
9
+ } from "../Workers/Worker.ts";
4
10
 
5
- export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
11
+ export interface ViteProps<
12
+ Bindings extends WorkerBindingProps = {},
13
+ > extends Omit<WorkerProps<Bindings>, "vite" | "main"> {
6
14
  /**
7
15
  * Root directory passed to Vite's `root` option.
8
16
  * Defaults to the current working directory (`process.cwd()`).
@@ -21,22 +29,31 @@ export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
21
29
  /**
22
30
  * A Cloudflare Worker deployed from a Vite project.
23
31
  *
24
- * `Vite` uses the Cloudflare Vite plugin to build both the server bundle and
25
- * client assets in a single `vite build` invocation — no manual `main`
26
- * entrypoint, build command, output directory, or Wrangler configuration
27
- * required.
32
+ * `Vite` uses the Cloudflare Vite plugin to build both the server bundle
33
+ * and client assets in a single `vite build` invocation — no manual
34
+ * `main` entrypoint, build command, output directory, or Wrangler
35
+ * configuration required.
28
36
  *
29
37
  * Input files are content-hashed (respecting `.gitignore` by default) so
30
38
  * unchanged projects skip the build and deploy entirely.
31
39
  *
40
+ * @resource
41
+ *
32
42
  * @section Deploying a Static Site
33
- * @example Basic Static Site
43
+ * For a pure static site (no SSR), a single call is all you need.
44
+ * Vite builds the project and Alchemy deploys the output as a
45
+ * Cloudflare Worker with static assets.
46
+ *
47
+ * @example Static Vite site
34
48
  * ```typescript
35
49
  * const site = yield* Cloudflare.Vite("Website");
36
50
  * ```
37
51
  *
38
- * @section Deploying a TanStack Start App
39
- * @example TanStack Start with SSR
52
+ * @section SSR Frameworks
53
+ * For SSR frameworks like TanStack Start, SolidStart, or Nuxt, enable
54
+ * `nodejs_compat` so the server bundle can use Node.js APIs.
55
+ *
56
+ * @example TanStack Start
40
57
  * ```typescript
41
58
  * const app = yield* Cloudflare.Vite("TanStackStart", {
42
59
  * compatibility: {
@@ -45,8 +62,43 @@ export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
45
62
  * });
46
63
  * ```
47
64
  *
65
+ * @example SolidStart with worker-first routing
66
+ * ```typescript
67
+ * const app = yield* Cloudflare.Vite("SolidStart", {
68
+ * compatibility: {
69
+ * flags: ["nodejs_compat"],
70
+ * },
71
+ * assets: {
72
+ * config: { runWorkerFirst: true },
73
+ * },
74
+ * });
75
+ * ```
76
+ *
77
+ * @section Single-Page Applications
78
+ * For SPAs (React, Vue, etc.), configure asset handling so all
79
+ * routes fall back to `index.html`.
80
+ *
81
+ * @example Vue SPA
82
+ * ```typescript
83
+ * const app = yield* Cloudflare.Vite("Vue", {
84
+ * compatibility: {
85
+ * flags: ["nodejs_compat"],
86
+ * },
87
+ * assets: {
88
+ * config: {
89
+ * htmlHandling: "auto-trailing-slash",
90
+ * notFoundHandling: "single-page-application",
91
+ * },
92
+ * },
93
+ * });
94
+ * ```
95
+ *
48
96
  * @section Custom Rebuild Scope
49
- * @example Narrow the Memo Scope
97
+ * By default, every non-gitignored file is hashed to decide whether
98
+ * a rebuild is needed. Use `memo` to narrow the scope when your
99
+ * project has large directories that don't affect the build output.
100
+ *
101
+ * @example Narrowing the memo scope
50
102
  * ```typescript
51
103
  * const site = yield* Cloudflare.Vite("Docs", {
52
104
  * memo: {
@@ -55,12 +107,26 @@ export interface ViteProps extends Omit<WorkerProps, "vite" | "main"> {
55
107
  * });
56
108
  * ```
57
109
  */
58
- export const Vite = (id: string, props: InputProps<ViteProps> = {}) =>
59
- Worker(id, {
60
- ...props,
61
- main: undefined!,
62
- vite: {
63
- rootDir: props.rootDir,
64
- memo: props.memo,
65
- },
66
- });
110
+ export const Vite = <
111
+ const Bindings extends WorkerBindingProps = {},
112
+ Req = never,
113
+ >(
114
+ id: string,
115
+ propsEff?:
116
+ | InputProps<ViteProps<Bindings>>
117
+ | Effect.Effect<InputProps<ViteProps<Bindings>>, never, Req>,
118
+ ) =>
119
+ Worker<Bindings, WorkerAssetsConfig, Req>(
120
+ id,
121
+ Effect.map(
122
+ Effect.isEffect(propsEff) ? propsEff : Effect.succeed(propsEff),
123
+ (props) => ({
124
+ ...props,
125
+ main: undefined!,
126
+ vite: {
127
+ rootDir: props?.rootDir,
128
+ memo: props?.memo,
129
+ },
130
+ }),
131
+ ),
132
+ );
@@ -1,9 +1,7 @@
1
1
  import * as workers from "@distilled.cloud/cloudflare/workers";
2
- import * as Context from "effect/Context";
3
2
  import * as Data from "effect/Data";
4
3
  import * as Effect from "effect/Effect";
5
4
  import * as FileSystem from "effect/FileSystem";
6
- import * as Layer from "effect/Layer";
7
5
  import * as Path from "effect/Path";
8
6
  import type { PlatformError } from "effect/PlatformError";
9
7
  import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
@@ -12,6 +10,13 @@ import { sha256, sha256Object } from "../../Util/index.ts";
12
10
  const MAX_ASSET_SIZE = 1024 * 1024 * 25; // 25MB
13
11
  const MAX_ASSET_COUNT = 20_000;
14
12
 
13
+ export interface Assets {
14
+ kind: "Cloudflare.Workers.Assets";
15
+ }
16
+
17
+ export const isAssets = (value: any): value is Assets =>
18
+ value?.kind === "Cloudflare.Workers.Assets";
19
+
15
20
  export interface AssetsConfig extends Exclude<
16
21
  Exclude<workers.PutScriptRequest["metadata"]["assets"], undefined>["config"],
17
22
  undefined
@@ -31,27 +36,6 @@ export interface AssetsProps {
31
36
  config?: AssetsConfig;
32
37
  }
33
38
 
34
- export class Assets extends Context.Service<
35
- Assets,
36
- {
37
- read(
38
- directory: AssetsProps,
39
- ): Effect.Effect<AssetReadResult, PlatformError | ValidationError>;
40
- upload(
41
- accountId: string,
42
- workerName: string,
43
- assets: AssetReadResult,
44
- session: ScopedPlanStatusSession,
45
- ): Effect.Effect<
46
- { jwt: string | undefined },
47
- | PlatformError
48
- | ValidationError
49
- | workers.CreateScriptAssetUploadError
50
- | workers.CreateAssetUploadError
51
- >;
52
- }
53
- >()("Cloudflare.Assets") {}
54
-
55
39
  export type ValidationError =
56
40
  | AssetTooLargeError
57
41
  | TooManyAssetsError
@@ -102,196 +86,175 @@ const getContentType = (name: string) => {
102
86
  return "application/octet-stream";
103
87
  };
104
88
 
105
- export const AssetsProvider = () =>
106
- Layer.effect(
107
- Assets,
108
- Effect.gen(function* () {
109
- const fs = yield* FileSystem.FileSystem;
110
- const path = yield* Path.Path;
111
- const createAssetUpload = yield* workers.createAssetUpload;
112
- const createScriptAssetUpload = yield* workers.createScriptAssetUpload;
89
+ const maybeReadString = Effect.fnUntraced(function* (file: string) {
90
+ const fs = yield* FileSystem.FileSystem;
91
+ return yield* fs.readFileString(file).pipe(
92
+ Effect.catchIf(
93
+ (error) =>
94
+ error._tag === "PlatformError" && error.reason._tag === "NotFound",
95
+ () => Effect.succeed(undefined),
96
+ ),
97
+ );
98
+ });
113
99
 
114
- const maybeReadString = Effect.fnUntraced(function* (file: string) {
115
- return yield* fs.readFileString(file).pipe(
116
- Effect.catchIf(
117
- (error) =>
118
- error._tag === "PlatformError" &&
119
- error.reason._tag === "NotFound",
120
- () => Effect.succeed(undefined),
121
- ),
122
- );
123
- });
100
+ const createIgnoreMatcher = Effect.fnUntraced(function* (patterns: string[]) {
101
+ const matcher = yield* Effect.promise(() =>
102
+ import("ignore").then(({ default: ignore }) => ignore().add(patterns)),
103
+ );
104
+ return (file: string) => matcher.ignores(file);
105
+ });
124
106
 
125
- const createIgnoreMatcher = Effect.fnUntraced(function* (
126
- patterns: string[],
127
- ) {
128
- const matcher = yield* Effect.promise(() =>
129
- import("ignore").then(({ default: ignore }) =>
130
- ignore().add(patterns),
131
- ),
132
- );
133
- return (file: string) => matcher.ignores(file);
134
- });
107
+ export const readAssets = Effect.fnUntraced(function* (props: AssetsProps) {
108
+ const fs = yield* FileSystem.FileSystem;
109
+ const path = yield* Path.Path;
110
+ const resolvedDirectory = path.resolve(props.directory);
111
+ const [files, ignore, _headers, _redirects] = yield* Effect.all([
112
+ fs.readDirectory(resolvedDirectory, { recursive: true }),
113
+ maybeReadString(path.join(resolvedDirectory, ".assetsignore")),
114
+ maybeReadString(path.join(resolvedDirectory, "_headers")),
115
+ maybeReadString(path.join(resolvedDirectory, "_redirects")),
116
+ ]);
117
+ const ignores = yield* createIgnoreMatcher([
118
+ ".assetsignore",
119
+ "_headers",
120
+ "_redirects",
121
+ ...(ignore
122
+ ?.split("\n")
123
+ .map((line) => line.trim())
124
+ .filter((line) => line.length > 0 && !line.startsWith("#")) ?? []),
125
+ ]);
126
+ const manifest = new Map<string, { hash: string; size: number }>();
127
+ let count = 0;
128
+ yield* Effect.forEach(
129
+ files,
130
+ Effect.fnUntraced(function* (name) {
131
+ if (ignores(name)) {
132
+ return;
133
+ }
134
+ const file = path.join(resolvedDirectory, name);
135
+ const stat = yield* fs.stat(file);
136
+ if (stat.type !== "File") {
137
+ return;
138
+ }
139
+ const size = Number(stat.size);
140
+ if (size > MAX_ASSET_SIZE) {
141
+ return yield* new AssetTooLargeError({
142
+ message: `Asset ${name} is too large (the maximum size is ${MAX_ASSET_SIZE / 1024 / 1024} MB; this asset is ${size / 1024 / 1024} MB)`,
143
+ name,
144
+ size,
145
+ });
146
+ }
147
+ const hash = yield* fs.readFile(file).pipe(
148
+ Effect.flatMap(sha256),
149
+ Effect.map((hash) => hash.slice(0, 32)),
150
+ );
151
+ count++;
152
+ if (count > MAX_ASSET_COUNT) {
153
+ return yield* new TooManyAssetsError({
154
+ message: `Too many assets (the maximum count is ${MAX_ASSET_COUNT}; this directory has ${count} assets)`,
155
+ directory: props.directory,
156
+ count,
157
+ });
158
+ }
159
+ manifest.set(
160
+ (name.startsWith("/") ? name : `/${name}`).replaceAll("\\", "/"),
161
+ {
162
+ hash,
163
+ size,
164
+ },
165
+ );
166
+ }),
167
+ );
168
+ const result = {
169
+ directory: props.directory,
170
+ config: props.config,
171
+ manifest: Object.fromEntries(
172
+ Array.from(manifest.entries()).sort((a, b) => a[0].localeCompare(b[0])),
173
+ ),
174
+ _headers,
175
+ _redirects,
176
+ };
177
+ return {
178
+ ...result,
179
+ hash: yield* sha256Object(result),
180
+ };
181
+ });
135
182
 
136
- return {
137
- read: Effect.fnUntraced(function* (props: AssetsProps) {
138
- const resolvedDirectory = path.resolve(props.directory);
139
- const [files, ignore, _headers, _redirects] = yield* Effect.all([
140
- fs.readDirectory(resolvedDirectory, { recursive: true }),
141
- maybeReadString(path.join(resolvedDirectory, ".assetsignore")),
142
- maybeReadString(path.join(resolvedDirectory, "_headers")),
143
- maybeReadString(path.join(resolvedDirectory, "_redirects")),
144
- ]);
145
- const ignores = yield* createIgnoreMatcher([
146
- ".assetsignore",
147
- "_headers",
148
- "_redirects",
149
- ...(ignore
150
- ?.split("\n")
151
- .map((line) => line.trim())
152
- .filter((line) => line.length > 0 && !line.startsWith("#")) ??
153
- []),
154
- ]);
155
- const manifest = new Map<string, { hash: string; size: number }>();
156
- let count = 0;
157
- yield* Effect.forEach(
158
- files,
159
- Effect.fnUntraced(function* (name) {
160
- if (ignores(name)) {
161
- return;
162
- }
163
- const file = path.join(resolvedDirectory, name);
164
- const stat = yield* fs.stat(file);
165
- if (stat.type !== "File") {
166
- return;
167
- }
168
- const size = Number(stat.size);
169
- if (size > MAX_ASSET_SIZE) {
170
- return yield* new AssetTooLargeError({
171
- message: `Asset ${name} is too large (the maximum size is ${MAX_ASSET_SIZE / 1024 / 1024} MB; this asset is ${size / 1024 / 1024} MB)`,
172
- name,
173
- size,
174
- });
175
- }
176
- const hash = yield* fs.readFile(file).pipe(
177
- Effect.flatMap(sha256),
178
- Effect.map((hash) => hash.slice(0, 32)),
179
- );
180
- count++;
181
- if (count > MAX_ASSET_COUNT) {
182
- return yield* new TooManyAssetsError({
183
- message: `Too many assets (the maximum count is ${MAX_ASSET_COUNT}; this directory has ${count} assets)`,
184
- directory: props.directory,
185
- count,
186
- });
187
- }
188
- manifest.set(
189
- (name.startsWith("/") ? name : `/${name}`).replaceAll(
190
- "\\",
191
- "/",
192
- ),
193
- {
194
- hash,
195
- size,
196
- },
197
- );
198
- }),
199
- );
200
- const result = {
201
- directory: props.directory,
202
- config: props.config,
203
- manifest: Object.fromEntries(
204
- Array.from(manifest.entries()).sort((a, b) =>
205
- a[0].localeCompare(b[0]),
206
- ),
207
- ),
208
- _headers,
209
- _redirects,
210
- };
211
- return {
212
- ...result,
213
- hash: yield* sha256Object(result),
214
- };
215
- }),
216
- upload: Effect.fnUntraced(function* (
217
- accountId: string,
218
- workerName: string,
219
- assets: AssetReadResult,
220
- { note }: ScopedPlanStatusSession,
221
- ) {
222
- yield* note("Checking assets...");
223
- const session = yield* createScriptAssetUpload({
224
- accountId,
225
- scriptName: workerName,
226
- manifest: assets.manifest,
227
- });
228
- if (!session.buckets?.length) {
229
- return { jwt: session.jwt ?? undefined };
230
- }
231
- if (!session.jwt) {
232
- return { jwt: undefined };
233
- }
234
- const uploadJwt = session.jwt;
235
- let uploaded = 0;
236
- const total = session.buckets.flat().length;
237
- yield* note(`Uploaded ${uploaded} of ${total} assets...`);
238
- const assetsByHash = new Map<string, string>();
239
- for (const [name, { hash }] of Object.entries(assets.manifest)) {
240
- assetsByHash.set(hash, name);
183
+ export const uploadAssets = Effect.fnUntraced(function* (
184
+ accountId: string,
185
+ workerName: string,
186
+ assets: AssetReadResult,
187
+ { note }: ScopedPlanStatusSession,
188
+ ) {
189
+ const fs = yield* FileSystem.FileSystem;
190
+ const path = yield* Path.Path;
191
+ const createScriptAssetUpload = yield* workers.createScriptAssetUpload;
192
+ const createAssetUpload = yield* workers.createAssetUpload;
193
+
194
+ yield* note("Checking assets...");
195
+ const session = yield* createScriptAssetUpload({
196
+ accountId,
197
+ scriptName: workerName,
198
+ manifest: assets.manifest,
199
+ });
200
+ if (!session.buckets?.length) {
201
+ return { jwt: session.jwt ?? undefined };
202
+ }
203
+ if (!session.jwt) {
204
+ return { jwt: undefined };
205
+ }
206
+ const uploadJwt = session.jwt;
207
+ let uploaded = 0;
208
+ const total = session.buckets.flat().length;
209
+ yield* note(`Uploaded ${uploaded} of ${total} assets...`);
210
+ const assetsByHash = new Map<string, string>();
211
+ for (const [name, { hash }] of Object.entries(assets.manifest)) {
212
+ assetsByHash.set(hash, name);
213
+ }
214
+ let jwt: string | undefined | null;
215
+ const directory = path.resolve(assets.directory);
216
+ yield* Effect.forEach(
217
+ session.buckets,
218
+ Effect.fnUntraced(function* (bucket) {
219
+ const body: Record<string, File> = {};
220
+ yield* Effect.forEach(
221
+ bucket,
222
+ Effect.fnUntraced(function* (hash) {
223
+ const name = assetsByHash.get(hash);
224
+ if (!name) {
225
+ return yield* new AssetNotFoundError({
226
+ message: `Asset ${hash} not found in manifest`,
227
+ hash,
228
+ });
241
229
  }
242
- let jwt: string | undefined | null;
243
- const directory = path.resolve(assets.directory);
244
- yield* Effect.forEach(
245
- session.buckets,
246
- Effect.fnUntraced(function* (bucket) {
247
- const body: Record<string, File> = {};
248
- yield* Effect.forEach(
249
- bucket,
250
- Effect.fnUntraced(function* (hash) {
251
- const name = assetsByHash.get(hash);
252
- if (!name) {
253
- return yield* new AssetNotFoundError({
254
- message: `Asset ${hash} not found in manifest`,
255
- hash,
256
- });
257
- }
258
- const file = yield* fs
259
- .readFile(path.join(directory, name))
260
- .pipe(
261
- Effect.mapError(
262
- (error) =>
263
- new FailedToReadAssetError({
264
- message: `Failed to read asset ${name}: ${error.message}`,
265
- name,
266
- cause: error,
267
- }),
268
- ),
269
- );
270
- body[hash] = new File(
271
- [Buffer.from(file).toString("base64")],
272
- hash,
273
- {
274
- type: getContentType(name),
275
- },
276
- );
230
+ const file = yield* fs.readFile(path.join(directory, name)).pipe(
231
+ Effect.mapError(
232
+ (error) =>
233
+ new FailedToReadAssetError({
234
+ message: `Failed to read asset ${name}: ${error.message}`,
235
+ name,
236
+ cause: error,
277
237
  }),
278
- );
279
- const result = yield* createAssetUpload({
280
- accountId,
281
- base64: true,
282
- body,
283
- jwtToken: uploadJwt,
284
- });
285
-
286
- uploaded += bucket.length;
287
- yield* note(`Uploaded ${uploaded} of ${total} assets...`);
288
- if (result.jwt) {
289
- jwt = result.jwt;
290
- }
291
- }),
238
+ ),
292
239
  );
293
- return { jwt: jwt ?? undefined };
240
+ body[hash] = new File([Buffer.from(file).toString("base64")], hash, {
241
+ type: getContentType(name),
242
+ });
294
243
  }),
295
- };
244
+ );
245
+ const result = yield* createAssetUpload({
246
+ accountId,
247
+ base64: true,
248
+ body,
249
+ jwtToken: uploadJwt,
250
+ });
251
+
252
+ uploaded += bucket.length;
253
+ yield* note(`Uploaded ${uploaded} of ${total} assets...`);
254
+ if (result.jwt) {
255
+ jwt = result.jwt;
256
+ }
296
257
  }),
297
258
  );
259
+ return { jwt: jwt ?? undefined };
260
+ });