@thomasfosterau/effect-sveltekit 0.0.0 → 0.1.0
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/README.md +36 -13
- package/dist/Hooks.d.ts +2 -2
- package/dist/Hooks.js +2 -1
- package/dist/Hooks.js.map +1 -1
- package/dist/adapter/assets.d.ts +33 -0
- package/dist/adapter/assets.js +87 -0
- package/dist/adapter/assets.js.map +1 -0
- package/dist/adapter/handler.d.ts +75 -10
- package/dist/adapter/handler.js +73 -44
- package/dist/adapter/handler.js.map +1 -1
- package/dist/adapter/index.d.ts +15 -0
- package/dist/adapter/index.js +43 -5
- package/dist/adapter/index.js.map +1 -1
- package/dist/app-runtime.d.ts +18 -2
- package/dist/app-runtime.js +13 -10
- package/dist/app-runtime.js.map +1 -1
- package/dist/internal/remote-run.d.ts +17 -0
- package/dist/internal/remote-run.js +39 -0
- package/dist/internal/remote-run.js.map +1 -0
- package/dist/internal/remote-validators.js +23 -0
- package/dist/internal/remote-validators.js.map +1 -0
- package/dist/remote.server.d.ts +107 -33
- package/dist/remote.server.js +96 -41
- package/dist/remote.server.js.map +1 -1
- package/dist/request-runtime.d.ts +138 -0
- package/dist/request-runtime.js +186 -0
- package/dist/request-runtime.js.map +1 -0
- package/dist/server/Endpoint.d.ts +35 -4
- package/dist/server/Endpoint.js +14 -5
- package/dist/server/Endpoint.js.map +1 -1
- package/dist/server.d.ts +3 -2
- package/dist/server.js +2 -1
- package/dist/server.js.map +1 -1
- package/dist/transport.d.ts +43 -5
- package/dist/transport.js +36 -6
- package/dist/transport.js.map +1 -1
- 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
|
-
- **
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
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** —
|
|
59
|
-
`
|
|
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
|
|
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 {
|
|
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
|
|
12
|
-
*
|
|
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 =
|
|
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,
|
|
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
|
package/dist/adapter/handler.js
CHANGED
|
@@ -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/
|
|
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:
|
|
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
|
|
143
|
-
|
|
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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"}
|
package/dist/adapter/index.d.ts
CHANGED
|
@@ -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.
|