alchemy 2.0.0-beta.78 → 2.0.0-beta.79
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/cli.js +25 -65
- package/bin/exec.js +1 -1
- package/bin/tsconfig.json +5 -0
- package/lib/Cloudflare/Workers/InferEnv.d.ts +3 -1
- package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +5 -2
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Prisma/Providers.d.ts +4 -1
- package/lib/Prisma/Providers.d.ts.map +1 -1
- package/lib/Prisma/Providers.js +7 -2
- package/lib/Prisma/Providers.js.map +1 -1
- package/lib/Prisma/Website/Artifact.d.ts +52 -0
- package/lib/Prisma/Website/Artifact.d.ts.map +1 -0
- package/lib/Prisma/Website/Artifact.js +285 -0
- package/lib/Prisma/Website/Artifact.js.map +1 -0
- package/lib/Prisma/Website/Astro.d.ts +62 -0
- package/lib/Prisma/Website/Astro.d.ts.map +1 -0
- package/lib/Prisma/Website/Astro.js +50 -0
- package/lib/Prisma/Website/Astro.js.map +1 -0
- package/lib/Prisma/Website/Foldkit.d.ts +30 -0
- package/lib/Prisma/Website/Foldkit.d.ts.map +1 -0
- package/lib/Prisma/Website/Foldkit.js +30 -0
- package/lib/Prisma/Website/Foldkit.js.map +1 -0
- package/lib/Prisma/Website/FrameworkSite.d.ts +82 -0
- package/lib/Prisma/Website/FrameworkSite.d.ts.map +1 -0
- package/lib/Prisma/Website/FrameworkSite.js +96 -0
- package/lib/Prisma/Website/FrameworkSite.js.map +1 -0
- package/lib/Prisma/Website/Nextjs.d.ts +35 -0
- package/lib/Prisma/Website/Nextjs.d.ts.map +1 -0
- package/lib/Prisma/Website/Nextjs.js +37 -0
- package/lib/Prisma/Website/Nextjs.js.map +1 -0
- package/lib/Prisma/Website/Nuxt.d.ts +37 -0
- package/lib/Prisma/Website/Nuxt.d.ts.map +1 -0
- package/lib/Prisma/Website/Nuxt.js +37 -0
- package/lib/Prisma/Website/Nuxt.js.map +1 -0
- package/lib/Prisma/Website/Octane.d.ts +35 -0
- package/lib/Prisma/Website/Octane.d.ts.map +1 -0
- package/lib/Prisma/Website/Octane.js +36 -0
- package/lib/Prisma/Website/Octane.js.map +1 -0
- package/lib/Prisma/Website/ReactRouter.d.ts +35 -0
- package/lib/Prisma/Website/ReactRouter.d.ts.map +1 -0
- package/lib/Prisma/Website/ReactRouter.js +36 -0
- package/lib/Prisma/Website/ReactRouter.js.map +1 -0
- package/lib/Prisma/Website/SolidStart.d.ts +37 -0
- package/lib/Prisma/Website/SolidStart.d.ts.map +1 -0
- package/lib/Prisma/Website/SolidStart.js +37 -0
- package/lib/Prisma/Website/SolidStart.js.map +1 -0
- package/lib/Prisma/Website/StaticSite.d.ts +57 -0
- package/lib/Prisma/Website/StaticSite.d.ts.map +1 -0
- package/lib/Prisma/Website/StaticSite.js +118 -0
- package/lib/Prisma/Website/StaticSite.js.map +1 -0
- package/lib/Prisma/Website/SvelteKit.d.ts +37 -0
- package/lib/Prisma/Website/SvelteKit.d.ts.map +1 -0
- package/lib/Prisma/Website/SvelteKit.js +37 -0
- package/lib/Prisma/Website/SvelteKit.js.map +1 -0
- package/lib/Prisma/Website/TanStackStart.d.ts +35 -0
- package/lib/Prisma/Website/TanStackStart.d.ts.map +1 -0
- package/lib/Prisma/Website/TanStackStart.js +36 -0
- package/lib/Prisma/Website/TanStackStart.js.map +1 -0
- package/lib/Prisma/Website/Vite.d.ts +42 -0
- package/lib/Prisma/Website/Vite.d.ts.map +1 -0
- package/lib/Prisma/Website/Vite.js +38 -0
- package/lib/Prisma/Website/Vite.js.map +1 -0
- package/lib/Prisma/Website/Vocs.d.ts +37 -0
- package/lib/Prisma/Website/Vocs.d.ts.map +1 -0
- package/lib/Prisma/Website/Vocs.js +38 -0
- package/lib/Prisma/Website/Vocs.js.map +1 -0
- package/lib/Prisma/Website/Waku.d.ts +44 -0
- package/lib/Prisma/Website/Waku.d.ts.map +1 -0
- package/lib/Prisma/Website/Waku.js +38 -0
- package/lib/Prisma/Website/Waku.js.map +1 -0
- package/lib/Prisma/Website/index.d.ts +15 -0
- package/lib/Prisma/Website/index.d.ts.map +1 -0
- package/lib/Prisma/Website/index.js +14 -0
- package/lib/Prisma/Website/index.js.map +1 -0
- package/lib/Prisma/index.d.ts +1 -0
- package/lib/Prisma/index.d.ts.map +1 -1
- package/lib/Prisma/index.js +1 -0
- package/lib/Prisma/index.js.map +1 -1
- package/lib/State/HttpStateApi.d.ts +11 -11
- package/lib/Website/Server.d.ts.map +1 -1
- package/lib/Website/Server.js +18 -8
- package/lib/Website/Server.js.map +1 -1
- package/package.json +6 -6
- package/src/Cloudflare/Workers/InferEnv.ts +5 -3
- package/src/Cloudflare/Workers/Worker.ts +25 -5
- package/src/Prisma/Providers.ts +11 -0
- package/src/Prisma/Website/Artifact.ts +429 -0
- package/src/Prisma/Website/Astro.ts +72 -0
- package/src/Prisma/Website/Foldkit.ts +34 -0
- package/src/Prisma/Website/FrameworkSite.ts +204 -0
- package/src/Prisma/Website/Nextjs.ts +41 -0
- package/src/Prisma/Website/Nuxt.ts +44 -0
- package/src/Prisma/Website/Octane.ts +40 -0
- package/src/Prisma/Website/ReactRouter.ts +40 -0
- package/src/Prisma/Website/SolidStart.ts +44 -0
- package/src/Prisma/Website/StaticSite.ts +149 -0
- package/src/Prisma/Website/SvelteKit.ts +44 -0
- package/src/Prisma/Website/TanStackStart.ts +40 -0
- package/src/Prisma/Website/Vite.ts +50 -0
- package/src/Prisma/Website/Vocs.ts +45 -0
- package/src/Prisma/Website/Waku.ts +52 -0
- package/src/Prisma/Website/index.ts +19 -0
- package/src/Prisma/index.ts +1 -0
- package/src/Website/Server.ts +21 -8
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { makeNodeServeEntrySource } from "@alchemy.run/frontend-frameworks/core";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Path from "effect/Path";
|
|
5
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { createPhysicalName } from "../../PhysicalName.ts";
|
|
8
|
+
import * as Provider from "../../Provider.ts";
|
|
9
|
+
import { Resource } from "../../Resource.ts";
|
|
10
|
+
import { initialCwd } from "../../Util/Node.ts";
|
|
11
|
+
import { packSiteExtraFiles } from "../../Website/packExtraFiles.ts";
|
|
12
|
+
import { createComputeArchive } from "../ComputeArchive.ts";
|
|
13
|
+
|
|
14
|
+
/** Resolved framework output to stage after Website.Server has built it. */
|
|
15
|
+
export interface WebsiteArtifactProps {
|
|
16
|
+
/** Application root, relative to the initial working directory. */
|
|
17
|
+
root: string;
|
|
18
|
+
/** Build output directory, relative to the initial working directory. */
|
|
19
|
+
distDir: string;
|
|
20
|
+
/** Node serve entry, relative to the initial working directory. */
|
|
21
|
+
serverEntry?: string;
|
|
22
|
+
/** Generate the shared static-file server instead of tracing a framework entry. */
|
|
23
|
+
static?: {
|
|
24
|
+
/** Unmatched-path behavior. @default "none" */
|
|
25
|
+
notFoundHandling?: "none" | "spa" | "404-page";
|
|
26
|
+
/** Extensionless HTML handling. @default "none" */
|
|
27
|
+
htmlHandling?: "none" | "drop-trailing-slash";
|
|
28
|
+
/** HTML file served for misses, relative to the static output. @default "404.html" */
|
|
29
|
+
errorPage?: string;
|
|
30
|
+
};
|
|
31
|
+
/** Next.js keeps its output and configuration beside the serve entry. */
|
|
32
|
+
layout?: "output" | "next";
|
|
33
|
+
/** Build output fingerprint; keeps the build dependency explicit. */
|
|
34
|
+
buildHash?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Internal, filesystem-only artifact lifecycle; never creates cloud resources. */
|
|
38
|
+
export interface WebsiteArtifact extends Resource<
|
|
39
|
+
"Prisma.WebsiteArtifact",
|
|
40
|
+
WebsiteArtifactProps,
|
|
41
|
+
{
|
|
42
|
+
/** Private directory owned by this artifact resource. */
|
|
43
|
+
directory: string;
|
|
44
|
+
/** Verified Compute tarball path. */
|
|
45
|
+
artifactPath: string;
|
|
46
|
+
/** SHA-256 of the tarball uploaded by Compute. */
|
|
47
|
+
hash: string;
|
|
48
|
+
}
|
|
49
|
+
> {}
|
|
50
|
+
|
|
51
|
+
export const WebsiteArtifact = Resource<WebsiteArtifact>(
|
|
52
|
+
"Prisma.WebsiteArtifact",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const MAX_ENTRIES = 50_000;
|
|
56
|
+
const MAX_BYTES = 256 * 1024 * 1024;
|
|
57
|
+
const MAX_FILE_BYTES = 128 * 1024 * 1024;
|
|
58
|
+
const excluded = (relative: string) =>
|
|
59
|
+
relative
|
|
60
|
+
.split(/[\\/]/)
|
|
61
|
+
.some((part) =>
|
|
62
|
+
/^(?:\.git|\.alchemy|\.env(?:\..*)?|\.envrc|\.npmrc|\.yarnrc.*|\.netrc|\.pypirc|\.aws|\.ssh)$/.test(
|
|
63
|
+
part,
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
const javascript = (file: string) => /\.[cm]?[jt]sx?$/.test(file);
|
|
67
|
+
const nextExcluded = (relative: string) =>
|
|
68
|
+
/^\.next\/(?:cache|standalone|types|diagnostics|dev)(?:\/|$)/.test(
|
|
69
|
+
relative,
|
|
70
|
+
) || /^\.next\/trace(?:-|$)/.test(relative);
|
|
71
|
+
|
|
72
|
+
const fail = (message: string) => Effect.fail(new Error(message));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stage real build files and NFT's reachable runtime graph without relocating
|
|
76
|
+
* individual modules. Keeping their relative layout preserves import.meta.url,
|
|
77
|
+
* package scopes, pnpm links, and dynamically loaded framework chunks.
|
|
78
|
+
* The caller owns the surrounding Scope and must archive before it closes.
|
|
79
|
+
*/
|
|
80
|
+
export const stageWebsiteArtifact = Effect.fn(function* (
|
|
81
|
+
props: WebsiteArtifactProps,
|
|
82
|
+
) {
|
|
83
|
+
const fs = yield* FileSystem.FileSystem;
|
|
84
|
+
const path = yield* Path.Path;
|
|
85
|
+
const root = yield* fs.realPath(path.resolve(initialCwd, props.root));
|
|
86
|
+
const dist = yield* fs.realPath(path.resolve(initialCwd, props.distDir));
|
|
87
|
+
const entry =
|
|
88
|
+
props.serverEntry === undefined
|
|
89
|
+
? undefined
|
|
90
|
+
: yield* fs.realPath(path.resolve(initialCwd, props.serverEntry));
|
|
91
|
+
if (entry === undefined && props.static === undefined) {
|
|
92
|
+
return yield* fail(
|
|
93
|
+
"A website artifact requires a Node server entry or static serving options.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (entry !== undefined && (yield* fs.stat(entry)).type !== "File") {
|
|
97
|
+
return yield* fail(`Website server entry is not a file: ${entry}`);
|
|
98
|
+
}
|
|
99
|
+
if (props.layout !== "next" && dist === root) {
|
|
100
|
+
return yield* fail(
|
|
101
|
+
"Website output must be a dedicated build directory, not the application root.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const files = new Set<string>(entry === undefined ? [] : [entry]);
|
|
105
|
+
const seeds = new Set<string>(entry === undefined ? [] : [entry]);
|
|
106
|
+
const manifests: string[] = [];
|
|
107
|
+
let visited = 0;
|
|
108
|
+
const collect: (
|
|
109
|
+
source: string,
|
|
110
|
+
ancestors?: ReadonlySet<string>,
|
|
111
|
+
runtimePackage?: boolean,
|
|
112
|
+
boundary?: string,
|
|
113
|
+
) => Effect.Effect<void, Error | PlatformError> = Effect.fn(function* (
|
|
114
|
+
source,
|
|
115
|
+
ancestors = new Set(),
|
|
116
|
+
runtimePackage = false,
|
|
117
|
+
boundary,
|
|
118
|
+
) {
|
|
119
|
+
if (++visited > MAX_ENTRIES)
|
|
120
|
+
return yield* fail(
|
|
121
|
+
"Website artifact exceeds the 50,000-entry safety limit.",
|
|
122
|
+
);
|
|
123
|
+
const relative = path.relative(root, source).replaceAll("\\", "/");
|
|
124
|
+
if (
|
|
125
|
+
excluded(relative) ||
|
|
126
|
+
(runtimePackage && /(?:\.map|\.d\.ts)$/.test(source)) ||
|
|
127
|
+
(props.layout === "next" && nextExcluded(relative))
|
|
128
|
+
)
|
|
129
|
+
return;
|
|
130
|
+
const real = yield* fs.realPath(source);
|
|
131
|
+
const relativeToBoundary = path.relative(boundary ?? real, real);
|
|
132
|
+
if (
|
|
133
|
+
relativeToBoundary === ".." ||
|
|
134
|
+
relativeToBoundary.startsWith(`..${path.sep}`) ||
|
|
135
|
+
path.isAbsolute(relativeToBoundary)
|
|
136
|
+
) {
|
|
137
|
+
return yield* fail(
|
|
138
|
+
`Website output symlink escapes its selected directory: ${source}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const stat = yield* fs.stat(real);
|
|
142
|
+
if (stat.type === "Directory") {
|
|
143
|
+
if (ancestors.has(real))
|
|
144
|
+
return yield* fail(`Cyclic website output symlink: ${source}`);
|
|
145
|
+
const next = new Set([...ancestors, real]);
|
|
146
|
+
for (const name of yield* fs.readDirectory(source)) {
|
|
147
|
+
// Runtime dependencies are traced, never copied wholesale.
|
|
148
|
+
if (name === "node_modules") continue;
|
|
149
|
+
yield* collect(
|
|
150
|
+
path.join(source, name),
|
|
151
|
+
next,
|
|
152
|
+
runtimePackage,
|
|
153
|
+
boundary ?? real,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
} else if (stat.type === "File") {
|
|
157
|
+
files.add(source);
|
|
158
|
+
if (!runtimePackage && props.static === undefined && javascript(source))
|
|
159
|
+
seeds.add(source);
|
|
160
|
+
if (props.layout === "next" && source.endsWith(".nft.json"))
|
|
161
|
+
manifests.push(source);
|
|
162
|
+
} else {
|
|
163
|
+
return yield* fail(`Unsupported website output file: ${source}`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const extras = yield* packSiteExtraFiles(
|
|
168
|
+
props.layout === "next" ? root : dist,
|
|
169
|
+
props.layout === "next" ? "next" : "client",
|
|
170
|
+
);
|
|
171
|
+
if (
|
|
172
|
+
props.layout === "next" &&
|
|
173
|
+
!(yield* fs.exists(path.join(root, ".next", "BUILD_ID")))
|
|
174
|
+
) {
|
|
175
|
+
return yield* fail(
|
|
176
|
+
"Next.js output is missing .next/BUILD_ID; run the production build before packaging.",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
for (const extra of extras ?? []) yield* collect(extra.source);
|
|
180
|
+
// Also include config extensions not currently listed by packSiteExtraFiles.
|
|
181
|
+
if (props.layout === "next") {
|
|
182
|
+
for (const name of ["next.config.mts", "next.config.cts"]) {
|
|
183
|
+
const config = path.join(root, name);
|
|
184
|
+
if (yield* fs.exists(config)) yield* collect(config);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (props.layout === "next") {
|
|
189
|
+
const nextPackage = yield* Effect.try(() =>
|
|
190
|
+
createRequire(path.join(root, "package.json")).resolve(
|
|
191
|
+
"next/package.json",
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
const nextRoot = path.dirname(nextPackage);
|
|
195
|
+
// Next's custom server resolves internal modules through runtime alias
|
|
196
|
+
// tables that NFT cannot evaluate. Keep that package, not node_modules.
|
|
197
|
+
yield* collect(nextRoot, undefined, true);
|
|
198
|
+
yield* collect(path.join(nextRoot, "dist", "compiled", "webpack"));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const manifest of manifests) {
|
|
202
|
+
const text = yield* fs.readFileString(manifest);
|
|
203
|
+
const parsed = yield* Effect.try(
|
|
204
|
+
() => JSON.parse(text) as { files?: unknown },
|
|
205
|
+
);
|
|
206
|
+
if (
|
|
207
|
+
!Array.isArray(parsed.files) ||
|
|
208
|
+
!parsed.files.every((file): file is string => typeof file === "string")
|
|
209
|
+
) {
|
|
210
|
+
return yield* fail(`Invalid Next.js trace manifest: ${manifest}`);
|
|
211
|
+
}
|
|
212
|
+
for (const file of parsed.files) {
|
|
213
|
+
const source = path.resolve(path.dirname(manifest), file);
|
|
214
|
+
if (excluded(path.relative(root, source)))
|
|
215
|
+
return yield* fail(
|
|
216
|
+
`A Next.js runtime dependency is a sensitive file: ${source}`,
|
|
217
|
+
);
|
|
218
|
+
files.add(source);
|
|
219
|
+
if (javascript(source)) seeds.add(source);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const traceBase = path.parse(root).root;
|
|
224
|
+
const trace =
|
|
225
|
+
seeds.size === 0
|
|
226
|
+
? { fileList: new Set<string>() }
|
|
227
|
+
: yield* Effect.tryPromise({
|
|
228
|
+
try: () =>
|
|
229
|
+
import("@vercel/nft").then(({ nodeFileTrace }) =>
|
|
230
|
+
nodeFileTrace([...seeds], {
|
|
231
|
+
base: traceBase,
|
|
232
|
+
processCwd: root,
|
|
233
|
+
conditions: ["node", "production"],
|
|
234
|
+
analysis: {
|
|
235
|
+
emitGlobs: true,
|
|
236
|
+
computeFileReferences: true,
|
|
237
|
+
evaluatePureExpressions: true,
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
catch: (cause) =>
|
|
242
|
+
new Error(
|
|
243
|
+
"Failed to trace website runtime dependencies. Install @vercel/nft in the deployment workspace.",
|
|
244
|
+
{ cause },
|
|
245
|
+
),
|
|
246
|
+
}).pipe(Effect.timeout("90 seconds"));
|
|
247
|
+
for (const file of trace.fileList) files.add(path.resolve(traceBase, file));
|
|
248
|
+
if (files.size > MAX_ENTRIES)
|
|
249
|
+
return yield* fail(
|
|
250
|
+
"Website dependency trace exceeds the 50,000-entry safety limit.",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const links = new Map<string, string>();
|
|
254
|
+
const selected = new Set<string>();
|
|
255
|
+
for (const file of files) {
|
|
256
|
+
if (excluded(path.relative(root, file)))
|
|
257
|
+
return yield* fail(
|
|
258
|
+
`A website runtime dependency is a sensitive file: ${file}`,
|
|
259
|
+
);
|
|
260
|
+
const link = yield* fs
|
|
261
|
+
.readLink(file)
|
|
262
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)));
|
|
263
|
+
if (link !== undefined) {
|
|
264
|
+
const target = yield* fs.realPath(file);
|
|
265
|
+
links.set(file, target);
|
|
266
|
+
if ((yield* fs.stat(target)).type === "File")
|
|
267
|
+
selected.add(yield* fs.realPath(target));
|
|
268
|
+
} else {
|
|
269
|
+
const stat = yield* fs.stat(file);
|
|
270
|
+
if (stat.type !== "File")
|
|
271
|
+
return yield* fail(
|
|
272
|
+
`A traced runtime dependency is not a file: ${file}`,
|
|
273
|
+
);
|
|
274
|
+
selected.add(file);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let base = root;
|
|
278
|
+
const within = (directory: string, file: string) => {
|
|
279
|
+
const relative = path.relative(directory, file);
|
|
280
|
+
return (
|
|
281
|
+
relative !== ".." &&
|
|
282
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
283
|
+
!path.isAbsolute(relative)
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
for (const file of [...selected, ...links.keys(), ...links.values()]) {
|
|
287
|
+
while (!within(base, file)) base = path.dirname(base);
|
|
288
|
+
}
|
|
289
|
+
const directory = yield* fs.makeTempDirectoryScoped({
|
|
290
|
+
prefix: "alchemy-prisma-website-",
|
|
291
|
+
});
|
|
292
|
+
// pnpm's peer-qualified store names exceed ustar's 100-byte link field.
|
|
293
|
+
// Shorten only store-directory names; package scopes and links stay intact.
|
|
294
|
+
const stores = new Map(
|
|
295
|
+
[
|
|
296
|
+
...new Set(
|
|
297
|
+
[...selected, ...links.keys(), ...links.values()].flatMap((file) => {
|
|
298
|
+
const parts = file.split(path.sep);
|
|
299
|
+
return parts.flatMap((part, index) =>
|
|
300
|
+
part === ".pnpm" && parts[index + 1] !== undefined
|
|
301
|
+
? [parts[index + 1]!]
|
|
302
|
+
: [],
|
|
303
|
+
);
|
|
304
|
+
}),
|
|
305
|
+
),
|
|
306
|
+
]
|
|
307
|
+
.sort()
|
|
308
|
+
.map((name, index) => [name, `p${index.toString(36)}`]),
|
|
309
|
+
);
|
|
310
|
+
const staged = (file: string) => {
|
|
311
|
+
const parts = path.relative(base, file).split(path.sep);
|
|
312
|
+
return path.join(
|
|
313
|
+
directory,
|
|
314
|
+
"files",
|
|
315
|
+
...parts.map((part, index) =>
|
|
316
|
+
parts[index - 1] === ".pnpm" ? (stores.get(part) ?? part) : part,
|
|
317
|
+
),
|
|
318
|
+
);
|
|
319
|
+
};
|
|
320
|
+
let bytes = 0;
|
|
321
|
+
for (const file of [...selected].sort()) {
|
|
322
|
+
const stat = yield* fs.stat(file);
|
|
323
|
+
const size = Number(stat.size);
|
|
324
|
+
bytes += size;
|
|
325
|
+
if (size > MAX_FILE_BYTES || bytes > MAX_BYTES)
|
|
326
|
+
return yield* fail(
|
|
327
|
+
"Website artifact exceeds Prisma Compute's file or total byte safety limit.",
|
|
328
|
+
);
|
|
329
|
+
const dest = staged(file);
|
|
330
|
+
yield* fs.makeDirectory(path.dirname(dest), { recursive: true });
|
|
331
|
+
yield* fs.copyFile(file, dest);
|
|
332
|
+
yield* fs.chmod(dest, stat.mode & 0o777);
|
|
333
|
+
}
|
|
334
|
+
for (const [file, target] of links) {
|
|
335
|
+
// NFT also reports resolution-only links with no retained runtime files.
|
|
336
|
+
if (!(yield* fs.exists(staged(target)))) continue;
|
|
337
|
+
const dest = staged(file);
|
|
338
|
+
if (yield* fs.exists(dest)) continue;
|
|
339
|
+
yield* fs.makeDirectory(path.dirname(dest), { recursive: true });
|
|
340
|
+
yield* fs.symlink(path.relative(path.dirname(dest), staged(target)), dest);
|
|
341
|
+
}
|
|
342
|
+
if (props.static !== undefined) {
|
|
343
|
+
const client = path.relative(directory, staged(dist)).replaceAll("\\", "/");
|
|
344
|
+
yield* fs.writeFileString(
|
|
345
|
+
path.join(directory, "server.mjs"),
|
|
346
|
+
makeNodeServeEntrySource({
|
|
347
|
+
...props.static,
|
|
348
|
+
clientDirExpression: `fileURLToPath(new URL(${JSON.stringify(`./${client}/`)}, import.meta.url))`,
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
return { directory, entrypoint: "server.mjs", requiredFiles: [] };
|
|
352
|
+
}
|
|
353
|
+
if (entry === undefined) return yield* fail("Missing website server entry.");
|
|
354
|
+
const entryRelative = path
|
|
355
|
+
.relative(directory, staged(entry))
|
|
356
|
+
.replaceAll("\\", "/");
|
|
357
|
+
const cwdRelative = path
|
|
358
|
+
.relative(directory, staged(root))
|
|
359
|
+
.replaceAll("\\", "/");
|
|
360
|
+
yield* fs.makeDirectory(staged(root), { recursive: true });
|
|
361
|
+
yield* fs.writeFileString(
|
|
362
|
+
path.join(directory, "server.mjs"),
|
|
363
|
+
[
|
|
364
|
+
'import { fileURLToPath } from "node:url";',
|
|
365
|
+
`process.chdir(fileURLToPath(new URL(${JSON.stringify(`./${cwdRelative}/`)}, import.meta.url)));`,
|
|
366
|
+
`await import(new URL(${JSON.stringify(`./${entryRelative}`)}, import.meta.url).href);`,
|
|
367
|
+
"",
|
|
368
|
+
].join("\n"),
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
directory,
|
|
372
|
+
entrypoint: "server.mjs",
|
|
373
|
+
requiredFiles: [entryRelative],
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
export const WebsiteArtifactProvider = () =>
|
|
378
|
+
Provider.effect(
|
|
379
|
+
WebsiteArtifact,
|
|
380
|
+
Effect.gen(function* () {
|
|
381
|
+
const fs = yield* FileSystem.FileSystem;
|
|
382
|
+
const path = yield* Path.Path;
|
|
383
|
+
return {
|
|
384
|
+
list: () => Effect.succeed([]),
|
|
385
|
+
// Traced dependencies can change independently of the framework's hash.
|
|
386
|
+
diff: () => Effect.succeed({ action: "update" as const }),
|
|
387
|
+
reconcile: Effect.fn(function* ({ id, news }) {
|
|
388
|
+
const name = yield* createPhysicalName({ id, maxLength: 80 });
|
|
389
|
+
const directory = path.join(
|
|
390
|
+
initialCwd,
|
|
391
|
+
".alchemy",
|
|
392
|
+
"prisma-websites",
|
|
393
|
+
name,
|
|
394
|
+
);
|
|
395
|
+
return yield* Effect.scoped(
|
|
396
|
+
Effect.gen(function* () {
|
|
397
|
+
const staged = yield* stageWebsiteArtifact(news);
|
|
398
|
+
const archive = yield* createComputeArchive({
|
|
399
|
+
...staged,
|
|
400
|
+
output: "file",
|
|
401
|
+
});
|
|
402
|
+
return yield* Effect.gen(function* () {
|
|
403
|
+
yield* fs.makeDirectory(directory, { recursive: true });
|
|
404
|
+
const artifactPath = path.join(
|
|
405
|
+
directory,
|
|
406
|
+
`${archive.sha256}.tar.gz`,
|
|
407
|
+
);
|
|
408
|
+
yield* fs.copyFile(archive.path, artifactPath);
|
|
409
|
+
for (const old of yield* fs.readDirectory(directory)) {
|
|
410
|
+
if (
|
|
411
|
+
old !== path.basename(artifactPath) &&
|
|
412
|
+
/^[a-f0-9]{64}\.tar\.gz$/.test(old)
|
|
413
|
+
) {
|
|
414
|
+
yield* fs.remove(path.join(directory, old), {
|
|
415
|
+
force: true,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { directory, artifactPath, hash: archive.sha256 };
|
|
420
|
+
}).pipe(Effect.ensuring(archive.cleanup));
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
423
|
+
}),
|
|
424
|
+
delete: Effect.fn(function* ({ output }) {
|
|
425
|
+
yield* fs.remove(output.directory, { recursive: true, force: true });
|
|
426
|
+
}),
|
|
427
|
+
};
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Namespace from "../../Namespace.ts";
|
|
2
|
+
import { makeFrameworkSite, type FrameworkSiteProps } from "./FrameworkSite.ts";
|
|
3
|
+
|
|
4
|
+
/** Configuration for a Prisma Astro website. */
|
|
5
|
+
export interface AstroProps extends FrameworkSiteProps {
|
|
6
|
+
/** Serializable overrides merged over astro.config.*; the Node target owns adapter. */
|
|
7
|
+
astro?: {
|
|
8
|
+
/** Canonical deployment URL. */
|
|
9
|
+
site?: string;
|
|
10
|
+
/** Deployment base path. */
|
|
11
|
+
base?: string;
|
|
12
|
+
/** Render on demand or prerender every page. @default "server" */
|
|
13
|
+
output?: "server" | "static";
|
|
14
|
+
/** Source directory relative to rootDir. @default "./src" */
|
|
15
|
+
srcDir?: string;
|
|
16
|
+
/** Static passthrough directory. @default "./public" */
|
|
17
|
+
publicDir?: string;
|
|
18
|
+
/** Production output directory. @default "./dist" */
|
|
19
|
+
outDir?: string;
|
|
20
|
+
/** Route trailing-slash policy. */
|
|
21
|
+
trailingSlash?: "always" | "never" | "ignore";
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Astro SSR and prerendered pages on Prisma Compute. The Node target owns the adapter; omit adapter from astro.config.*. Set astro.output to "static" to prerender every page.
|
|
27
|
+
*
|
|
28
|
+
* Native framework development and HMR run without Prisma cloud resources.
|
|
29
|
+
* Apply `Alchemy.remote()` to use the live Compute deployment during dev.
|
|
30
|
+
* Packaging requires the optional `@vercel/nft` peer dependency.
|
|
31
|
+
*
|
|
32
|
+
* ### Creating a Website
|
|
33
|
+
* **Example:** Astro application
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const site = yield* Prisma.Website.Astro("Web", {
|
|
36
|
+
* rootDir: "./app",
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ### Deployment Configuration
|
|
41
|
+
* **Example:** Existing project and custom hostname
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const site = yield* Prisma.Website.Astro("Web", {
|
|
44
|
+
* project,
|
|
45
|
+
* domain: "www.example.com",
|
|
46
|
+
* env: { API_BASE: "https://api.example.com" },
|
|
47
|
+
* compute: { destroyOldDeployment: true },
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* ### Static Output
|
|
52
|
+
* **Example:** Prerendered documentation with a 404 page
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const docs = yield* Prisma.Website.Astro("Docs", {
|
|
55
|
+
* rootDir: "./docs",
|
|
56
|
+
* astro: { output: "static" },
|
|
57
|
+
* assets: { notFoundHandling: "404-page" },
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* @resource
|
|
62
|
+
* @product Website
|
|
63
|
+
*/
|
|
64
|
+
export const Astro = (id: string, props: AstroProps = {}) =>
|
|
65
|
+
makeFrameworkSite(id, props, {
|
|
66
|
+
framework: "@alchemy.run/frontend-frameworks/astro",
|
|
67
|
+
target: "@alchemy.run/frontend-frameworks/astro/node",
|
|
68
|
+
options: {
|
|
69
|
+
astro: { ...props.astro, output: props.astro?.output ?? "server" },
|
|
70
|
+
},
|
|
71
|
+
notFoundHandling: "none",
|
|
72
|
+
}).pipe(Namespace.push(id));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Vite, type ViteProps } from "./Vite.ts";
|
|
2
|
+
|
|
3
|
+
/** Configuration for a client-only Foldkit website. */
|
|
4
|
+
export interface FoldkitProps extends ViteProps {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deploy a Foldkit Vite application to Prisma Compute with SPA routing.
|
|
8
|
+
* The project's Foldkit Vite plugin drives the build; native Vite HMR runs
|
|
9
|
+
* during development without creating cloud resources.
|
|
10
|
+
*
|
|
11
|
+
* ### Creating a Website
|
|
12
|
+
* **Example:** Foldkit application
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const site = yield* Prisma.Website.Foldkit("Web", {
|
|
15
|
+
* rootDir: "./app",
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* ### Multi-Page Routing
|
|
20
|
+
* **Example:** Override the default SPA fallback
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const site = yield* Prisma.Website.Foldkit("Web", {
|
|
23
|
+
* assets: { notFoundHandling: "404-page" },
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @resource
|
|
28
|
+
* @product Website
|
|
29
|
+
*/
|
|
30
|
+
export const Foldkit = (id: string, props: FoldkitProps = {}) =>
|
|
31
|
+
Vite(id, {
|
|
32
|
+
...props,
|
|
33
|
+
assets: { notFoundHandling: "single-page-application", ...props.assets },
|
|
34
|
+
});
|