@k8ordo/framework 0.1.0 → 1.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 CHANGED
@@ -33,12 +33,13 @@ dependencies in either mode: the build bundles what the application runs.
33
33
 
34
34
  <!-- peers -->
35
35
 
36
- | Package | Version | Required | Needed for |
37
- | ---------------- | ------- | -------- | ------------------------------------------ |
38
- | `@k8ordo/router` | ^1.0.0 | yes | routing, re-exported by the framework |
39
- | `react` | ≥19.3.0 | yes | rendering |
40
- | `react-dom` | ≥19.3.0 | yes | rendering |
41
- | `vite` | ≥8.0.0 | yes | the build (`framework()` is a Vite plugin) |
36
+ | Package | Version | Required | Needed for |
37
+ | --------------------- | -------- | -------- | ------------------------------------------ |
38
+ | `@k8ordo/router` | ^1.1.0 | yes | routing, re-exported by the framework |
39
+ | `react` | ≥19.3.0 | yes | rendering |
40
+ | `react-dom` | ≥19.3.0 | yes | rendering |
41
+ | `vite` | ≥8.0.0 | yes | the build (`framework()` is a Vite plugin) |
42
+ | `oxc-transform-react` | ^0.152.0 | optional | `reactCompiler: true` (React Compiler) |
42
43
 
43
44
  <!-- /peers -->
44
45
 
@@ -134,7 +135,9 @@ A value `paths` does not list — a product added after the build — is
134
135
  answered by a `fallback.tsx` beside the page: the build writes it once as a
135
136
  shell, the layouts in HTML, and the browser draws the page from the URL.
136
137
  The build also writes the `_redirects` through which Netlify and Cloudflare
137
- reach the shell, and `vercel()` the same rewrites for Vercel.
138
+ reach the shell, and `vercel()` the same rewrites for Vercel — around them,
139
+ routes of your own: headers on every page, redirects, a rewrite decided by
140
+ `Accept`, a proxy to another origin.
138
141
 
139
142
  ## AI Agent Documentation
140
143
 
@@ -154,7 +157,10 @@ error.tsx, loading.tsx, fallback.tsx, redirect.ts, guard.ts and route.ts;
154
157
  components go in `src/components/` and every other module (Server Actions,
155
158
  schemas, state definitions, data, helpers) in `src/lib/`, since a
156
159
  `_`-prefixed directory there is a URL segment, not a private one. Never
