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.
- package/bin/{alchemy-effect.js → alchemy.js} +171 -171
- package/bin/alchemy.js.map +1 -0
- package/bin/{alchemy-effect.sh → alchemy.sh} +2 -2
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +167 -167
- package/lib/cli/index.js.map +1 -1
- package/package.json +54 -45
- package/src/AWS/AGENTS.md +10 -10
- package/src/AWS/Assets.ts +1 -1
- package/src/AWS/AuthProvider.ts +392 -0
- package/src/AWS/Credentials.ts +0 -1
- package/src/AWS/DynamoDB/Table.ts +63 -10
- package/src/AWS/EC2/VpcEndpoint.ts +4 -4
- package/src/AWS/EC2/hosted.ts +1 -1
- package/src/AWS/ECS/Task.ts +1 -1
- package/src/AWS/Kinesis/Stream.ts +44 -8
- package/src/AWS/Lambda/Function.ts +218 -10
- package/src/AWS/S3/Bucket.ts +26 -19
- package/src/AWS/S3/BucketNotifications.ts +1 -1
- package/src/AWS/SNS/Topic.ts +47 -10
- package/src/AWS/SQS/Queue.ts +66 -0
- package/src/Auth/AuthProvider.ts +33 -0
- package/src/Auth/Credentials.ts +56 -0
- package/src/Auth/Env.ts +45 -0
- package/src/Auth/Profile.ts +72 -0
- package/src/Auth/index.ts +2 -0
- package/src/Cloudflare/Auth/AuthProvider.ts +670 -0
- package/src/Cloudflare/Auth/OAuthClient.ts +315 -0
- package/src/Cloudflare/Container/Container.ts +122 -0
- package/src/Cloudflare/Container/ContainerApplication.ts +6 -3
- package/src/Cloudflare/Container/ContainerBinding.ts +2 -4
- package/src/Cloudflare/Container/StartContainer.ts +1 -1
- package/src/Cloudflare/D1/D1Database.ts +23 -4
- package/src/Cloudflare/KV/KVNamespace.ts +25 -0
- package/src/Cloudflare/Providers.ts +1 -6
- package/src/Cloudflare/R2/R2Bucket.ts +45 -2
- package/src/Cloudflare/Website/StaticSite.ts +101 -15
- package/src/Cloudflare/Website/Vite.ts +86 -20
- package/src/Cloudflare/Workers/Assets.ts +170 -207
- package/src/Cloudflare/Workers/DurableObjectNamespace.ts +629 -0
- package/src/Cloudflare/Workers/DurableObjectState.ts +63 -0
- package/src/Cloudflare/Workers/DurableObjectStorage.ts +256 -0
- package/src/Cloudflare/Workers/DynamicWorkerLoader.ts +66 -7
- package/src/Cloudflare/Workers/InferEnv.ts +11 -7
- package/src/Cloudflare/Workers/Rpc.ts +13 -2
- package/src/Cloudflare/Workers/ScheduledEvents.ts +185 -0
- package/src/Cloudflare/Workers/WebSocket.ts +1 -1
- package/src/Cloudflare/Workers/Worker.ts +572 -67
- package/src/Cloudflare/Workers/Workflow.ts +53 -11
- package/src/Cloudflare/Workers/index.ts +4 -1
- package/src/Construct.ts +2 -2
- package/src/GitHub/Comment.ts +224 -0
- package/src/GitHub/Secret.ts +257 -0
- package/src/GitHub/Variable.ts +166 -0
- package/src/GitHub/index.ts +3 -0
- package/src/Kubernetes/client.ts +2 -2
- package/src/Output.ts +1 -1
- package/src/Platform.ts +10 -5
- package/src/Provider.ts +1 -1
- package/src/Resource.ts +4 -4
- package/src/Test/Vitest.ts +4 -3
- package/src/Util/Clank.ts +77 -0
- package/src/Util/PlatformServices.ts +21 -0
- package/src/Util/dedent.ts +59 -0
- package/src/Util/index.ts +1 -0
- package/bin/alchemy-effect.js.map +0 -1
- package/src/Cloudflare/Workers/DurableObject.ts +0 -527
- package/src/Daemon/Client.ts +0 -116
- package/src/Daemon/Config.ts +0 -28
- package/src/Daemon/Errors.ts +0 -48
- package/src/Daemon/Lock.ts +0 -162
- package/src/Daemon/ProcessRegistry.ts +0 -284
- package/src/Daemon/RpcSchema.ts +0 -43
- package/src/Daemon/RpcServer.ts +0 -231
- package/src/Daemon/index.ts +0 -27
- package/src/Spawn.ts +0 -23
- /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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
96
|
+
const props = Effect.isEffect(propsEff)
|
|
97
|
+
? propsEff
|
|
98
|
+
: Effect.succeed(propsEff);
|
|
26
99
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
2
|
import type { MemoOptions } from "../../Build/Memo.ts";
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
25
|
-
* client assets in a single `vite build` invocation — no manual
|
|
26
|
-
* entrypoint, build command, output directory, or Wrangler
|
|
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
|
-
*
|
|
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
|
|
39
|
-
*
|
|
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
|
-
*
|
|
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 =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Effect.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
+
});
|