@thomasfosterau/effect-sveltekit 0.0.0 → 0.1.1

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 (40) hide show
  1. package/README.md +36 -13
  2. package/dist/Hooks.d.ts +2 -2
  3. package/dist/Hooks.js +2 -1
  4. package/dist/Hooks.js.map +1 -1
  5. package/dist/adapter/assets.d.ts +33 -0
  6. package/dist/adapter/assets.js +87 -0
  7. package/dist/adapter/assets.js.map +1 -0
  8. package/dist/adapter/handler.d.ts +75 -10
  9. package/dist/adapter/handler.js +73 -44
  10. package/dist/adapter/handler.js.map +1 -1
  11. package/dist/adapter/index.d.ts +15 -0
  12. package/dist/adapter/index.js +43 -5
  13. package/dist/adapter/index.js.map +1 -1
  14. package/dist/app-runtime.d.ts +24 -7
  15. package/dist/app-runtime.js +13 -10
  16. package/dist/app-runtime.js.map +1 -1
  17. package/dist/internal/remote-run.d.ts +17 -0
  18. package/dist/internal/remote-run.js +39 -0
  19. package/dist/internal/remote-run.js.map +1 -0
  20. package/dist/internal/remote-validators.js +23 -0
  21. package/dist/internal/remote-validators.js.map +1 -0
  22. package/dist/internal/route-params.d.ts +23 -0
  23. package/dist/remote.server.d.ts +105 -33
  24. package/dist/remote.server.js +96 -41
  25. package/dist/remote.server.js.map +1 -1
  26. package/dist/request-runtime.d.ts +139 -0
  27. package/dist/request-runtime.js +186 -0
  28. package/dist/request-runtime.js.map +1 -0
  29. package/dist/server/Endpoint.d.ts +35 -4
  30. package/dist/server/Endpoint.js +14 -5
  31. package/dist/server/Endpoint.js.map +1 -1
  32. package/dist/server.d.ts +8 -6
  33. package/dist/server.js +2 -1
  34. package/dist/server.js.map +1 -1
  35. package/dist/transport.d.ts +43 -5
  36. package/dist/transport.js +36 -6
  37. package/dist/transport.js.map +1 -1
  38. package/dist/universal.d.ts +2 -1
  39. package/dist/universal.js.map +1 -1
  40. package/package.json +2 -2
package/README.md CHANGED
@@ -20,13 +20,13 @@ npm install @effect/platform-node@beta
20
20
 
21
21
  ## Entry points
22
22
 
23
- | Import | What it provides |
24
- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
- | `@thomasfosterau/effect-sveltekit` | universal helpers (`load`, the `Remote` call namespace, `transport`, `sveltekitConfig`, `reroute`) |
26
- | `…/server` | server wrappers (`serverLoad`, `action`, `actions`, `handle`, `endpoint`), app runtimes (`makeServerRuntime` / `makeUniversalRuntime`), `RemoteFunction` |
27
- | `…/client` | browser-only `Navigation` / `State` / `Remote` namespaces and `useRemoteQuery` |
28
- | `…/vite` | the `effectKit()` Vite plugin (auto-wrap bare-Effect route exports) |
29
- | `…/adapter` | the Effect HTTP deployment adapter |
23
+ | Import | What it provides |
24
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
25
+ | `@thomasfosterau/effect-sveltekit` | universal helpers (`load`, the `Remote` call namespace, `transport`, `sveltekitConfig`, `reroute`) |
26
+ | `…/server` | server wrappers (`serverLoad`, `action`, `actions`, `handle`, `endpoint`), app runtimes (`makeServerRuntime` / `makeUniversalRuntime`), the per-request runtime seam (`makeRuntimeAdapter` / `RequestRuntime`), `RemoteFunction` |
27
+ | `…/client` | browser-only `Navigation` / `State` / `Remote` namespaces and `useRemoteQuery` |
28
+ | `…/vite` | the `effectKit()` Vite plugin (auto-wrap bare-Effect route exports) |
29
+ | `…/adapter` | the Effect HTTP deployment adapter |
30
30
 
31
31
  ## Example
32
32
 
@@ -48,15 +48,38 @@ export const load = (event) => Effect.succeed({ id: event.params.id });
48
48
 
49
49
  - **Effects as route exports** — explicit wrappers, app-wide runtimes (services
50
50
  built once per process), or the `effectKit()` plugin.
