1import * as FrameworkCore from "../core/index.ts";
2import {
3 Framework,
4 FrameworkError,
5 type DeployTarget,
6 type DeployTargetInput,
7} from "../core/index.ts";
8import type { Adapter } from "@sveltejs/kit";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as Path from "effect/Path";
13import * as NodeFs from "node:fs";
14import * as NodePath from "node:path";
15import { pathToFileURL } from "node:url";
16import type * as ViteModule from "vite";
17import {
18 DEFAULT_VITE_CONFIG_FILES,
19 makeSvelteKitConfigPlugin,
20} from "./UserConfig.ts";
21
22/** The shape of the project's `@sveltejs/kit/vite` module. */
23interface KitViteModule {
24 readonly sveltekit: (
25 config?: Record<string, unknown>,
26 ) => Promise<ViteModule.PluginOption>;
27}
28
29/**
30 * Adapter behavior a deploy target can configure without being consulted on
31 * every write the adapter performs. The names mirror the knobs a static-asset
32 * host exposes (fallback-page generation, the name of the binding the worker
33 * shim serves assets through).
34 */
35export interface SvelteKitAdapterOptions {
36 /**
37 * Name of the static-assets binding the generated worker entry serves
38 * files through.
39 * @default "ASSETS"
40 */
41 readonly assetsBinding?: string | undefined;
42 /**
43 * Fallback-page behavior for requests that match no asset or route:
44 * `"404-page"` writes `404.html`, `"single-page-application"` writes
45 * `index.html` and additionally routes router-level not-founds on
46 * navigation-shaped requests through the fallback (the Cloudflare
47 * target's worker shim defers them to the assets layer).
48 * @default "none"
49 */
50 readonly notFoundHandling?:
51 | "none"
52 | "404-page"
53 | "single-page-application"
54 | undefined;
55 /**
56 * With `notFoundHandling: "404-page"`: `"spa"` renders the app shell as the
57 * fallback, `"plaintext"` writes a plain `Not Found` page.
58 * @default "plaintext"
59 */
60 readonly fallback?: "spa" | "plaintext" | undefined;
61}
62
63/**
64 * The configuration this package assembles from {@link SvelteKitOptions} and
65 * hands to a deploy-target factory (see {@link SvelteKitTargetInput}). The
66 * target treats it as its `DeployTarget.config`; the framework half never
67 * inspects a resolved target's config.
68 */
69export interface SvelteKitTargetConfig {
70 /** Compatibility date for the target's finishing pass. */
71 readonly compatibilityDate?: string | undefined;
72 /** Compatibility flags for the target's finishing pass. */
73 readonly compatibilityFlags?: Array<string> | undefined;
74 /** Adapter behavior (assets binding name, fallback generation). */
75 readonly adapter?: SvelteKitAdapterOptions | undefined;
76 /**
77 * The kit config the framework half was constructed with
78 * ({@link SvelteKitOptions.kit}). Carried on the target config so
79 * wholesale targets that re-run the framework in a child process (the
80 * AWS target — kit's postbuild workers require `cwd === root`) can
81 * reconstruct the framework with the same options.
82 */
83 readonly kit?: Record<string, unknown> | undefined;
84}
85
86/** Inputs the framework passes when asking the target for a kit `Adapter`. */
87export interface SvelteKitAdapterContext {
88 /** Absolute project root. */
89 readonly root: string;
90 /**
91 * Present when the adapter is constructed for the dev server: inputs for
92 * the `platform` its `emulate()` must supply. Absent for production
93 * builds.
94 */
95 readonly dev?:
96 | {
97 /**
98 * Literal `platform.env` overrides — a same-named literal wins over
99 * any target-provided platform value.
100 */
101 readonly env?: Record<string, unknown> | undefined;
102 /**
103 * Target-specific binding specs the dev platform should serve
104 * (opaque to the framework half — e.g. the Cloudflare target takes
105 * `cloudflare-runtime` binding hooks and proxies them onto
106 * `platform.env`).
107 */
108 readonly bindings?: ReadonlyArray<unknown> | undefined;
109 /**
110 * A pre-built runtime-services context the dev platform hosts its
111 * proxy in (opaque to the framework half; the Cloudflare target
112 * takes a `cloudflare-runtime` `Context<RuntimeServices>`). Enables
113 * remote()-lowered bindings; without it the platform is local-only.
114 */
115 readonly services?: unknown;
116 }
117 | undefined;
118}
119
120/** What a target's `adapt()` must record for the framework to continue. */
121export interface SvelteKitAdapterResult {
122 /**
123 * The static-assets directory the adapter wrote (client build +
124 * prerendered pages) — becomes `BuildOutput.clientDirectory`.
125 */
126 readonly dest: string;
127 /**
128 * Absolute path of the on-disk (unbundled) server entry the adapter
129 * generated — passed to the target's `finish` pass as `context.entry`.
130 */
131 readonly workerEntry: string;
132}
133
134/**
135 * A kit `Adapter` that records where it wrote its output. Every SvelteKit
136 * deploy target's adapter must populate `result.current` from `adapt()`.
137 */
138export interface SvelteKitAdapter extends Adapter {
139 readonly result: { current?: SvelteKitAdapterResult | undefined };
140 /**
141 * Release resources the adapter holds for the dev server (e.g. the
142 * Cloudflare target's platform-proxy workerd instance). Called by the
143 * framework after the dev server closes.
144 */
145 readonly dispose?: (() => Promise<void>) | undefined;
146}
147
148/**
149 * A deploy target for SvelteKit: the generic `DeployTarget` seams plus the
150 * one framework-specific hook kit needs — the `Adapter` instance to hand to
151 * `sveltekit(config)`. The target's `finish` pass receives the adapter's
152 * on-disk `workerEntry` (via `context.entry`) and is responsible for turning
153 * it into `BuildOutput.serverModules` for the target runtime.
154 */
155export interface SvelteKitTarget extends DeployTarget<SvelteKitTargetConfig> {
156 /** Produce the kit `Adapter` for one build/dev invocation. */
157 readonly adapter: (context: SvelteKitAdapterContext) => SvelteKitAdapter;
158}
159
160/**
161 * How a deploy target is passed to this package: a `SvelteKitTarget` value, a
162 * factory `(config) => SvelteKitTarget`, or a module specifier resolved from
163 * the *project's* `node_modules` (default-export — or named export `target` —
164 * a value or factory).
165 */
166export type SvelteKitTargetInput = DeployTargetInput<
167 SvelteKitTarget,
168 SvelteKitTargetConfig
169>;
170
171/**
172 * The default deploy target: this package's own Cloudflare Workers target
173 * module (`src/cloudflare.ts`), loaded from the project's dependency tree.
174 */
175export const DEFAULT_TARGET_SPECIFIER =
176 "@alchemy.run/frontend-frameworks/sveltekit/cloudflare";
177
178export interface SvelteKitOptions {
179 /**
180 * Project root. Must also be the process working directory: SvelteKit
181 * resolves its peers (`vite`, `@sveltejs/vite-plugin-svelte`) relative to
182 * `process.cwd()`.
183 * @default process.cwd()
184 */
185 readonly root?: string | undefined;
186 /**
187 * The deploy target the build is produced for. Accepts a target value, a
188 * `(config) => target` factory, or a module specifier string.
189 * @default "@alchemy.run/frontend-frameworks/sveltekit/cloudflare"
190 */
191 readonly target?: SvelteKitTargetInput | undefined;
192 /** Compatibility date forwarded to the target via its config. */
193 readonly compatibilityDate?: string | undefined;
194 /**
195 * Compatibility flags forwarded to the target via its config. SvelteKit's
196 * server graph is built for Node, so on workerd-style targets
197 * `nodejs_compat` is effectively required.
198 * @default ["nodejs_compat"] (applied by the Cloudflare target)
199 */
200 readonly compatibilityFlags?: Array<string> | undefined;
201 /**
202 * SvelteKit configuration for `sveltekit(config)` (kit v3 takes its config
203 * via the Vite plugin; a `svelte.config.js` on disk is an upstream error).
204 * When the project has its own `vite.config.ts`, these options are merged
205 * over the ones passed to the user's `sveltekit(...)` call (integration
206 * options win); construction-time options (`preprocess`, `extensions`,
207 * `compilerOptions`, `vitePlugin`) can only be applied when no user config
208 * file exists. The `adapter` field is always injected by this package.
209 */
210 readonly kit?: Record<string, unknown> | undefined;
211 /** Extra Vite inline config merged into the build/dev config. */
212 readonly vite?: ViteModule.InlineConfig | undefined;
213 /** Adapter behavior forwarded to the target via its config. */
214 readonly adapter?: SvelteKitAdapterOptions | undefined;
215 readonly dev?:
216 | {
217 /** Default dev-server port (overridden by `FrameworkDevOptions.port`). */
218 readonly port?: number | undefined;
219 /**
220 * Literal values exposed on the dev `platform.env`. Applied on top
221 * of the values the target's dev platform serves for
222 * {@link SvelteKitOptions.dev.bindings} — a same-named literal
223 * always wins.
224 */
225 readonly env?: Record<string, unknown> | undefined;
226 /**
227 * Target-specific binding specs the dev platform should serve on
228 * `platform.env`. Opaque to the framework half; the Cloudflare
229 * target accepts `cloudflare-runtime` binding hooks (`Text.local`,
230 * `KvNamespace.local`, …) and proxies them into kit's Node SSR via
231 * a workerd-backed platform proxy.
232 */
233 readonly bindings?: ReadonlyArray<unknown> | undefined;
234 /**
235 * A pre-built runtime-services context the dev platform hosts its
236 * proxy in (opaque to the framework half; the Cloudflare target
237 * takes a `cloudflare-runtime` `Context<RuntimeServices>`). Enables
238 * remote()-lowered bindings; without it the platform is local-only.
239 */
240 readonly services?: unknown;
241 }
242 | undefined;
243}
244
245const fail = (message: string, cause?: unknown) =>
246 new FrameworkError({ framework: "sveltekit", message, cause });
247
248/**
249 * Pick the ESM target out of a package-exports entry (`"./x": "./file.js"` or
250 * `"./x": { import: ..., default: ... }`, possibly nested conditions).
251 */
252export const resolveExportTarget = (entry: unknown): string | undefined => {
253 if (typeof entry === "string") {
254 return entry;
255 }
256 if (entry !== null && typeof entry === "object") {
257 const record = entry as Record<string, unknown>;
258 for (const condition of ["import", "default"]) {
259 if (condition in record) {
260 const target = resolveExportTarget(record[condition]);
261 if (target !== undefined) {
262 return target;
263 }
264 }
265 }
266 }
267 return undefined;
268};
269
270/** Path segment identifying an id inside an `@sveltejs/kit` package. */
271const KIT_PACKAGE_MARKER = "/@sveltejs/kit/";
272
273/**
274 * Re-anchor kit-runtime module ids that were resolved against the wrong
275 * base directory.
276 *
277 * SvelteKit's route manifest stores its fallback root components
278 * (`<kit>/src/runtime/components/{layout,error}.svelte`) as paths RELATIVE
279 * to the Vite root, but resolves them against `process.cwd()`: the dev
280 * server's node loader calls `path.resolve(id)`, and the generated
281 * client/server modules embed `path.relative(absoluteBase, relativeId)`
282 * output (whose `relativeId` is resolved against the cwd). Kit therefore
283 * assumes `process.cwd() === root`. When the dev server is driven
284 * programmatically from another directory — alchemy's dev sidecar, a
285 * monorepo root — those ids re-root outside the project and every SSR
286 * request 500s with `Failed to load url
287 * /@fs/<wrong-base>/.../@sveltejs/kit/src/runtime/components/layout.svelte`.
288 *
289 * This plugin remaps exactly those casualties: an id that points inside an
290 * `@sveltejs/kit` package directory but does NOT exist on disk is re-rooted
291 * onto the project's real kit installation. Ids that resolve to an existing
292 * file are never touched.
293 */
294const makeKitRuntimeRealignPlugin = (
295 kitDirectory: string,
296): ViteModule.Plugin => {
297 const realign = (candidate: string, query: string): string | undefined => {
298 if (NodeFs.existsSync(candidate)) {
299 return undefined;
300 }
301 const normalized = candidate.replaceAll("\\", "/");
302 const index = normalized.lastIndexOf(KIT_PACKAGE_MARKER);
303 if (index === -1) {
304 return undefined;
305 }
306 const remapped = NodePath.join(
307 kitDirectory,
308 normalized.slice(index + KIT_PACKAGE_MARKER.length),
309 );
310 return NodeFs.existsSync(remapped) ? remapped + query : undefined;
311 };
312 return {
313 name: "distilled-sveltekit-kit-runtime-realign",
314 enforce: "pre",
315 resolveId(source, importer) {
316 if (!source.includes("@sveltejs")) {
317 return undefined;
318 }
319 const queryIndex = source.indexOf("?");
320 const bare = queryIndex === -1 ? source : source.slice(0, queryIndex);
321 const query = queryIndex === -1 ? "" : source.slice(queryIndex);
322 if (bare.startsWith("/@fs/")) {
323 // Vite serves absolute fs paths as `/@fs/<path>` (`/@fs/C:/...` on
324 // Windows — strip the extra leading slash before a drive letter).
325 const fsPath = bare.slice("/@fs".length);
326 return realign(
327 /^\/[A-Za-z]:[/\\]/.test(fsPath) ? fsPath.slice(1) : fsPath,
328 query,
329 );
330 }
331 if (NodePath.isAbsolute(bare)) {
332 return realign(bare, query);
333 }
334 if (bare.startsWith(".") && importer !== undefined) {
335 const importerPath = importer.split("?")[0] ?? importer;
336 return realign(
337 NodePath.resolve(NodePath.dirname(importerPath), bare),
338 query,
339 );
340 }
341 return undefined;
342 },
343 };
344};
345
346/**
347 * Build the `Framework` service implementation for a SvelteKit project.
348 *
349 * - `build` resolves the deploy target, then either delegates wholesale
350 * (`target.build`) or runs kit's production build via programmatic Vite
351 * (`createBuilder().buildApp()`) with the target-provided adapter and hands
352 * the adapter's on-disk worker entry to the target's `finish` pass, which
353 * produces the final `BuildOutput.serverModules`. A project-owned
354 * `vite.config.ts` loads natively (the user's `sveltekit(...)` call and
355 * plugins apply; the target's adapter is injected into it — see
356 * `UserConfig.ts`); without one, the config is fully programmatic.
357 * - `dev` runs kit's own Vite dev server (Node SSR, full HMR) with the
358 * target-provided adapter supplying the `platform` emulation.
359 *
360 * This module is target-agnostic: everything Cloudflare-specific lives in
361 * `@alchemy.run/frontend-frameworks/sveltekit/cloudflare`.
362 */
363export const make: (
364 options?: SvelteKitOptions,
365) => Effect.Effect<
366 Framework["Service"],
367 never,
368 FileSystem.FileSystem | Path.Path
369> = Effect.fnUntraced(function* (options?: SvelteKitOptions) {
370 const fs = yield* FileSystem.FileSystem;
371 const path = yield* Path.Path;
372 const baseRoot = options?.root ?? (yield* Effect.sync(() => process.cwd()));
373
374 const targetConfig: SvelteKitTargetConfig = {
375 compatibilityDate: options?.compatibilityDate,
376 compatibilityFlags: options?.compatibilityFlags,
377 adapter: options?.adapter,
378 kit: options?.kit,
379 };
380
381 const resolveTarget = (root: string) =>
382 FrameworkCore.resolveDeployTarget<SvelteKitTarget, SvelteKitTargetConfig>(
383 root,
384 options?.target ?? DEFAULT_TARGET_SPECIFIER,
385 targetConfig,
386 ).pipe(Effect.mapError((error) => fail(error.message, error.cause)));
387
388 const requireAdapterHook = (target: SvelteKitTarget) =>
389 typeof target.adapter === "function"
390 ? Effect.succeed(target)
391 : Effect.fail(
392 fail(
393 `The resolved "${target.platform}" deploy target does not implement the SvelteKit ` +
394 "adapter hook (`adapter(context)`) and has no wholesale `build`",
395 ),
396 );
397
398 const loadVite = (root: string) =>
399 FrameworkCore.loadProjectModule<typeof ViteModule>(root, "vite").pipe(
400 Effect.mapError((error) =>
401 fail("Failed to load the project's Vite", error.cause),
402 ),
403 );
404
405 const resolveKitDirectory = (root: string) =>
406 FrameworkCore.resolveProjectPackageDirectory(root, "@sveltejs/kit").pipe(
407 Effect.mapError((error) =>
408 fail("Failed to locate the project's @sveltejs/kit", error.cause),
409 ),
410 );
411
412 // `@sveltejs/kit`'s `./vite` export carries only an `import` condition, so
413 // framework-core's `createRequire().resolve` dance cannot resolve it.
414 // Resolve the package directory (via its universally-exported
415 // `./package.json`) and walk the exports map instead.
416 const loadKitViteModule = Effect.fn(function* (root: string) {
417 const kitDirectory = yield* resolveKitDirectory(root);
418 const manifest = yield* fs
419 .readFileString(path.join(kitDirectory, "package.json"))
420 .pipe(
421 Effect.mapError((error) =>
422 fail("Failed to read @sveltejs/kit's package.json", error),
423 ),
424 );
425 const parsed = yield* Effect.try({
426 try: () => JSON.parse(manifest) as { exports?: Record<string, unknown> },
427 catch: (error) =>
428 fail("Failed to parse @sveltejs/kit's package.json", error),
429 });
430 const target = resolveExportTarget(parsed.exports?.["./vite"]);
431 if (target === undefined) {
432 return yield* Effect.fail(
433 fail(
434 `The project's @sveltejs/kit (${kitDirectory}) has no "./vite" export`,
435 ),
436 );
437 }
438 return yield* Effect.tryPromise({
439 try: async () =>
440 (await import(
441 /* @vite-ignore */ pathToFileURL(path.join(kitDirectory, target)).href
442 )) as KitViteModule,
443 catch: (error) =>
444 fail("Failed to load the project's @sveltejs/kit/vite", error),
445 });
446 });
447
448 const loadKitPlugins = Effect.fn(function* (root: string, adapter: Adapter) {
449 const kitVite = yield* loadKitViteModule(root);
450 return yield* Effect.tryPromise({
451 try: async () => await kitVite.sveltekit({ ...options?.kit, adapter }),
452 catch: (error) => fail("Failed to construct the SvelteKit plugin", error),
453 });
454 });
455
456 const findViteConfigFile = Effect.fnUntraced(function* (root: string) {
457 for (const name of DEFAULT_VITE_CONFIG_FILES) {
458 const exists = yield* fs
459 .exists(path.join(root, name))
460 .pipe(Effect.catchTag("PlatformError", () => Effect.succeed(false)));
461 if (exists) {
462 return name;
463 }
464 }
465 return undefined;
466 });
467
468 /**
469 * Assemble the Vite inline config for a build/dev invocation.
470 *
471 * - Project has its own Vite config file: the file loads natively
472 * (`configFile` is left to Vite's discovery — the user's plugins,
473 * including their `sveltekit(...)` call, all apply) and the inline
474 * config only injects `makeSvelteKitConfigPlugin`, which installs the
475 * deploy target's adapter (and `options.kit` overrides) into the
476 * user's kit config before kit processes it.
477 * - No config file on disk: internal fallback — `configFile: false` with
478 * the `sveltekit()` plugin constructed programmatically from
479 * `options.kit` + the target's adapter.
480 */
481 const resolveViteConfig = Effect.fn(function* (
482 root: string,
483 adapter: Adapter,
484 ) {
485 // Kit resolves the manifest's `..`-relative fallback-component ids
486 // against `process.cwd()` instead of the Vite root; the realign plugin
487 // repairs them when the two differ (see makeKitRuntimeRealignPlugin).
488 const realign = makeKitRuntimeRealignPlugin(
489 yield* resolveKitDirectory(root),
490 );
491 const userConfigFile = yield* findViteConfigFile(root);
492 if (userConfigFile === undefined) {
493 const plugins = yield* loadKitPlugins(root, adapter);
494 return {
495 root,
496 configFile: false,
497 // Default to warn: rolldown-vite's native progress reporter
498 // ("transforming...") writes straight to the fd, which corrupts
499 // hosting-process reporters (e.g. alchemy-test) that can only
500 // intercept JS-level writers.
501 logLevel: "warn",
502 ...options?.vite,
503 plugins: [realign, ...(options?.vite?.plugins ?? []), plugins],
504 } satisfies ViteModule.InlineConfig;
505 }
506 return {
507 root,
508 logLevel: "warn",
509 ...options?.vite,
510 plugins: [
511 realign,
512 ...(options?.vite?.plugins ?? []),
513 makeSvelteKitConfigPlugin({ adapter, kit: options?.kit }),
514 ],
515 } satisfies ViteModule.InlineConfig;
516 });
517
518 const build: Framework["Service"]["build"] = Effect.fn(
519 function* (buildOptions) {
520 const root = buildOptions?.root ?? baseRoot;
521 const target = yield* resolveTarget(root);
522 const targetContext = {
523 root,
524 framework: "sveltekit",
525 env: buildOptions?.env,
526 };
527
528 // Wholesale build takeover (targets that own the entire pipeline).
529 if (target.build !== undefined) {
530 return yield* target.build(targetContext).pipe(
531 Effect.mapError((error) => fail(error.message, error.cause)),
532 Effect.provideService(FileSystem.FileSystem, fs),
533 Effect.provideService(Path.Path, path),
534 );
535 }
536
537 yield* requireAdapterHook(target);
538 const adapter = target.adapter({ root });
539 const vite = yield* loadVite(root);
540 const config = yield* resolveViteConfig(root, adapter);
541
542 yield* Effect.tryPromise({
543 try: async () => {
544 const builder = await vite.createBuilder(config, null);
545 await builder.buildApp();
546 },
547 catch: (error) => fail("Failed to build", error),
548 });
549
550 const result = adapter.result.current;
551 if (result === undefined) {
552 return yield* Effect.fail(
553 fail("The SvelteKit build completed without running the adapter"),
554 );
555 }
556
557 const output: FrameworkCore.BuildOutput = {
558 distDirectory: path.resolve(root, "dist"),
559 clientDirectory: result.dest,
560 serverModules: undefined,
561 externalWorkspaces: new Set<string>(),
562 };
563
564 // The target's finishing pass turns the adapter's on-disk worker entry
565 // into runtime-ready server modules (e.g. the Cloudflare target's
566 // rolldown re-bundle for workerd).
567 return yield* FrameworkCore.applyDeployTargetFinish(target, output, {
568 ...targetContext,
569 entry: result.workerEntry,
570 }).pipe(
571 Effect.mapError((error) => fail(error.message, error.cause)),
572 Effect.provideService(FileSystem.FileSystem, fs),
573 Effect.provideService(Path.Path, path),
574 );
575 },
576 );
577
578 const devInProcess: Framework["Service"]["dev"] = Effect.fn(
579 function* (devOptions) {
580 const root = devOptions?.root ?? baseRoot;
581 const target = yield* resolveTarget(root);
582 yield* requireAdapterHook(target);
583 const adapter = target.adapter({
584 root,
585 dev: {
586 env: options?.dev?.env,
587 bindings: options?.dev?.bindings,
588 services: options?.dev?.services,
589 },
590 });
591 // Registered before the server is acquired so it runs after the server
592 // closes (finalizers are LIFO): the dev platform (e.g. the Cloudflare
593 // target's platform proxy) outlives the last in-flight request.
594 yield* Effect.addFinalizer(() =>
595 Effect.promise(async () => {
596 try {
597 await adapter.dispose?.();
598 } catch {
599 // dev-platform teardown is best-effort
600 }
601 }),
602 );
603 const vite = yield* loadVite(root);
604 const config = yield* resolveViteConfig(root, adapter);
605 // `port: 0` (true OS-assigned) on Vite >= 8.2.1, probed ephemeral port
606 // on older Vite — see `resolveViteDevPort`.
607 const port = yield* FrameworkCore.resolveViteDevPort(
608 vite.version,
609 devOptions?.port ?? options?.dev?.port,
610 );
611 const host = devOptions?.host;
612
613 const server = yield* Effect.acquireRelease(
614 Effect.tryPromise({
615 try: async () => {
616 const server = await vite.createServer({
617 ...config,
618 server: {
619 ...config.server,
620 port,
621 ...(host !== undefined ? { host } : undefined),
622 },
623 });
624 return await server.listen();
625 },
626 catch: (error) => fail("Failed to start the dev server", error),
627 }),
628 (server) => Effect.promise(async () => await server.close()),
629 );
630 const url = server.resolvedUrls?.local[0];
631 if (url === undefined) {
632 return yield* Effect.fail(
633 fail("Could not determine the dev server URL"),
634 );
635 }
636 return { url };
637 },
638 );
639
640 // Constructing the SvelteKit plugin loads the project's `@sveltejs/kit`
641 // module graph, which misbehaves when several projects load it
642 // concurrently in one process (partially-initialized module bindings —
643 // `Cannot access 'kit_options' before initialization`). A process hosting
644 // many sites' dev servers (the alchemy dev sidecar) hits exactly that, so
645 // run the dev server in a dedicated child process instead (see
646 // core/DevChild.ts). Inside that child — or when the options cannot cross
647 // the process boundary (deploy-target VALUES, live vite plugins from the
648 // e2e harness) — the in-process path runs directly.
649 const dev: Framework["Service"]["dev"] = (devOptions) => {
650 if (
651 FrameworkCore.isInsideDevChild() ||
652 !FrameworkCore.isJsonSerializable(options)
653 ) {
654 return devInProcess(devOptions);
655 }
656 const root = devOptions?.root ?? baseRoot;
657 const port = devOptions?.port ?? options?.dev?.port;
658 return FrameworkCore.runDevChild({
659 framework: "sveltekit",
660 module: "@alchemy.run/frontend-frameworks/sveltekit",
661 callerUrl: import.meta.url,
662 rootDir: root,
663 makeOptions: { ...options, root },
664 devOptions: {
665 root,
666 ...(port !== undefined ? { port } : {}),
667 ...(devOptions?.host !== undefined ? { host: devOptions.host } : {}),
668 },
669 });
670 };
671
672 return Framework.of({ build, dev });
673});
674
675/**
676 * A `Layer` providing framework-core's `Framework` service for a SvelteKit
677 * project — the fully-typed entrypoint for `e2e.config.ts` (harness form 4)
678 * and alchemy's `Website.SvelteKit` composite.
679 */
680export const layer = (
681 options?: SvelteKitOptions,
682): Layer.Layer<Framework, never, FileSystem.FileSystem | Path.Path> =>
683 Layer.effect(Framework, make(options));