157
- edit `.k8ordo/` — it is generated. Import from `@k8ordo/framework` (never
160
+ edit `.k8ordo/` — it is generated, with the `Register` augmentations: the
161
+ route table's, and `@k8ordo/i18n`'s when `src/i18n.ts` exports the locale
162
+ set by the name `locales` (write that one by hand only when the set lives
163
+ elsewhere). Import from `@k8ordo/framework` (never
158
164
  `@k8ordo/router`), and the request API from `@k8ordo/framework/server`,
159
165
  which `mode: 'static'` refuses. Build links with `href()`; search params
160
166
  are `@k8ordo/state`'s.
@@ -1,2 +1 @@
1
- import { defineRoutes } from "@k8ordo/router";
2
- export { defineRoutes };
1
+ export { defineRoutes } from "@k8ordo/router";
package/dist/index.mjs CHANGED
@@ -1,2 +1 @@
1
- import { bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase } from "@k8ordo/router";
2
- export { bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase };
1
+ export { bindParams, href, isNotFound, matchPath, navigateTo, normalizePathname, notFound, useMatch, usePathname, usePendingPathname, withBase } from "@k8ordo/router";
@@ -1,6 +1,10 @@
1
+ import { Payload } from "./payload.mjs";
1
2
  import { ReactNode } from "react";
2
3
  //#region src/runtime/app-router.d.ts
3
4
  export declare const setDocumentClient: (client: string) => void;
5
+ /** What a Server Action's answer does to the document, once it is read. */
6
+ export declare const answerAction: (payload: Payload) => Promise<unknown>;
7
+ export declare const setPayloadReader: (read: (stream: ReadableStream<Uint8Array>) => Promise<Payload>) => void;
4
8
  /**
5
9
  * The client half under the framework: the tree comes from the server, so
6
10
  * there is no route table in the browser at all — only navigation. The
@@ -1,21 +1,19 @@
1
1
  "use client";
2
- import { ACTION_ID_HEADER } from "./payload.mjs";
3
2
  import { payloadPathFor } from "./payload-path.mjs";
4
3
  import { ShowNotFound } from "./fallback-boundary.mjs";
5
4
  import { isPayload } from "./is-payload.mjs";
6
5
  import { createPrefetchCache, prefetchTargetOf } from "./prefetch.mjs";
7
6
  import { Recover, markNavigated, reloadInstead } from "./recover.mjs";
8
7
  import { NavigationGeneration, PathnameProvider, useInterceptedNavigation, withoutBase } from "@k8ordo/router";
9
- import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback } from "@vitejs/plugin-rsc/browser";
10
8
  import { startTransition, useDeferredValue, useEffect, useRef, useState } from "react";
11
9
  import { jsx } from "react/jsx-runtime";
12
10
  //#region src/runtime/app-router.tsx
13
11
  /**
14
12
  * Where a Server Action's answer lands. The callback has to be registered
15
13
  * before any client component can call one, which is earlier than a component
16
- * can offer its own setter — so the wiring is here and the mounted router
17
- * lends it a way to apply what comes back, and to drop the pages it fetched
18
- * ahead, which the action may have changed.
14
+ * can offer its own setter — so `answerAction` is module-level and the
15
+ * mounted router lends it a way to apply what comes back, and to drop the
16
+ * pages it fetched ahead, which the action may have changed.
19
17
  */
20
18
  let mounted = null;
21
19
  /**
@@ -36,13 +34,8 @@ const setDocumentClient = (client) => {
36
34
  * document load brings the script that can.
37
35
  */
38
36
  const canRender = (payload) => payload.client === documentClient;
39
- setServerCallback(async (id, args) => {
40
- const temporaryReferences = createTemporaryReferenceSet();
41
- const payload = await createFromFetch(fetch(location.href, {
42
- method: "POST",
43
- headers: { [ACTION_ID_HEADER]: id },
44
- body: await encodeReply(args, { temporaryReferences })
45
- }), { temporaryReferences });
37
+ /** What a Server Action's answer does to the document, once it is read. */
38
+ const answerAction = async (payload) => {
46
39
  mounted?.forgetPrefetched();
47
40
  if (payload.redirect !== void 0) {
48
41
  await navigation.navigate(payload.redirect).finished;
@@ -51,7 +44,11 @@ setServerCallback(async (id, args) => {
51
44
  if (!canRender(payload)) return reloadInstead();
52
45
  mounted?.apply(payload);
53
46
  return payload.returnValue;
54
- });
47
+ };
48
+ let readPayload = () => Promise.reject(/* @__PURE__ */ new Error("k8ordo: payloads are read in the browser only"));
49
+ const setPayloadReader = (read) => {
50
+ readPayload = read;
51
+ };
55
52
  /**
56
53
  * The request each fetched payload is still streaming in on. A payload
57
54
  * resolves once its top arrives, and a page that awaits its data arrives
@@ -77,7 +74,7 @@ const fetchPage = async (payloadPath, signal) => {
77
74
  signal.addEventListener("abort", cancel, { once: true });
78
75
  const response = await fetch(payloadPath, { signal: request.signal });
79
76
  if (!isPayload(response)) return null;
80
- const payload = await createFromReadableStream(response.body);
77
+ const payload = await readPayload(response.body);
81
78
  streams.set(payload, {
82
79
  cancel: () => {
83
80
  request.abort();
@@ -272,4 +269,4 @@ function AppRouter({ pathname, search, tree, notFound }) {
272
269
  });
273
270
  }
274
271
  //#endregion
275
- export { AppRouter, setDocumentClient };
272
+ export { AppRouter, answerAction, setDocumentClient, setPayloadReader };
@@ -0,0 +1,8 @@
1
+ //#region src/runtime/browser-payloads.d.ts
2
+ /**
3
+ * Hands app-router the browser's Flight client: how a fetched payload is
4
+ * read, and where a Server Action is sent. Called by the browser entry
5
+ * before anything renders.
6
+ */
7
+ export declare const connectAppRouter: () => void;
8
+ //#endregion
@@ -0,0 +1,23 @@
1
+ import { ACTION_ID_HEADER } from "./payload.mjs";
2
+ import { answerAction, setPayloadReader } from "./app-router.mjs";
3
+ import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback } from "@vitejs/plugin-rsc/browser";
4
+ //#region src/runtime/browser-payloads.ts
5
+ /**
6
+ * Hands app-router the browser's Flight client: how a fetched payload is
7
+ * read, and where a Server Action is sent. Called by the browser entry
8
+ * before anything renders.
9
+ */
10
+ const connectAppRouter = () => {
11
+ setPayloadReader((stream) => createFromReadableStream(stream));
12
+ setServerCallback(async (id, args) => {
13
+ const temporaryReferences = createTemporaryReferenceSet();
14
+ const payload = await createFromFetch(fetch(location.href, {
15
+ method: "POST",
16
+ headers: { [ACTION_ID_HEADER]: id },
17
+ body: await encodeReply(args, { temporaryReferences })
18
+ }), { temporaryReferences });
19
+ return answerAction(payload);
20
+ });
21
+ };
22
+ //#endregion
23
+ export { connectAppRouter };
@@ -1,11 +1,13 @@
1
1
  import { AppRouter, setDocumentClient } from "./app-router.mjs";
2
+ import { connectAppRouter } from "./browser-payloads.mjs";
2
3
  import { mount } from "./mount.mjs";
3
4
  import { reportCaught } from "./page-boundary.mjs";
4
5
  import { whenRevealed } from "./revealed.mjs";
5
- import { createFromReadableStream } from "@vitejs/plugin-rsc/browser";
6
6
  import { jsx } from "react/jsx-runtime";
7
+ import { createFromReadableStream } from "@vitejs/plugin-rsc/browser";
7
8
  import { rscStream } from "rsc-html-stream/client";
8
9
  //#region src/runtime/entry.browser.tsx
10
+ connectAppRouter();
9
11
  const payload = await createFromReadableStream(rscStream);
10
12
  setDocumentClient(payload.client);
11
13
  await whenRevealed();
@@ -39,4 +39,5 @@ export declare class FallbackCatch extends Component<CatchProps, CatchState> {
39
39
  export declare const FallbackBoundary: ({ children }: {
40
40
  children: ReactNode;
41
41
  }) => ReactNode;
42
- //#endregion
42
+ //#endregion
43
+ export {};
@@ -26,4 +26,5 @@ type AnyGuard = (context: {
26
26
  * inner guard assume what an outer one checked.
27
27
  */
28
28
  export declare const runGuards: (guards: readonly AnyGuard[], context: Parameters<AnyGuard>[0]) => Promise<Response | null>;
29
- //#endregion
29
+ //#endregion
30
+ export {};
@@ -36,4 +36,5 @@ export declare class PageBoundary extends Component<{
36
36
  componentDidCatch(error: unknown): void;
37
37
  render(): ReactNode;
38
38
  }
39
- //#endregion
39
+ //#endregion
40
+ export {};
@@ -46,4 +46,5 @@ export declare const parseParams: (schemas: readonly ParamsSchema[], raw: Readon
46
46
  * says.
47
47
  */
48
48
  export declare const parseCatchAllParams: (schemas: readonly ParamsSchema[], raw: Readonly<Record<string, string>>) => ParsedParams;
49
- //#endregion
49
+ //#endregion
50
+ export {};
@@ -18,4 +18,5 @@ type ResolvedRedirect = {
18
18
  * the table normalizes it, so `/old/` is `/old` here too.
19
19
  */
20
20
  export declare const resolveRedirects: (declared: Readonly<Record<string, RedirectTarget>>) => ((pattern: string, pathname: string) => ResolvedRedirect | null);
21
- //#endregion
21
+ //#endregion
22
+ export {};
@@ -25,4 +25,5 @@ export declare class Redirect extends Error {
25
25
  */
26
26
  export declare const redirect: (to: string) => never;
27
27
  export declare const isRedirect: (value: unknown) => value is Redirect;
28
- //#endregion
28
+ //#endregion
29
+ export {};
@@ -32,4 +32,5 @@ export declare const methodNotAllowed: (module: AnyRouteModule) => Response;
32
32
  * answers the request as much as a guard does.
33
33
  */
34
34
  export declare const runRoute: ({ handler, headOnly }: RouteAnswer, context: RouteContext) => Promise<Response>;
35
- //#endregion
35
+ //#endregion
36
+ export {};
package/dist/serve.mjs CHANGED
@@ -120,7 +120,7 @@ const sendAnswer = async (incoming, response, result) => {
120
120
  const encoding = compressible ? negotiateEncoding(incoming.headers["accept-encoding"], ENCODINGS) : null;
121
121
  if (compressible) {
122
122
  const vary = result.headers.get("vary");
123
- headers["vary"] = vary === null ? "Accept-Encoding" : `${vary}, Accept-Encoding`;
123
+ headers.vary = vary === null ? "Accept-Encoding" : `${vary}, Accept-Encoding`;
124
124
  }
125
125
  if (encoding !== null) {
126
126
  headers["content-encoding"] = encoding;
@@ -0,0 +1,66 @@
1
+ import { Plugin, PluginOption } from "vite";
2
+ import "react";
3
+ //#region ../framework-engine/dist/host.d.mts
4
+ /** What `framework()` takes whatever the mode. */
5
+ type EngineOptions = {
6
+ /** Where the route files live, relative to the project root. */
7
+ readonly routesDir?: string;
8
+ /**
9
+ * Compiles the client components with React Compiler — the Rust one,
10
+ * through `@vitejs/plugin-react`'s `compiler`, which needs
11
+ * `oxc-transform-react` installed. Server components are left as they are.
12
+ */
13
+ readonly reactCompiler?: boolean;
14
+ };
15
+ //#endregion
16
+ //#region src/documents.d.ts
17
+ /**
18
+ * The `Content-Security-Policy` an application wants on every page, as
19
+ * directives and their sources. The build writes it into each page's
20
+ * `<meta>`, with the hashes of that page's signed inline scripts added.
21
+ */
22
+ type ContentSecurityPolicy = Readonly<Record<string, readonly string[]>>;
23
+ //#endregion
24
+ //#region src/static.d.ts
25
+ type StaticOptions = EngineOptions & {
26
+ /**
27
+ * Every route rendered at build time and written to files; no server at
28
+ * run time, so nothing that answers a request (Server Actions, guards, the
29
+ * request API) builds.
30
+ */
31
+ readonly mode: 'static';
32
+ /**
33
+ * Pathnames for routes with parameters, in the table's terms — without
34
+ * Vite's `base`. Static rendering cannot invent them, and a build that
35
+ * quietly skipped half the site would be worse than one that refuses.
36
+ *
37
+ * The patterns that need covering are handed in, so a site whose parameter
38
+ * takes the same values everywhere — a locale segment, say — expands them
39
+ * rather than listing every page twice.
40
+ *
41
+ * For a page with a `fallback.tsx` beside it, a pathname that still holds
42
+ * a parameter (`/ja/posts/:id`) is where its shell stands: the values the
43
+ * build did not write are answered there. Without one, the bare pattern
44
+ * is the shell's location.
45
+ */
46
+ readonly paths?: (patterns: readonly string[]) => readonly string[] | Promise<readonly string[]>;
47
+ /**
48
+ * The origin the site is served from — `https://example.com`. With it the
49
+ * build also writes `sitemap.xml`, listing every page it rendered at its
50
+ * URL, Vite's `base` included; without it, no sitemap, because a sitemap of
51
+ * relative URLs is not one.
52
+ */
53
+ readonly site?: string;
54
+ /**
55
+ * The `Content-Security-Policy` every page carries, as directives and
56
+ * their sources. The framework decides no policy: it writes this one into
57
+ * each page's `<meta>`, adding to `script-src` the hashes of its own inline
58
+ * scripts on that page — the payload and React's. An inline script of the
59
+ * application's is allowed by its hash here, as
60
+ * `colorSchemeScriptHash()` gives `@k8ordo/color-scheme`'s. Without it, no
61
+ * policy is written.
62
+ */
63
+ readonly csp?: ContentSecurityPolicy;
64
+ };
65
+ //#endregion
66
+ export { ContentSecurityPolicy as n, EngineOptions as r, StaticOptions as t };
package/dist/vercel.d.mts CHANGED
@@ -1,4 +1,46 @@
1
+ import "./static-BaGdAVF1.mjs";
1
2
  import { Plugin } from "vite";
3
+ //#region src/vercel-output.d.ts
4
+ /**
5
+ * A route as Vercel's Build Output API (v3) reads it, written to
6
+ * `.vercel/output/config.json` as given — the fields an application needs.
7
+ */
8
+ type VercelRoute = {
9
+ /**
10
+ * A regular expression Vercel matches against the whole pathname, the
11
+ * query left out — `^/posts/` does not match `/posts/first`, `^/posts/.+$`
12
+ * does — regardless of case unless `caseSensitive`.
13
+ */
14
+ readonly src: string;
15
+ /** A path in the deployment, or a URL elsewhere. */
16
+ readonly dest?: string;
17
+ readonly status?: number;
18
+ readonly headers?: Readonly<Record<string, string>>;
19
+ /** Keep matching the routes after this one: a route that only adds headers. */
20
+ readonly continue?: boolean;
21
+ /**
22
+ * Look for `dest` among the files; when none is there, go on with `dest`
23
+ * as the pathname.
24
+ */
25
+ readonly check?: boolean;
26
+ readonly caseSensitive?: boolean;
27
+ readonly has?: ReadonlyArray<{
28
+ readonly type: 'header';
29
+ /** Lowercase, as Vercel compares it. */
30
+ readonly key: string;
31
+ readonly value?: string;
32
+ }>;
33
+ };
34
+ type VercelOptions = {
35
+ /** Under `mode: 'static'`, routes tried before any file. */
36
+ readonly beforeFiles?: readonly VercelRoute[];
37
+ /**
38
+ * Under `mode: 'static'`, routes tried once neither a file nor a shell
39
+ * answered, before `404.html`.
40
+ */
41
+ readonly beforeNotFound?: readonly VercelRoute[];
42
+ };
43
+ //#endregion
2
44
  //#region src/vercel.d.ts
3
45
  /**
4
46
  * Deploys the application to Vercel: after `vite build`, the build is also
@@ -9,7 +51,11 @@ import { Plugin } from "vite";
9
51
  * nothing but its own directory, which is all the handler needs: server mode
10
52
  * builds it with every dependency bundled in. Under `mode: 'static'` there is
11
53
  * no function, only the rewrites that answer a value the build did not write
12
- * with its shell — Vercel never reads `_redirects`.
54
+ * with its shell — Vercel never reads `_redirects` — and the application's
55
+ * own routes around them, written as given: `beforeFiles` ahead of every
56
+ * file, `beforeNotFound` ahead of `404.html`. A file a `route.ts` wrote is
57
+ * served with the `Content-Type` its `GET` answered.
13
58
  */
14
- export declare const vercel: () => Plugin;
15
- //#endregion
59
+ export declare const vercel: (options?: VercelOptions) => Plugin;
60
+ //#endregion
61
+ export type { VercelOptions, VercelRoute };
package/dist/vercel.mjs CHANGED
@@ -7,12 +7,17 @@ import path from "node:path";
7
7
  const FUNCTION = "handler";
8
8
  /**
9
9
  * The hashed assets are marked immutable in the `hit` phase — only once a
10
- * file answered — so a missing one is never cached for a year.
10
+ * file answered — so a missing one is never cached for a year. `src` spells
11
+ * out the rest of the path, since Vercel matches it against the whole path
12
+ * and a prefix alone matches no file; `important`, as `@vercel/next` marks
13
+ * its own hashed files, keeps another route's `Cache-Control` from taking
14
+ * its place.
11
15
  */
12
16
  const immutableAssets = (base) => ({
13
- src: `^${RegExp.escape(`${base}assets/`)}`,
17
+ src: `^${RegExp.escape(`${base}assets/`)}.+$`,
14
18
  headers: { "cache-control": "public, max-age=31536000, immutable" },
15
- continue: true
19
+ continue: true,
20
+ important: true
16
21
  });
17
22
  /** Files first, and whatever names none goes to the handler. */
18
23
  const configFor = (base) => ({
@@ -57,15 +62,21 @@ const routeOf = (rule) => {
57
62
  };
58
63
  };
59
64
  /**
60
- * Files first; only what names none is rewritten — a value the build did
61
- * not write to its shell, the payload URL of a built URL that is not a page
62
- * to HTML — and what nothing rewrote is the build's `404.html`.
65
+ * The application's `beforeFiles`, then the files; only what names none is
66
+ * rewritten — a value the build did not write to its shell, the payload URL
67
+ * of a built URL that is not a page to HTML — and what nothing rewrote goes
68
+ * through the application's `beforeNotFound` to the build's `404.html`. A
69
+ * file a route.ts wrote is served with the type its `GET` answered, even
70
+ * where the extension says the same, so no file is left to the types Vercel
71
+ * gives an extension.
63
72
  */
64
- const staticConfigFor = ({ base, rewrites, notFound }) => ({
73
+ const staticConfigFor = ({ base, rewrites, notFound, types }, { beforeFiles = [], beforeNotFound = [] }) => ({
65
74
  version: 3,
66
75
  routes: [
76
+ ...beforeFiles,
67
77
  { handle: "filesystem" },
68
78
  ...rewrites.map((rule) => routeOf(rule)),
79
+ ...beforeNotFound,
69
80
  ...notFound ? [{
70
81
  src: "^/.*$",
71
82
  dest: withBase("/404.html", base),
@@ -73,7 +84,8 @@ const staticConfigFor = ({ base, rewrites, notFound }) => ({
73
84
  }] : [],
74
85
  { handle: "hit" },
75
86
  immutableAssets(base)
76
- ]
87
+ ],
88
+ ...types.size === 0 ? {} : { overrides: Object.fromEntries([...types].toSorted(([a], [b]) => a < b ? -1 : 1).map(([file, contentType]) => [withBase(`/${file}`, base).slice(1), { contentType }])) }
77
89
  });
78
90
  /**
79
91
  * A Node.js function that hands Vercel the handler as it is: the launcher
@@ -129,10 +141,11 @@ const writeVercelOutput = async (root, dirs, base) => {
129
141
  /**
130
142
  * A static build laid out as Vercel's Build Output API (v3) reads it: the
131
143
  * client build as static files at Vite's `base`, and the rewrites a host
132
- * applies once no file answered as routes — no function. `_redirects` stays
133
- * behind: Vercel never reads it, and would serve it as a file.
144
+ * applies once no file answered as routes, around them the application's
145
+ * own — no function. `_redirects` stays behind: Vercel never reads it, and
146
+ * would serve it as a file.
134
147
  */
135
- const writeStaticVercelOutput = async (output) => {
148
+ const writeStaticVercelOutput = async (output, routes = {}) => {
136
149
  const dir = path.join(output.root, ".vercel", "output");
137
150
  await rm(dir, {
138
151
  recursive: true,
@@ -143,7 +156,7 @@ const writeStaticVercelOutput = async (output) => {
143
156
  recursive: true,
144
157
  filter: (source) => source !== redirects
145
158
  });
146
- await writeFile(path.join(dir, "config.json"), json(staticConfigFor(output)));
159
+ await writeFile(path.join(dir, "config.json"), json(staticConfigFor(output, routes)));
147
160
  };
148
161
  //#endregion
149
162
  //#region src/vercel.ts
@@ -156,9 +169,14 @@ const writeStaticVercelOutput = async (output) => {
156
169
  * nothing but its own directory, which is all the handler needs: server mode
157
170
  * builds it with every dependency bundled in. Under `mode: 'static'` there is
158
171
  * no function, only the rewrites that answer a value the build did not write
159
- * with its shell — Vercel never reads `_redirects`.
172
+ * with its shell — Vercel never reads `_redirects` — and the application's
173
+ * own routes around them, written as given: `beforeFiles` ahead of every
174
+ * file, `beforeNotFound` ahead of `404.html`. A file a `route.ts` wrote is
175
+ * served with the `Content-Type` its `GET` answered.
160
176
  */
161
- const vercel = () => {
177
+ const vercel = (options = {}) => {
178
+ const { beforeFiles, beforeNotFound } = options;
179
+ for (const route of [...beforeFiles ?? [], ...beforeNotFound ?? []]) for (const { key } of route.has ?? []) if (key !== key.toLowerCase()) throw new Error(`vercel() route ${route.src} has the header key "${key}" — write it lowercase ("${key.toLowerCase()}"), as Vercel compares it`);
162
180
  let isStatic = false;
163
181
  let logger;
164
182
  return {
@@ -167,9 +185,10 @@ const vercel = () => {
167
185
  configResolved(config) {
168
186
  isStatic = config.plugins.some((plugin) => plugin.name === "k8ordo:static");
169
187
  ({logger} = config);
188
+ if (!isStatic && (beforeFiles !== void 0 || beforeNotFound !== void 0)) throw new Error("vercel() takes beforeFiles and beforeNotFound under mode: 'static' only — under mode: 'server' the handler answers every URL no file names");
170
189
  },
171
190
  api: { async writeStatic(output) {
172
- await writeStaticVercelOutput(output);
191
+ await writeStaticVercelOutput(output, options);
173
192
  logger?.info("k8ordo: wrote .vercel/output");
174
193
  } },
175
194
  buildApp: {
package/dist/vite.d.mts CHANGED
@@ -1,62 +1,5 @@
1
- import { Plugin, PluginOption } from "vite";
2
- import "react";
3
- //#region ../framework-engine/dist/host.d.mts
4
- /** What `framework()` takes whatever the mode. */
5
- type EngineOptions = {
6
- /** Where the route files live, relative to the project root. */
7
- readonly routesDir?: string;
8
- };
9
- //#endregion
10
- //#region src/documents.d.ts
11
- /**
12
- * The `Content-Security-Policy` an application wants on every page, as
13
- * directives and their sources. The build writes it into each page's
14
- * `<meta>`, with the hashes of that page's signed inline scripts added.
15
- */
16
- type ContentSecurityPolicy = Readonly<Record<string, readonly string[]>>;
17
- //#endregion
18
- //#region src/static.d.ts
19
- type StaticOptions = EngineOptions & {
20
- /**
21
- * Every route rendered at build time and written to files; no server at
22
- * run time, so nothing that answers a request (Server Actions, guards, the
23
- * request API) builds.
24
- */
25
- readonly mode: 'static';
26
- /**
27
- * Pathnames for routes with parameters, in the table's terms — without
28
- * Vite's `base`. Static rendering cannot invent them, and a build that
29
- * quietly skipped half the site would be worse than one that refuses.
30
- *
31
- * The patterns that need covering are handed in, so a site whose parameter
32
- * takes the same values everywhere — a locale segment, say — expands them
33
- * rather than listing every page twice.
34
- *
35
- * For a page with a `fallback.tsx` beside it, a pathname that still holds
36
- * a parameter (`/ja/posts/:id`) is where its shell stands: the values the
37
- * build did not write are answered there. Without one, the bare pattern
38
- * is the shell's location.
39
- */
40
- readonly paths?: (patterns: readonly string[]) => readonly string[] | Promise<readonly string[]>;
41
- /**
42
- * The origin the site is served from — `https://example.com`. With it the
43
- * build also writes `sitemap.xml`, listing every page it rendered at its
44
- * URL, Vite's `base` included; without it, no sitemap, because a sitemap of
45
- * relative URLs is not one.
46
- */
47
- readonly site?: string;
48
- /**
49
- * The `Content-Security-Policy` every page carries, as directives and
50
- * their sources. The framework decides no policy: it writes this one into
51
- * each page's `<meta>`, adding to `script-src` the hashes of its own inline
52
- * scripts on that page — the payload and React's. An inline script of the
53
- * application's is allowed by its hash here, as
54
- * `colorSchemeScriptHash()` gives `@k8ordo/color-scheme`'s. Without it, no
55
- * policy is written.
56
- */
57
- readonly csp?: ContentSecurityPolicy;
58
- };
59
- //#endregion
1
+ import { n as ContentSecurityPolicy, r as EngineOptions, t as StaticOptions } from "./static-BaGdAVF1.mjs";
2
+ import { PluginOption } from "vite";
60
3
  //#region src/vite.d.ts
61
4
  export type ServerOptions = EngineOptions & {
62
5
  /**