51
- - **Remote functions** — define with `RemoteFunction.*`, consume via the
52
- `Remote` namespace (`Remote.callQuery`, `Remote.liveQueryStream`, …);
53
- `useRemoteQuery` bridges a reactive remote resource to an `AsyncResult`.
54
- - **`transport` ⇄ Effect Schema** and **`$env` ⇄ Effect `Config`**.
51
+ - **Bring-your-own per-request runtime** — `makeRuntimeAdapter(provider, opts)`
52
+ binds the server wrappers to a `RequestRuntime` provider, so the handler
53
+ effect's services are your own application catalogue (not `SvelteKitServices`).
54
+ Adopt a runtime the consumer stashed on `event.locals`
55
+ (`RequestRuntime.fromLocals`, resolved per call so a mid-request rebuild is
56
+ honoured), let the package build + dispose one per request
57
+ (`RequestRuntime.fromLayerPerRequest`, with `afterResponse` to route disposal
58
+ through Cloudflare Workers' `ctx.waitUntil`), or reuse the build-once model
59
+ (`fromManagedRuntime` / `fromLayer`). `RemoteFunction.withRuntime(provider)`
60
+ gives remote functions the same seam.
61
+ - **Pluggable boundary error translation** — pass `translateError` to
62
+ `makeRuntimeAdapter`, `makeServerRuntime`, or `RemoteFunction.withRuntime` to
63
+ map a failed `Cause` to SvelteKit control flow (`error(404)`, `redirect()`,
64
+ remote-form `invalid()`) instead of a generic 500.
65
+ - **Remote functions** — define with `RemoteFunction.*` (a bare Effect `Schema`
66
+ validator is accepted and auto-wrapped), consume via the `Remote` namespace
67
+ (`Remote.callQuery`, `Remote.liveQueryStream`, …); `useRemoteQuery` bridges a
68
+ reactive remote resource to an `AsyncResult`. `RemoteFunction.withRuntime` also
69
+ takes a `formError` hook to map a domain error into SvelteKit field issues.
70
+ - **`transport` ⇄ Effect Schema** (`Hooks.transport` / `transporter`, plus
71
+ `Hooks.markerTransporter` for non-round-tripping wrappers) and **`$env` ⇄
72
+ Effect `Config`**.
55
73
  - **Client navigation/state** — the `Navigation` (`goto`/`invalidate`/… plus
56
74
  `beforeNavigate`/`afterNavigate`/`onNavigate` streams) and `State` (`page`)
57
75
  namespaces.
58
- - **Effect deployment adapter** — a production HTTP server on
59
- `effect/unstable/http` + `@effect/platform-node`.
76
+ - **Effect deployment adapter** — the generated app is a platform-neutral
77
+ Effect `HttpApp` (`makeHandlerWith`): serve it as a production Node HTTP server
78
+ (`effect/unstable/http` + `@effect/platform-node`, the default) or as a Web
79
+ `fetch` handler on Cloudflare Workers (`effectAdapter({ target: "fetch" })` /
80
+ `toWebHandler`). Asset serving is a pluggable `AssetSource`
81
+ (`AssetSource.fileSystem` for Node, `AssetSource.cloudflareAssets(env.ASSETS)`
82
+ for Workers, or any custom source — R2, KV, a CDN passthrough).
60
83
 
61
84
  See the [monorepo README](https://github.com/thomasfosterau/effect-svelte#readme)
62
85
  for the full API and a runnable SvelteKit example.
package/dist/Hooks.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { transport, transporter } from "./transport.js";
1
+ import { MarkerTransporterOptions, TransportEntry, markerTransporter, transport, transporter } from "./transport.js";
2
2
  import { Effect } from "effect";
3
3
  import * as HttpClient from "effect/unstable/http/HttpClient";
4
4
  import { Reroute } from "@sveltejs/kit";
5
5
 
6
6
  //#region src/Hooks.d.ts
7
7
  declare namespace Hooks_d_exports {
8
- export { reroute, transport, transporter };
8
+ export { MarkerTransporterOptions, TransportEntry, markerTransporter, reroute, transport, transporter };
9
9
  }
10
10
  /**
11
11
  * Wraps an Effect-based function as the SvelteKit universal
package/dist/Hooks.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { defaultUniversalRuntime } from "./internal/default-runtime.js";
3
- import { transport, transporter } from "./transport.js";
3
+ import { markerTransporter, transport, transporter } from "./transport.js";
4
4
  import "effect";
5
5
  //#region src/Hooks.ts
6
6
  var Hooks_exports = /* @__PURE__ */ __exportAll({
7
+ markerTransporter: () => markerTransporter,
7
8
  reroute: () => reroute,
8
9
  transport: () => transport,
9
10
  transporter: () => transporter
package/dist/Hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Hooks.js","names":[],"sources":["../src/Hooks.ts"],"sourcesContent":["/**\n * Effect helpers for SvelteKit's universal hooks (`src/hooks.ts`), exposed as\n * the `Hooks` namespace of `@thomasfosterau/effect-sveltekit`.\n *\n * The `reroute` hook is expressed as an Effect (running on the universal\n * runtime, with an `HttpClient` backed by the event's `fetch`), and the\n * universal `transport` hook is built from Effect schemas via {@link transport}\n * / {@link transporter}.\n *\n * @example\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from '@thomasfosterau/effect-sveltekit';\n * import { Effect, Schema } from 'effect';\n *\n * export const reroute = Hooks.reroute((event) =>\n * Effect.succeed(event.url.pathname === '/old' ? '/new' : undefined),\n * );\n *\n * export const transport = Hooks.transport({\n * Option: Schema.Option(Schema.Number),\n * Date: Schema.Date,\n * });\n * ```\n */\n\nimport type { Reroute } from \"@sveltejs/kit\";\nimport { Effect } from \"effect\";\nimport type * as HttpClient from \"effect/unstable/http/HttpClient\";\nimport { defaultUniversalRuntime } from \"./internal/default-runtime.js\";\n\n/**\n * Wraps an Effect-based function as the SvelteKit universal\n * [`reroute`](https://svelte.dev/docs/kit/hooks#Universal-hooks-reroute) hook,\n * which rewrites the URL before routing. The effect runs on both the server and\n * the client and gets an `HttpClient` backed by the event's `fetch`.\n *\n * @example\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from '@thomasfosterau/effect-sveltekit';\n * import { Effect } from 'effect';\n *\n * export const reroute = Hooks.reroute((event) =>\n * Effect.succeed(event.url.pathname === '/old' ? '/new' : undefined),\n * );\n * ```\n */\nexport function reroute(\n fn: (event: {\n url: URL;\n fetch: typeof globalThis.fetch;\n }) => Effect.Effect<string | void, unknown, HttpClient.HttpClient>,\n): Reroute {\n return (event) => defaultUniversalRuntime().runPromise(event.fetch, fn(event));\n}\n\n/**\n * The universal `transport` hook built from Effect schemas: `transport`\n * assembles a whole `transport` object from a record of schemas, and\n * `transporter` builds a single entry. See {@link transport} for details.\n */\nexport { transport, transporter } from \"./transport.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,QACd,IAIS;CACT,QAAQ,UAAU,wBAAwB,EAAE,WAAW,MAAM,OAAO,GAAG,KAAK,CAAC;AAC/E"}
1
+ {"version":3,"file":"Hooks.js","names":[],"sources":["../src/Hooks.ts"],"sourcesContent":["/**\n * Effect helpers for SvelteKit's universal hooks (`src/hooks.ts`), exposed as\n * the `Hooks` namespace of `@thomasfosterau/effect-sveltekit`.\n *\n * The `reroute` hook is expressed as an Effect (running on the universal\n * runtime, with an `HttpClient` backed by the event's `fetch`), and the\n * universal `transport` hook is built from Effect schemas via {@link transport}\n * / {@link transporter}.\n *\n * @example\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from '@thomasfosterau/effect-sveltekit';\n * import { Effect, Schema } from 'effect';\n *\n * export const reroute = Hooks.reroute((event) =>\n * Effect.succeed(event.url.pathname === '/old' ? '/new' : undefined),\n * );\n *\n * export const transport = Hooks.transport({\n * Option: Schema.Option(Schema.Number),\n * Date: Schema.Date,\n * });\n * ```\n */\n\nimport type { Reroute } from \"@sveltejs/kit\";\nimport { Effect } from \"effect\";\nimport type * as HttpClient from \"effect/unstable/http/HttpClient\";\nimport { defaultUniversalRuntime } from \"./internal/default-runtime.js\";\n\n/**\n * Wraps an Effect-based function as the SvelteKit universal\n * [`reroute`](https://svelte.dev/docs/kit/hooks#Universal-hooks-reroute) hook,\n * which rewrites the URL before routing. The effect runs on both the server and\n * the client and gets an `HttpClient` backed by the event's `fetch`.\n *\n * @example\n * ```ts\n * // src/hooks.ts\n * import { Hooks } from '@thomasfosterau/effect-sveltekit';\n * import { Effect } from 'effect';\n *\n * export const reroute = Hooks.reroute((event) =>\n * Effect.succeed(event.url.pathname === '/old' ? '/new' : undefined),\n * );\n * ```\n */\nexport function reroute(\n fn: (event: {\n url: URL;\n fetch: typeof globalThis.fetch;\n }) => Effect.Effect<string | void, unknown, HttpClient.HttpClient>,\n): Reroute {\n return (event) => defaultUniversalRuntime().runPromise(event.fetch, fn(event));\n}\n\n/**\n * The universal `transport` hook built from Effect schemas: `transport`\n * assembles a whole `transport` object from a record of entries, `transporter`\n * builds a single round-tripping entry from a `Schema.Codec`, and\n * `markerTransporter` builds a deliberately-lossy entry for a non-POJO wrapper\n * whose contents should not round-trip. See {@link transport} for details.\n */\nexport {\n transport,\n transporter,\n markerTransporter,\n type MarkerTransporterOptions,\n type TransportEntry,\n} from \"./transport.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,QACd,IAIS;CACT,QAAQ,UAAU,wBAAwB,EAAE,WAAW,MAAM,OAAO,GAAG,KAAK,CAAC;AAC/E"}
@@ -0,0 +1,33 @@
1
+ import { Effect, Option } from "effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
5
+ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
6
+
7
+ //#region src/adapter/assets.d.ts
8
+ /**
9
+ * A source of static assets. `serveAsset` returns the response for an asset
10
+ * request, or `Option.none()` when the request is not an asset (so the SSR core
11
+ * continues to server-side rendering). `R` is the set of services the source
12
+ * needs (e.g. `FileSystem | Path | HttpPlatform` for {@link fileSystem}, or
13
+ * `never` for {@link cloudflareAssets}).
14
+ */
15
+ interface AssetSource<R = never> {
16
+ readonly serveAsset: (request: Request, pathname: string) => Effect.Effect<Option.Option<HttpServerResponse.HttpServerResponse>, never, R>;
17
+ }
18
+ /** The services a file-system {@link AssetSource} requires. */
19
+ type FileSystemAssetServices = FileSystem.FileSystem | Path.Path | HttpPlatform.HttpPlatform;
20
+ /** The {@link AssetSource} constructors, grouped for the adapter API. */
21
+ declare const AssetSource: {
22
+ fileSystem: (config: {
23
+ /** Absolute path of the build output directory (contains `client/`, `prerendered/`). */readonly dir: string; /** The configured `manifest.appPath` — its `immutable/` subtree gets immutable caching. */
24
+ readonly appPath: string; /** The set of prerendered paths exported by the build output. */
25
+ readonly prerendered: ReadonlySet<string>;
26
+ }) => AssetSource<FileSystemAssetServices>;
27
+ cloudflareAssets: (assets: {
28
+ fetch: (request: Request) => Promise<Response> | Response;
29
+ }) => AssetSource;
30
+ };
31
+ //#endregion
32
+ export { AssetSource, FileSystemAssetServices };
33
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1,87 @@
1
+ import { Effect, Option } from "effect";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import "effect/unstable/http/HttpPlatform";
5
+ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
6
+ //#region src/adapter/assets.ts
7
+ /**
8
+ * Pluggable asset serving for the Effect SvelteKit adapter.
9
+ *
10
+ * The SSR core ({@link makeHandler}) is platform-neutral: it falls through to an
11
+ * injected {@link AssetSource} for static client assets and prerendered pages,
12
+ * rather than reading the file system directly. This lets the same SvelteKit
13
+ * `HttpApp` run on a Node server (assets from disk) or on Cloudflare Workers
14
+ * (assets from the `env.ASSETS` binding, or any custom source — R2, KV, a CDN
15
+ * passthrough).
16
+ *
17
+ * Ship several sources as equals and let the adapter (or the caller) pick:
18
+ *
19
+ * - {@link fileSystem} — serve `client/` + `prerendered/` from disk (Node
20
+ * default). Requires `FileSystem` / `Path` / `HttpPlatform`.
21
+ * - {@link cloudflareAssets} — serve from a Workers static-assets binding
22
+ * (`env.ASSETS`). Requires nothing.
23
+ */
24
+ const IMMUTABLE_HEADERS = { "cache-control": "public, max-age=31536000, immutable" };
25
+ /**
26
+ * Serve static client assets and prerendered pages from a build directory on
27
+ * disk (the Node default). `dir` contains `client/` and `prerendered/`.
28
+ */
29
+ const fileSystem = (config) => ({ serveAsset: (request, pathname) => Effect.gen(function* () {
30
+ if (request.method !== "GET" && request.method !== "HEAD") return Option.none();
31
+ const path = yield* Path.Path;
32
+ const fs = yield* FileSystem.FileSystem;
33
+ const clientDir = path.join(config.dir, "client");
34
+ const prerenderedDir = path.join(config.dir, "prerendered");
35
+ const immutablePrefix = `/${config.appPath}/immutable/`;
36
+ /**
37
+ * Serve a file from `root` if `pathname` resolves to an existing file
38
+ * inside it (rejecting path traversal). `Option.none()` otherwise.
39
+ */
40
+ const serveFile = (root, candidate, headers) => Effect.gen(function* () {
41
+ const filePath = path.join(root, candidate);
42
+ const relative = path.relative(root, filePath);
43
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return Option.none();
44
+ const stat = yield* fs.stat(filePath).pipe(Effect.option);
45
+ if (Option.isNone(stat) || stat.value.type !== "File") return Option.none();
46
+ const response = yield* HttpServerResponse.file(filePath, { headers }).pipe(Effect.orDie);
47
+ return Option.some(response);
48
+ });
49
+ const asset = yield* serveFile(clientDir, pathname, pathname.startsWith(immutablePrefix) ? IMMUTABLE_HEADERS : void 0);
50
+ if (Option.isSome(asset)) return asset;
51
+ if (config.prerendered.has(pathname) || config.prerendered.has(pathname + "/")) {
52
+ const candidates = pathname === "/" ? ["/index.html"] : pathname.endsWith("/") ? [`${pathname}index.html`, `${pathname.slice(0, -1)}.html`] : [
53
+ pathname,
54
+ `${pathname}.html`,
55
+ `${pathname}/index.html`
56
+ ];
57
+ for (const candidate of candidates) {
58
+ const page = yield* serveFile(prerenderedDir, candidate);
59
+ if (Option.isSome(page)) return page;
60
+ }
61
+ }
62
+ return Option.none();
63
+ }) });
64
+ /**
65
+ * Serve static assets from a Cloudflare Workers static-assets binding (commonly
66
+ * `env.ASSETS`). The binding's `fetch` resolves client assets and prerendered
67
+ * pages uploaded by the deployment; a 404 from it means "not an asset", so the
68
+ * SSR core continues to server-side rendering.
69
+ *
70
+ * This is one provided convenience, not the assumption: pass any object with a
71
+ * `fetch` (R2, KV, a CDN passthrough) or a custom {@link AssetSource}.
72
+ */
73
+ const cloudflareAssets = (assets) => ({ serveAsset: (request) => Effect.gen(function* () {
74
+ if (request.method !== "GET" && request.method !== "HEAD") return Option.none();
75
+ const response = yield* Effect.promise(async () => assets.fetch(request));
76
+ if (response.status === 404) return Option.none();
77
+ return Option.some(HttpServerResponse.fromWeb(response));
78
+ }) });
79
+ /** The {@link AssetSource} constructors, grouped for the adapter API. */
80
+ const AssetSource = {
81
+ fileSystem,
82
+ cloudflareAssets
83
+ };
84
+ //#endregion
85
+ export { AssetSource };
86
+
87
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../src/adapter/assets.ts"],"sourcesContent":["/**\n * Pluggable asset serving for the Effect SvelteKit adapter.\n *\n * The SSR core ({@link makeHandler}) is platform-neutral: it falls through to an\n * injected {@link AssetSource} for static client assets and prerendered pages,\n * rather than reading the file system directly. This lets the same SvelteKit\n * `HttpApp` run on a Node server (assets from disk) or on Cloudflare Workers\n * (assets from the `env.ASSETS` binding, or any custom source — R2, KV, a CDN\n * passthrough).\n *\n * Ship several sources as equals and let the adapter (or the caller) pick:\n *\n * - {@link fileSystem} — serve `client/` + `prerendered/` from disk (Node\n * default). Requires `FileSystem` / `Path` / `HttpPlatform`.\n * - {@link cloudflareAssets} — serve from a Workers static-assets binding\n * (`env.ASSETS`). Requires nothing.\n */\n\nimport { Effect, Option } from \"effect\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as Path from \"effect/Path\";\nimport * as HttpPlatform from \"effect/unstable/http/HttpPlatform\";\nimport * as HttpServerResponse from \"effect/unstable/http/HttpServerResponse\";\n\n/**\n * A source of static assets. `serveAsset` returns the response for an asset\n * request, or `Option.none()` when the request is not an asset (so the SSR core\n * continues to server-side rendering). `R` is the set of services the source\n * needs (e.g. `FileSystem | Path | HttpPlatform` for {@link fileSystem}, or\n * `never` for {@link cloudflareAssets}).\n */\nexport interface AssetSource<R = never> {\n readonly serveAsset: (\n request: Request,\n pathname: string,\n ) => Effect.Effect<Option.Option<HttpServerResponse.HttpServerResponse>, never, R>;\n}\n\n/** The services a file-system {@link AssetSource} requires. */\nexport type FileSystemAssetServices = FileSystem.FileSystem | Path.Path | HttpPlatform.HttpPlatform;\n\nconst IMMUTABLE_HEADERS = { \"cache-control\": \"public, max-age=31536000, immutable\" };\n\n/**\n * Serve static client assets and prerendered pages from a build directory on\n * disk (the Node default). `dir` contains `client/` and `prerendered/`.\n */\nexport const fileSystem = (config: {\n /** Absolute path of the build output directory (contains `client/`, `prerendered/`). */\n readonly dir: string;\n /** The configured `manifest.appPath` — its `immutable/` subtree gets immutable caching. */\n readonly appPath: string;\n /** The set of prerendered paths exported by the build output. */\n readonly prerendered: ReadonlySet<string>;\n}): AssetSource<FileSystemAssetServices> => ({\n serveAsset: (request, pathname) =>\n Effect.gen(function* () {\n // Only GET/HEAD reach static assets.\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return Option.none();\n\n const path = yield* Path.Path;\n const fs = yield* FileSystem.FileSystem;\n\n const clientDir = path.join(config.dir, \"client\");\n const prerenderedDir = path.join(config.dir, \"prerendered\");\n const immutablePrefix = `/${config.appPath}/immutable/`;\n\n /**\n * Serve a file from `root` if `pathname` resolves to an existing file\n * inside it (rejecting path traversal). `Option.none()` otherwise.\n */\n const serveFile = (\n root: string,\n candidate: string,\n headers?: Record<string, string>,\n ): Effect.Effect<\n Option.Option<HttpServerResponse.HttpServerResponse>,\n never,\n FileSystemAssetServices\n > =>\n Effect.gen(function* () {\n const filePath = path.join(root, candidate);\n const relative = path.relative(root, filePath);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) return Option.none();\n\n const stat = yield* fs.stat(filePath).pipe(Effect.option);\n if (Option.isNone(stat) || stat.value.type !== \"File\") return Option.none();\n\n const response = yield* HttpServerResponse.file(filePath, { headers }).pipe(Effect.orDie);\n return Option.some(response);\n });\n\n // 1. Static client assets (hashed build assets get immutable caching).\n const isImmutable = pathname.startsWith(immutablePrefix);\n const asset = yield* serveFile(\n clientDir,\n pathname,\n isImmutable ? IMMUTABLE_HEADERS : undefined,\n );\n if (Option.isSome(asset)) return asset;\n\n // 2. Prerendered pages.\n if (config.prerendered.has(pathname) || config.prerendered.has(pathname + \"/\")) {\n const candidates =\n pathname === \"/\"\n ? [\"/index.html\"]\n : pathname.endsWith(\"/\")\n ? [`${pathname}index.html`, `${pathname.slice(0, -1)}.html`]\n : [pathname, `${pathname}.html`, `${pathname}/index.html`];\n for (const candidate of candidates) {\n const page = yield* serveFile(prerenderedDir, candidate);\n if (Option.isSome(page)) return page;\n }\n }\n\n return Option.none();\n }),\n});\n\n/**\n * Serve static assets from a Cloudflare Workers static-assets binding (commonly\n * `env.ASSETS`). The binding's `fetch` resolves client assets and prerendered\n * pages uploaded by the deployment; a 404 from it means \"not an asset\", so the\n * SSR core continues to server-side rendering.\n *\n * This is one provided convenience, not the assumption: pass any object with a\n * `fetch` (R2, KV, a CDN passthrough) or a custom {@link AssetSource}.\n */\nexport const cloudflareAssets = (assets: {\n fetch: (request: Request) => Promise<Response> | Response;\n}): AssetSource => ({\n serveAsset: (request) =>\n Effect.gen(function* () {\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return Option.none();\n const response = yield* Effect.promise(async () => assets.fetch(request));\n if (response.status === 404) return Option.none();\n return Option.some(HttpServerResponse.fromWeb(response));\n }),\n});\n\n/** The {@link AssetSource} constructors, grouped for the adapter API. */\nexport const AssetSource = {\n fileSystem,\n cloudflareAssets,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyCA,MAAM,oBAAoB,EAAE,iBAAiB,sCAAsC;;;;;AAMnF,MAAa,cAAc,YAOkB,EAC3C,aAAa,SAAS,aACpB,OAAO,IAAI,aAAa;CAEtB,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ,OAAO,OAAO,KAAK;CAE9E,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,KAAK,OAAO,WAAW;CAE7B,MAAM,YAAY,KAAK,KAAK,OAAO,KAAK,QAAQ;CAChD,MAAM,iBAAiB,KAAK,KAAK,OAAO,KAAK,aAAa;CAC1D,MAAM,kBAAkB,IAAI,OAAO,QAAQ;;;;;CAM3C,MAAM,aACJ,MACA,WACA,YAMA,OAAO,IAAI,aAAa;EACtB,MAAM,WAAW,KAAK,KAAK,MAAM,SAAS;EAC1C,MAAM,WAAW,KAAK,SAAS,MAAM,QAAQ;EAC7C,IAAI,SAAS,WAAW,IAAI,KAAK,KAAK,WAAW,QAAQ,GAAG,OAAO,OAAO,KAAK;EAE/E,MAAM,OAAO,OAAO,GAAG,KAAK,QAAQ,EAAE,KAAK,OAAO,MAAM;EACxD,IAAI,OAAO,OAAO,IAAI,KAAK,KAAK,MAAM,SAAS,QAAQ,OAAO,OAAO,KAAK;EAE1E,MAAM,WAAW,OAAO,mBAAmB,KAAK,UAAU,EAAE,QAAQ,CAAC,EAAE,KAAK,OAAO,KAAK;EACxF,OAAO,OAAO,KAAK,QAAQ;CAC7B,CAAC;CAIH,MAAM,QAAQ,OAAO,UACnB,WACA,UAHkB,SAAS,WAAW,eAI5B,IAAI,oBAAoB,KAAA,CACpC;CACA,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO;CAGjC,IAAI,OAAO,YAAY,IAAI,QAAQ,KAAK,OAAO,YAAY,IAAI,WAAW,GAAG,GAAG;EAC9E,MAAM,aACJ,aAAa,MACT,CAAC,aAAa,IACd,SAAS,SAAS,GAAG,IACnB,CAAC,GAAG,SAAS,aAAa,GAAG,SAAS,MAAM,GAAG,EAAE,EAAE,MAAM,IACzD;GAAC;GAAU,GAAG,SAAS;GAAQ,GAAG,SAAS;EAAY;EAC/D,KAAK,MAAM,aAAa,YAAY;GAClC,MAAM,OAAO,OAAO,UAAU,gBAAgB,SAAS;GACvD,IAAI,OAAO,OAAO,IAAI,GAAG,OAAO;EAClC;CACF;CAEA,OAAO,OAAO,KAAK;AACrB,CAAC,EACL;;;;;;;;;;AAWA,MAAa,oBAAoB,YAEb,EAClB,aAAa,YACX,OAAO,IAAI,aAAa;CACtB,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,QAAQ,OAAO,OAAO,KAAK;CAC9E,MAAM,WAAW,OAAO,OAAO,QAAQ,YAAY,OAAO,MAAM,OAAO,CAAC;CACxE,IAAI,SAAS,WAAW,KAAK,OAAO,OAAO,KAAK;CAChD,OAAO,OAAO,KAAK,mBAAmB,QAAQ,QAAQ,CAAC;AACzD,CAAC,EACL;;AAGA,MAAa,cAAc;CACzB;CACA;AACF"}
@@ -1,23 +1,23 @@
1
- import { Effect } from "effect";
1
+ import { AssetSource, FileSystemAssetServices } from "./assets.js";
2
+ import { Effect, Layer } from "effect";
2
3
  import { SSRManifest, Server } from "@sveltejs/kit";
3
- import * as FileSystem from "effect/FileSystem";
4
- import * as Path from "effect/Path";
5
- import * as HttpPlatform from "effect/unstable/http/HttpPlatform";
6
4
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
5
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
8
6
 
9
7
  //#region src/adapter/handler.d.ts
10
8
  /**
11
- * The services required by the SvelteKit HTTP application. All of them are
12
- * provided by `NodeHttpServer.layer` from `@effect/platform-node`.
9
+ * The services required by the SvelteKit HTTP application when serving assets
10
+ * from the file system (the Node default). All of them are provided by
11
+ * `NodeHttpServer.layer` from `@effect/platform-node`.
13
12
  */
14
- type SvelteKitHttpAppServices = FileSystem.FileSystem | Path.Path | HttpPlatform.HttpPlatform;
13
+ type SvelteKitHttpAppServices = FileSystemAssetServices;
15
14
  /**
16
15
  * The SvelteKit application as a composable Effect HTTP handler: an effect that
17
16
  * produces an {@link HttpServerResponse.HttpServerResponse} for the current
18
- * request. The `HttpServerRequest` is supplied per request by the server.
17
+ * request. The `HttpServerRequest` is supplied per request by the server. `R`
18
+ * is the asset source's services (defaults to the file-system services).
19
19
  */
20
- type SvelteKitHttpApp = Effect.Effect<HttpServerResponse.HttpServerResponse, never, SvelteKitHttpAppServices | HttpServerRequest.HttpServerRequest>;
20
+ type SvelteKitHttpApp<R = SvelteKitHttpAppServices> = Effect.Effect<HttpServerResponse.HttpServerResponse, never, R | HttpServerRequest.HttpServerRequest>;
21
21
  interface SvelteKitHandlerOptions {
22
22
  /** The SvelteKit `Server` class exported by the build output (`./server/index.js`). */
23
23
  readonly Server: typeof Server;
@@ -34,6 +34,25 @@ interface SvelteKitHandlerOptions {
34
34
  /** Prefix for the runtime configuration environment variables (ORIGIN, ...). */
35
35
  readonly envPrefix?: string;
36
36
  }
37
+ /** Options for the platform-neutral SSR core, {@link makeHandlerWith}. */
38
+ interface SvelteKitNeutralHandlerOptions<RAssets> {
39
+ /** The SvelteKit `Server` class exported by the build output. */
40
+ readonly Server: typeof Server;
41
+ /** The SSR manifest exported by the build output. */
42
+ readonly manifest: SSRManifest;
43
+ /** Environment variables passed to the SvelteKit server. */
44
+ readonly env?: Record<string, string | undefined> | undefined;
45
+ /** Prefix for the runtime configuration environment variables (ORIGIN, ...). */
46
+ readonly envPrefix?: string | undefined;
47
+ /** Where static client assets and prerendered pages come from. */
48
+ readonly assets: AssetSource<RAssets>;
49
+ /**
50
+ * How to read a server asset (backs `read()` from `$app/server`). On Node
51
+ * this streams from `<dir>/server`; on Workers, back it with the platform's
52
+ * own mechanism.
53
+ */
54
+ readonly read: (file: string) => ReadableStream<Uint8Array>;
55
+ }
37
56
  /**
38
57
  * Creates the SvelteKit HTTP handler.
39
58
  *
@@ -62,6 +81,52 @@ interface SvelteKitHandlerOptions {
62
81
  * ```
63
82
  */
64
83
  declare const makeHandler: (options: SvelteKitHandlerOptions) => Effect.Effect<SvelteKitHttpApp, never, SvelteKitHttpAppServices>;
84
+ /**
85
+ * The platform-neutral SSR core. Like {@link makeHandler}, but the asset source
86
+ * and the server-asset `read` capability are injected, so the same SvelteKit
87
+ * `HttpApp` can run on Node (file-system assets) or on Cloudflare Workers
88
+ * (`AssetSource.cloudflareAssets(env.ASSETS)`).
89
+ *
90
+ * The returned app:
91
+ *
92
+ * 1. falls through to the injected {@link AssetSource} for static client assets
93
+ * and prerendered pages (GET/HEAD), then
94
+ * 2. server-side renders via the SvelteKit server.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * // Workers: a fetch handler with assets from env.ASSETS
99
+ * import { makeHandlerWith, AssetSource, toWebHandler } from "...adapter/handler";
100
+ * import { Server } from "./server/index.js";
101
+ * import { manifest } from "./manifest.js";
102
+ *
103
+ * export default {
104
+ * fetch: (request, env, ctx) =>
105
+ * toWebHandler(
106
+ * makeHandlerWith({
107
+ * Server, manifest, env,
108
+ * assets: AssetSource.cloudflareAssets(env.ASSETS),
109
+ * read: () => { throw new Error("read() not supported on this deployment"); },
110
+ * }),
111
+ * )(request),
112
+ * };
113
+ * ```
114
+ */
115
+ declare const makeHandlerWith: <RAssets = never>(options: SvelteKitNeutralHandlerOptions<RAssets>) => Effect.Effect<SvelteKitHttpApp<RAssets>, never, never>;
116
+ /**
117
+ * Convert a SvelteKit `HttpApp` to a Web `fetch` handler, providing the asset
118
+ * source's services with `layer`. Use this for a Workers `fetch` export or any
119
+ * runtime that speaks the Web platform.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const fetchHandler = toWebHandler(
124
+ * makeHandlerWith({ ..., assets: AssetSource.cloudflareAssets(env.ASSETS) }),
125
+ * Layer.empty,
126
+ * );
127
+ * ```
128
+ */
129
+ declare const toWebHandler: <RAssets, E>(app: Effect.Effect<SvelteKitHttpApp<RAssets>, never, never>, layer: Layer.Layer<RAssets, E>) => ((request: Request) => Promise<Response>);
65
130
  //#endregion
66
- export { SvelteKitHandlerOptions, SvelteKitHttpApp, SvelteKitHttpAppServices, makeHandler };
131
+ export { AssetSource, type FileSystemAssetServices, SvelteKitHandlerOptions, SvelteKitHttpApp, SvelteKitHttpAppServices, SvelteKitNeutralHandlerOptions, makeHandler, makeHandlerWith, toWebHandler };
67
132
  //# sourceMappingURL=handler.d.ts.map
@@ -1,7 +1,8 @@
1
+ import { AssetSource } from "./assets.js";
1
2
  import { Config, Effect, Option, Stream } from "effect";
2
3
  import * as FileSystem from "effect/FileSystem";
3
4
  import * as Path from "effect/Path";
4
- import "effect/unstable/http/HttpPlatform";
5
+ import * as HttpEffect from "effect/unstable/http/HttpEffect";
5
6
  import * as HttpMethod from "effect/unstable/http/HttpMethod";
6
7
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7
8
  import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
@@ -55,7 +56,6 @@ const toWebRequest = (request, url, context) => {
55
56
  }
56
57
  return new Request(url, init);
57
58
  };
58
- const IMMUTABLE_HEADERS = { "cache-control": "public, max-age=31536000, immutable" };
59
59
  /**
60
60
  * Creates the SvelteKit HTTP handler.
61
61
  *
@@ -84,49 +84,61 @@ const IMMUTABLE_HEADERS = { "cache-control": "public, max-age=31536000, immutabl
84
84
  * ```
85
85
  */
86
86
  const makeHandler = (options) => Effect.gen(function* () {
87
- const config = yield* handlerConfig(options.envPrefix ?? "").pipe(Effect.orDie);
88
87
  const fs = yield* FileSystem.FileSystem;
89
88
  const path = yield* Path.Path;
90
89
  const context = yield* Effect.context();
91
- const clientDir = path.join(options.dir, "client");
92
- const prerenderedDir = path.join(options.dir, "prerendered");
93
90
  const serverDir = path.join(options.dir, "server");
91
+ return yield* makeHandlerWith({
92
+ Server: options.Server,
93
+ manifest: options.manifest,
94
+ env: options.env,
95
+ envPrefix: options.envPrefix,
96
+ read: (file) => Stream.toReadableStreamWith(fs.stream(path.join(serverDir, file)), context),
97
+ assets: AssetSource.fileSystem({
98
+ dir: options.dir,
99
+ appPath: options.manifest.appPath,
100
+ prerendered: options.prerendered
101
+ })
102
+ });
103
+ });
104
+ /**
105
+ * The platform-neutral SSR core. Like {@link makeHandler}, but the asset source
106
+ * and the server-asset `read` capability are injected, so the same SvelteKit
107
+ * `HttpApp` can run on Node (file-system assets) or on Cloudflare Workers
108
+ * (`AssetSource.cloudflareAssets(env.ASSETS)`).
109
+ *
110
+ * The returned app:
111
+ *
112
+ * 1. falls through to the injected {@link AssetSource} for static client assets
113
+ * and prerendered pages (GET/HEAD), then
114
+ * 2. server-side renders via the SvelteKit server.
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * // Workers: a fetch handler with assets from env.ASSETS
119
+ * import { makeHandlerWith, AssetSource, toWebHandler } from "...adapter/handler";
120
+ * import { Server } from "./server/index.js";
121
+ * import { manifest } from "./manifest.js";
122
+ *
123
+ * export default {
124
+ * fetch: (request, env, ctx) =>
125
+ * toWebHandler(
126
+ * makeHandlerWith({
127
+ * Server, manifest, env,
128
+ * assets: AssetSource.cloudflareAssets(env.ASSETS),
129
+ * read: () => { throw new Error("read() not supported on this deployment"); },
130
+ * }),
131
+ * )(request),
132
+ * };
133
+ * ```
134
+ */
135
+ const makeHandlerWith = (options) => Effect.gen(function* () {
136
+ const config = yield* handlerConfig(options.envPrefix ?? "").pipe(Effect.orDie);
94
137
  const server = new options.Server(options.manifest);
95
138
  yield* Effect.promise(() => server.init({
96
139
  env: options.env ?? {},
97
- read: (file) => Stream.toReadableStreamWith(fs.stream(path.join(serverDir, file)), context)
140
+ read: options.read
98
141
  }));
99
- /**
100
- * Serve a file from `root` if `pathname` resolves to an existing file
101
- * inside it. Returns `Option.none()` when the file does not exist or the
102
- * path escapes `root`.
103
- */
104
- const serveFile = (root, pathname, headers) => Effect.gen(function* () {
105
- const filePath = path.join(root, pathname);
106
- const relative = path.relative(root, filePath);
107
- if (relative.startsWith("..") || path.isAbsolute(relative)) return Option.none();
108
- const stat = yield* fs.stat(filePath).pipe(Effect.option);
109
- if (Option.isNone(stat) || stat.value.type !== "File") return Option.none();
110
- const response = yield* HttpServerResponse.file(filePath, { headers }).pipe(Effect.orDie);
111
- return Option.some(response);
112
- });
113
- /** Serve a prerendered page, trying the file layouts SvelteKit emits. */
114
- const servePrerendered = (pathname) => {
115
- const candidates = pathname === "/" ? ["/index.html"] : pathname.endsWith("/") ? [`${pathname}index.html`, `${pathname.slice(0, -1)}.html`] : [
116
- pathname,
117
- `${pathname}.html`,
118
- `${pathname}/index.html`
119
- ];
120
- return Effect.gen(function* () {
121
- let found = Option.none();
122
- for (const candidate of candidates) {
123
- if (Option.isSome(found)) break;
124
- found = yield* serveFile(prerenderedDir, candidate);
125
- }
126
- return found;
127
- });
128
- };
129
- const immutablePrefix = `/${options.manifest.appPath}/immutable/`;
130
142
  return Effect.gen(function* () {
131
143
  const request = yield* HttpServerRequest.HttpServerRequest;
132
144
  const requestContext = yield* Effect.context();
@@ -139,20 +151,37 @@ const makeHandler = (options) => Effect.gen(function* () {
139
151
  return HttpServerResponse.empty({ status: 400 });
140
152
  }
141
153
  if (request.method === "GET" || request.method === "HEAD") {
142
- const isImmutable = pathname.startsWith(immutablePrefix);
143
- const asset = yield* serveFile(clientDir, pathname, isImmutable ? IMMUTABLE_HEADERS : void 0);
154
+ const assetRequest = new Request(url, {
155
+ method: request.method,
156
+ headers: { ...request.headers }
157
+ });
158
+ const asset = yield* options.assets.serveAsset(assetRequest, pathname);
144
159
  if (Option.isSome(asset)) return asset.value;
145
- if (options.prerendered.has(pathname) || options.prerendered.has(pathname + "/")) {
146
- const page = yield* servePrerendered(pathname);
147
- if (Option.isSome(page)) return page.value;
148
- }
149
160
  }
150
161
  const webRequest = toWebRequest(request, url, requestContext);
151
162
  const response = yield* Effect.promise(() => server.respond(webRequest, { getClientAddress: () => clientAddressOf(request, config) }));
152
163
  return HttpServerResponse.fromWeb(response);
153
164
  });
154
165
  });
166
+ /**
167
+ * Convert a SvelteKit `HttpApp` to a Web `fetch` handler, providing the asset
168
+ * source's services with `layer`. Use this for a Workers `fetch` export or any
169
+ * runtime that speaks the Web platform.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const fetchHandler = toWebHandler(
174
+ * makeHandlerWith({ ..., assets: AssetSource.cloudflareAssets(env.ASSETS) }),
175
+ * Layer.empty,
176
+ * );
177
+ * ```
178
+ */
179
+ const toWebHandler = (app, layer) => {
180
+ let built;
181
+ const build = () => built ??= Effect.runPromise(app).then((inner) => HttpEffect.toWebHandlerLayer(inner, layer).handler);
182
+ return async (request) => (await build())(request);
183
+ };
155
184
  //#endregion
156
- export { makeHandler };
185
+ export { AssetSource, makeHandler, makeHandlerWith, toWebHandler };
157
186
 
158
187
  //# sourceMappingURL=handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.js","names":[],"sources":["../../src/adapter/handler.ts"],"sourcesContent":["/**\n * Runtime handler for the Effect SvelteKit adapter.\n *\n * `makeHandler` turns a built SvelteKit app (the `Server` class and manifest\n * emitted by `vite build`) into an Effect HTTP application that serves\n * static client assets, prerendered pages, and server-side rendered routes.\n *\n * The returned app is an ordinary Effect value: it can be served with the\n * generated `build/index.js` entry (see `./serve.js`), or mounted inside any\n * existing `effect/unstable/http` HTTP server alongside other routes.\n */\n\nimport type { SSRManifest, Server as SvelteKitServer } from \"@sveltejs/kit\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as HttpMethod from \"effect/unstable/http/HttpMethod\";\nimport * as HttpPlatform from \"effect/unstable/http/HttpPlatform\";\nimport * as HttpServerRequest from \"effect/unstable/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/unstable/http/HttpServerResponse\";\nimport * as Path from \"effect/Path\";\nimport { Config, Context, Effect, Option, Stream } from \"effect\";\n\n/**\n * The services required by the SvelteKit HTTP application. All of them are\n * provided by `NodeHttpServer.layer` from `@effect/platform-node`.\n */\nexport type SvelteKitHttpAppServices =\n | FileSystem.FileSystem\n | Path.Path\n | HttpPlatform.HttpPlatform;\n\n/**\n * The SvelteKit application as a composable Effect HTTP handler: an effect that\n * produces an {@link HttpServerResponse.HttpServerResponse} for the current\n * request. The `HttpServerRequest` is supplied per request by the server.\n */\nexport type SvelteKitHttpApp = Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n never,\n SvelteKitHttpAppServices | HttpServerRequest.HttpServerRequest\n>;\n\nexport interface SvelteKitHandlerOptions {\n /** The SvelteKit `Server` class exported by the build output (`./server/index.js`). */\n readonly Server: typeof SvelteKitServer;\n /** The SSR manifest exported by the build output (`./manifest.js`). */\n readonly manifest: SSRManifest;\n /** The set of prerendered paths exported by the build output (`./manifest.js`). */\n readonly prerendered: ReadonlySet<string>;\n /** The configured `paths.base`, exported by the build output (`./manifest.js`). */\n readonly base: string;\n /** Absolute path of the build output directory (contains `client/`, `server/`, `prerendered/`). */\n readonly dir: string;\n /** Environment variables passed to the SvelteKit server (e.g. `process.env`). */\n readonly env?: Record<string, string | undefined>;\n /** Prefix for the runtime configuration environment variables (ORIGIN, ...). */\n readonly envPrefix?: string;\n}\n\n/** Runtime configuration, read from the environment (compatible with adapter-node). */\nconst handlerConfig = (prefix: string) =>\n Config.all({\n origin: Config.option(Config.string(`${prefix}ORIGIN`)),\n protocolHeader: Config.option(Config.string(`${prefix}PROTOCOL_HEADER`)),\n hostHeader: Config.option(Config.string(`${prefix}HOST_HEADER`)),\n portHeader: Config.option(Config.string(`${prefix}PORT_HEADER`)),\n addressHeader: Config.option(Config.string(`${prefix}ADDRESS_HEADER`)),\n xffDepth: Config.int(`${prefix}XFF_DEPTH`).pipe(Config.withDefault(1)),\n });\n\ntype HandlerConfig = Effect.Success<ReturnType<typeof handlerConfig>>;\n\n/** Compute the request origin from configuration and headers (adapter-node semantics). */\nconst originOf = (request: HttpServerRequest.HttpServerRequest, config: HandlerConfig): string => {\n if (Option.isSome(config.origin)) return config.origin.value;\n\n const headers = request.headers;\n const protocolHeader = Option.getOrUndefined(config.protocolHeader)?.toLowerCase();\n const hostHeader = Option.getOrUndefined(config.hostHeader)?.toLowerCase() ?? \"host\";\n const portHeader = Option.getOrUndefined(config.portHeader)?.toLowerCase();\n\n const protocol = (protocolHeader && headers[protocolHeader]) || \"http\";\n const host = headers[hostHeader] ?? \"localhost\";\n const port = portHeader ? headers[portHeader] : undefined;\n\n return port ? `${protocol}://${host.split(\":\")[0]}:${port}` : `${protocol}://${host}`;\n};\n\n/** Resolve the client address, honouring ADDRESS_HEADER / XFF_DEPTH if configured. */\nconst clientAddressOf = (\n request: HttpServerRequest.HttpServerRequest,\n config: HandlerConfig,\n): string => {\n const addressHeader = Option.getOrUndefined(config.addressHeader)?.toLowerCase();\n if (addressHeader) {\n const value = request.headers[addressHeader];\n if (value) {\n if (addressHeader === \"x-forwarded-for\") {\n const addresses = value.split(\",\").map((address) => address.trim());\n if (config.xffDepth < 1 || config.xffDepth > addresses.length) {\n return addresses[0] ?? \"\";\n }\n return addresses[addresses.length - config.xffDepth]!;\n }\n return value;\n }\n }\n return Option.getOrElse(request.remoteAddress, () => \"\");\n};\n\n/** Build a web `Request` for SvelteKit from an `HttpServerRequest`. */\nconst toWebRequest = (\n request: HttpServerRequest.HttpServerRequest,\n url: URL,\n context: Context.Context<never>,\n): Request => {\n // `Headers` is a redactable record-like value; copy the enumerable string\n // entries into a plain record for the web `Request` constructor.\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n headers: { ...request.headers },\n };\n if (HttpMethod.hasBody(request.method)) {\n init.body = Stream.toReadableStreamWith(request.stream, context);\n init.duplex = \"half\";\n }\n return new Request(url, init);\n};\n\nconst IMMUTABLE_HEADERS = { \"cache-control\": \"public, max-age=31536000, immutable\" };\n\n/**\n * Creates the SvelteKit HTTP handler.\n *\n * The returned effect initialises the SvelteKit server (running your app's\n * server `init` hook) and yields an HTTP handler that:\n *\n * 1. serves static client assets from `<dir>/client` (with immutable cache\n * headers for hashed build assets),\n * 2. serves prerendered pages from `<dir>/prerendered`,\n * 3. falls back to server-side rendering via the SvelteKit server.\n *\n * @example\n * ```ts\n * // Mounting SvelteKit inside an existing effect/unstable/http server:\n * import { handler } from \"./build/handler.js\";\n * import * as HttpServer from \"effect/unstable/http/HttpServer\";\n * import { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n * import { Effect, Layer } from \"effect\";\n * import { createServer } from \"node:http\";\n *\n * const serverLayer = Layer.unwrap(\n * Effect.map(handler, (app) => HttpServer.serve(app))\n * ).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })));\n *\n * NodeRuntime.runMain(Layer.launch(serverLayer));\n * ```\n */\nexport const makeHandler = (\n options: SvelteKitHandlerOptions,\n): Effect.Effect<SvelteKitHttpApp, never, SvelteKitHttpAppServices> =>\n Effect.gen(function* () {\n const config = yield* handlerConfig(options.envPrefix ?? \"\").pipe(Effect.orDie);\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const context = yield* Effect.context<never>();\n\n const clientDir = path.join(options.dir, \"client\");\n const prerenderedDir = path.join(options.dir, \"prerendered\");\n const serverDir = path.join(options.dir, \"server\");\n\n // Initialise the SvelteKit server. `read` backs the `read()` helper from\n // `$app/server`, streaming server assets from the build output.\n const server = new options.Server(options.manifest);\n yield* Effect.promise(() =>\n server.init({\n env: (options.env ?? {}) as Record<string, string>,\n read: (file) => Stream.toReadableStreamWith(fs.stream(path.join(serverDir, file)), context),\n }),\n );\n\n /**\n * Serve a file from `root` if `pathname` resolves to an existing file\n * inside it. Returns `Option.none()` when the file does not exist or the\n * path escapes `root`.\n */\n const serveFile = (\n root: string,\n pathname: string,\n headers?: Record<string, string>,\n ): Effect.Effect<\n Option.Option<HttpServerResponse.HttpServerResponse>,\n never,\n SvelteKitHttpAppServices\n > =>\n Effect.gen(function* () {\n const filePath = path.join(root, pathname);\n\n // Prevent path traversal outside of the served directory.\n const relative = path.relative(root, filePath);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n return Option.none();\n }\n\n const stat = yield* fs.stat(filePath).pipe(Effect.option);\n if (Option.isNone(stat) || stat.value.type !== \"File\") {\n return Option.none();\n }\n\n const response = yield* HttpServerResponse.file(filePath, { headers }).pipe(Effect.orDie);\n return Option.some(response);\n });\n\n /** Serve a prerendered page, trying the file layouts SvelteKit emits. */\n const servePrerendered = (\n pathname: string,\n ): Effect.Effect<\n Option.Option<HttpServerResponse.HttpServerResponse>,\n never,\n SvelteKitHttpAppServices\n > => {\n const candidates =\n pathname === \"/\"\n ? [\"/index.html\"]\n : pathname.endsWith(\"/\")\n ? [`${pathname}index.html`, `${pathname.slice(0, -1)}.html`]\n : [pathname, `${pathname}.html`, `${pathname}/index.html`];\n\n return Effect.gen(function* () {\n let found = Option.none<HttpServerResponse.HttpServerResponse>();\n for (const candidate of candidates) {\n if (Option.isSome(found)) break;\n found = yield* serveFile(prerenderedDir, candidate);\n }\n return found;\n });\n };\n\n const immutablePrefix = `/${options.manifest.appPath}/immutable/`;\n\n // The HTTP handler itself, evaluated once per request.\n return Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const requestContext = yield* Effect.context<never>();\n\n const origin = originOf(request, config);\n const url = new URL(request.url, origin);\n\n let pathname: string;\n try {\n pathname = decodeURIComponent(url.pathname);\n } catch {\n return HttpServerResponse.empty({ status: 400 });\n }\n\n // 1. Static client assets (hashed build assets get immutable caching).\n if (request.method === \"GET\" || request.method === \"HEAD\") {\n const isImmutable = pathname.startsWith(immutablePrefix);\n const asset = yield* serveFile(\n clientDir,\n pathname,\n isImmutable ? IMMUTABLE_HEADERS : undefined,\n );\n if (Option.isSome(asset)) return asset.value;\n\n // 2. Prerendered pages.\n if (options.prerendered.has(pathname) || options.prerendered.has(pathname + \"/\")) {\n const page = yield* servePrerendered(pathname);\n if (Option.isSome(page)) return page.value;\n }\n }\n\n // 3. Server-side rendering.\n const webRequest = toWebRequest(request, url, requestContext);\n const response = yield* Effect.promise(() =>\n server.respond(webRequest, {\n getClientAddress: () => clientAddressOf(request, config),\n }),\n );\n return HttpServerResponse.fromWeb(response);\n });\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"handler.js","names":[],"sources":["../../src/adapter/handler.ts"],"sourcesContent":["/**\n * Runtime handler for the Effect SvelteKit adapter.\n *\n * `makeHandler` turns a built SvelteKit app (the `Server` class and manifest\n * emitted by `vite build`) into an Effect HTTP application that serves\n * static client assets, prerendered pages, and server-side rendered routes from\n * disk — the Node default.\n *\n * {@link makeHandlerWith} is the platform-neutral core: it takes an injected\n * {@link AssetSource} (where assets come from) and a `read` capability (how to\n * read server assets), so the same SvelteKit `HttpApp` can run on a Node server\n * or on Cloudflare Workers (assets from `env.ASSETS`).\n *\n * The returned app is an ordinary Effect value: it can be served with the\n * generated `build/index.js` entry (see `./serve.js`), mounted inside any\n * existing `effect/unstable/http` HTTP server, or converted to a `fetch` handler\n * (see {@link toWebHandler}).\n */\n\nimport type { SSRManifest, Server as SvelteKitServer } from \"@sveltejs/kit\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as HttpMethod from \"effect/unstable/http/HttpMethod\";\nimport * as HttpEffect from \"effect/unstable/http/HttpEffect\";\nimport * as HttpServerRequest from \"effect/unstable/http/HttpServerRequest\";\nimport * as HttpServerResponse from \"effect/unstable/http/HttpServerResponse\";\nimport * as Path from \"effect/Path\";\nimport { Config, Context, Effect, Layer, Option, Stream } from \"effect\";\nimport { AssetSource, type FileSystemAssetServices } from \"./assets.js\";\n\nexport { AssetSource, type FileSystemAssetServices } from \"./assets.js\";\n\n/**\n * The services required by the SvelteKit HTTP application when serving assets\n * from the file system (the Node default). All of them are provided by\n * `NodeHttpServer.layer` from `@effect/platform-node`.\n */\nexport type SvelteKitHttpAppServices = FileSystemAssetServices;\n\n/**\n * The SvelteKit application as a composable Effect HTTP handler: an effect that\n * produces an {@link HttpServerResponse.HttpServerResponse} for the current\n * request. The `HttpServerRequest` is supplied per request by the server. `R`\n * is the asset source's services (defaults to the file-system services).\n */\nexport type SvelteKitHttpApp<R = SvelteKitHttpAppServices> = Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n never,\n R | HttpServerRequest.HttpServerRequest\n>;\n\nexport interface SvelteKitHandlerOptions {\n /** The SvelteKit `Server` class exported by the build output (`./server/index.js`). */\n readonly Server: typeof SvelteKitServer;\n /** The SSR manifest exported by the build output (`./manifest.js`). */\n readonly manifest: SSRManifest;\n /** The set of prerendered paths exported by the build output (`./manifest.js`). */\n readonly prerendered: ReadonlySet<string>;\n /** The configured `paths.base`, exported by the build output (`./manifest.js`). */\n readonly base: string;\n /** Absolute path of the build output directory (contains `client/`, `server/`, `prerendered/`). */\n readonly dir: string;\n /** Environment variables passed to the SvelteKit server (e.g. `process.env`). */\n readonly env?: Record<string, string | undefined>;\n /** Prefix for the runtime configuration environment variables (ORIGIN, ...). */\n readonly envPrefix?: string;\n}\n\n/** Options for the platform-neutral SSR core, {@link makeHandlerWith}. */\nexport interface SvelteKitNeutralHandlerOptions<RAssets> {\n /** The SvelteKit `Server` class exported by the build output. */\n readonly Server: typeof SvelteKitServer;\n /** The SSR manifest exported by the build output. */\n readonly manifest: SSRManifest;\n /** Environment variables passed to the SvelteKit server. */\n readonly env?: Record<string, string | undefined> | undefined;\n /** Prefix for the runtime configuration environment variables (ORIGIN, ...). */\n readonly envPrefix?: string | undefined;\n /** Where static client assets and prerendered pages come from. */\n readonly assets: AssetSource<RAssets>;\n /**\n * How to read a server asset (backs `read()` from `$app/server`). On Node\n * this streams from `<dir>/server`; on Workers, back it with the platform's\n * own mechanism.\n */\n readonly read: (file: string) => ReadableStream<Uint8Array>;\n}\n\n/** Runtime configuration, read from the environment (compatible with adapter-node). */\nconst handlerConfig = (prefix: string) =>\n Config.all({\n origin: Config.option(Config.string(`${prefix}ORIGIN`)),\n protocolHeader: Config.option(Config.string(`${prefix}PROTOCOL_HEADER`)),\n hostHeader: Config.option(Config.string(`${prefix}HOST_HEADER`)),\n portHeader: Config.option(Config.string(`${prefix}PORT_HEADER`)),\n addressHeader: Config.option(Config.string(`${prefix}ADDRESS_HEADER`)),\n xffDepth: Config.int(`${prefix}XFF_DEPTH`).pipe(Config.withDefault(1)),\n });\n\ntype HandlerConfig = Effect.Success<ReturnType<typeof handlerConfig>>;\n\n/** Compute the request origin from configuration and headers (adapter-node semantics). */\nconst originOf = (request: HttpServerRequest.HttpServerRequest, config: HandlerConfig): string => {\n if (Option.isSome(config.origin)) return config.origin.value;\n\n const headers = request.headers;\n const protocolHeader = Option.getOrUndefined(config.protocolHeader)?.toLowerCase();\n const hostHeader = Option.getOrUndefined(config.hostHeader)?.toLowerCase() ?? \"host\";\n const portHeader = Option.getOrUndefined(config.portHeader)?.toLowerCase();\n\n const protocol = (protocolHeader && headers[protocolHeader]) || \"http\";\n const host = headers[hostHeader] ?? \"localhost\";\n const port = portHeader ? headers[portHeader] : undefined;\n\n return port ? `${protocol}://${host.split(\":\")[0]}:${port}` : `${protocol}://${host}`;\n};\n\n/** Resolve the client address, honouring ADDRESS_HEADER / XFF_DEPTH if configured. */\nconst clientAddressOf = (\n request: HttpServerRequest.HttpServerRequest,\n config: HandlerConfig,\n): string => {\n const addressHeader = Option.getOrUndefined(config.addressHeader)?.toLowerCase();\n if (addressHeader) {\n const value = request.headers[addressHeader];\n if (value) {\n if (addressHeader === \"x-forwarded-for\") {\n const addresses = value.split(\",\").map((address) => address.trim());\n if (config.xffDepth < 1 || config.xffDepth > addresses.length) {\n return addresses[0] ?? \"\";\n }\n return addresses[addresses.length - config.xffDepth]!;\n }\n return value;\n }\n }\n return Option.getOrElse(request.remoteAddress, () => \"\");\n};\n\n/** Build a web `Request` for SvelteKit from an `HttpServerRequest`. */\nconst toWebRequest = (\n request: HttpServerRequest.HttpServerRequest,\n url: URL,\n context: Context.Context<never>,\n): Request => {\n // `Headers` is a redactable record-like value; copy the enumerable string\n // entries into a plain record for the web `Request` constructor.\n const init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n headers: { ...request.headers },\n };\n if (HttpMethod.hasBody(request.method)) {\n init.body = Stream.toReadableStreamWith(request.stream, context);\n init.duplex = \"half\";\n }\n return new Request(url, init);\n};\n\n/**\n * Creates the SvelteKit HTTP handler.\n *\n * The returned effect initialises the SvelteKit server (running your app's\n * server `init` hook) and yields an HTTP handler that:\n *\n * 1. serves static client assets from `<dir>/client` (with immutable cache\n * headers for hashed build assets),\n * 2. serves prerendered pages from `<dir>/prerendered`,\n * 3. falls back to server-side rendering via the SvelteKit server.\n *\n * @example\n * ```ts\n * // Mounting SvelteKit inside an existing effect/unstable/http server:\n * import { handler } from \"./build/handler.js\";\n * import * as HttpServer from \"effect/unstable/http/HttpServer\";\n * import { NodeHttpServer, NodeRuntime } from \"@effect/platform-node\";\n * import { Effect, Layer } from \"effect\";\n * import { createServer } from \"node:http\";\n *\n * const serverLayer = Layer.unwrap(\n * Effect.map(handler, (app) => HttpServer.serve(app))\n * ).pipe(Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })));\n *\n * NodeRuntime.runMain(Layer.launch(serverLayer));\n * ```\n */\nexport const makeHandler = (\n options: SvelteKitHandlerOptions,\n): Effect.Effect<SvelteKitHttpApp, never, SvelteKitHttpAppServices> =>\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const context = yield* Effect.context<never>();\n\n const serverDir = path.join(options.dir, \"server\");\n\n return yield* makeHandlerWith<SvelteKitHttpAppServices>({\n Server: options.Server,\n manifest: options.manifest,\n env: options.env,\n envPrefix: options.envPrefix,\n // `read` backs the `read()` helper from `$app/server`, streaming server\n // assets from the build output.\n read: (file) => Stream.toReadableStreamWith(fs.stream(path.join(serverDir, file)), context),\n assets: AssetSource.fileSystem({\n dir: options.dir,\n appPath: options.manifest.appPath,\n prerendered: options.prerendered,\n }),\n });\n });\n\n/**\n * The platform-neutral SSR core. Like {@link makeHandler}, but the asset source\n * and the server-asset `read` capability are injected, so the same SvelteKit\n * `HttpApp` can run on Node (file-system assets) or on Cloudflare Workers\n * (`AssetSource.cloudflareAssets(env.ASSETS)`).\n *\n * The returned app:\n *\n * 1. falls through to the injected {@link AssetSource} for static client assets\n * and prerendered pages (GET/HEAD), then\n * 2. server-side renders via the SvelteKit server.\n *\n * @example\n * ```ts\n * // Workers: a fetch handler with assets from env.ASSETS\n * import { makeHandlerWith, AssetSource, toWebHandler } from \"...adapter/handler\";\n * import { Server } from \"./server/index.js\";\n * import { manifest } from \"./manifest.js\";\n *\n * export default {\n * fetch: (request, env, ctx) =>\n * toWebHandler(\n * makeHandlerWith({\n * Server, manifest, env,\n * assets: AssetSource.cloudflareAssets(env.ASSETS),\n * read: () => { throw new Error(\"read() not supported on this deployment\"); },\n * }),\n * )(request),\n * };\n * ```\n */\nexport const makeHandlerWith = <RAssets = never>(\n options: SvelteKitNeutralHandlerOptions<RAssets>,\n): Effect.Effect<SvelteKitHttpApp<RAssets>, never, never> =>\n Effect.gen(function* () {\n const config = yield* handlerConfig(options.envPrefix ?? \"\").pipe(Effect.orDie);\n\n // Initialise the SvelteKit server. `read` backs `read()` from `$app/server`.\n const server = new options.Server(options.manifest);\n yield* Effect.promise(() =>\n server.init({\n env: (options.env ?? {}) as Record<string, string>,\n read: options.read,\n }),\n );\n\n // The HTTP handler itself, evaluated once per request.\n return Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const requestContext = yield* Effect.context<never>();\n\n const origin = originOf(request, config);\n const url = new URL(request.url, origin);\n\n let pathname: string;\n try {\n pathname = decodeURIComponent(url.pathname);\n } catch {\n return HttpServerResponse.empty({ status: 400 });\n }\n\n // 1. Static client assets / prerendered pages, via the injected source.\n // Only GET/HEAD reach assets; build a body-less request for the source.\n if (request.method === \"GET\" || request.method === \"HEAD\") {\n const assetRequest = new Request(url, {\n method: request.method,\n headers: { ...request.headers },\n });\n const asset = yield* options.assets.serveAsset(assetRequest, pathname);\n if (Option.isSome(asset)) return asset.value;\n }\n\n // 2. Server-side rendering.\n const webRequest = toWebRequest(request, url, requestContext);\n const response = yield* Effect.promise(() =>\n server.respond(webRequest, {\n getClientAddress: () => clientAddressOf(request, config),\n }),\n );\n return HttpServerResponse.fromWeb(response);\n });\n });\n\n/**\n * Convert a SvelteKit `HttpApp` to a Web `fetch` handler, providing the asset\n * source's services with `layer`. Use this for a Workers `fetch` export or any\n * runtime that speaks the Web platform.\n *\n * @example\n * ```ts\n * const fetchHandler = toWebHandler(\n * makeHandlerWith({ ..., assets: AssetSource.cloudflareAssets(env.ASSETS) }),\n * Layer.empty,\n * );\n * ```\n */\nexport const toWebHandler = <RAssets, E>(\n app: Effect.Effect<SvelteKitHttpApp<RAssets>, never, never>,\n layer: Layer.Layer<RAssets, E>,\n): ((request: Request) => Promise<Response>) => {\n // Build the app (server.init) and the per-request web handler once, lazily on\n // the first request, then dispatch each request through it.\n let built: Promise<(request: Request) => Promise<Response>> | undefined;\n const build = (): Promise<(request: Request) => Promise<Response>> =>\n (built ??= Effect.runPromise(app).then(\n (inner) =>\n HttpEffect.toWebHandlerLayer(inner, layer).handler as (\n request: Request,\n ) => Promise<Response>,\n ));\n return async (request) => (await build())(request);\n};\n"],"mappings":"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"}
@@ -19,6 +19,21 @@ interface EffectAdapterOptions {
19
19
  * @default ""
20
20
  */
21
21
  readonly envPrefix?: string;
22
+ /**
23
+ * The deployment target for the generated entry:
24
+ *
25
+ * - `"node"` (default) — a production Node HTTP server (`node build`) on
26
+ * `@effect/platform-node`, serving assets from disk.
27
+ * - `"fetch"` — a Web `fetch` handler suitable for Cloudflare Workers
28
+ * (`build/index.js` exports `default { fetch }`), serving assets from the
29
+ * `env.ASSETS` binding via `AssetSource.cloudflareAssets`.
30
+ *
31
+ * Both targets also write `build/handler.js` (the SvelteKit `HttpApp`), so you
32
+ * can compose your own entry for any `effect/unstable/http` runtime.
33
+ *
34
+ * @default "node"
35
+ */
36
+ readonly target?: "node" | "fetch";
22
37
  }
23
38
  /**
24
39
  * Creates the Effect SvelteKit adapter.