@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 +14 -8
- package/dist/generated.mjs +1 -2
- package/dist/index.mjs +1 -2
- package/dist/runtime/app-router.d.mts +4 -0
- package/dist/runtime/app-router.mjs +12 -15
- package/dist/runtime/browser-payloads.d.mts +8 -0
- package/dist/runtime/browser-payloads.mjs +23 -0
- package/dist/runtime/entry.browser.mjs +3 -1
- package/dist/runtime/fallback-boundary.d.mts +2 -1
- package/dist/runtime/guard.d.mts +2 -1
- package/dist/runtime/page-boundary.d.mts +2 -1
- package/dist/runtime/params.d.mts +2 -1
- package/dist/runtime/redirect-file.d.mts +2 -1
- package/dist/runtime/redirect.d.mts +2 -1
- package/dist/runtime/route.d.mts +2 -1
- package/dist/serve.mjs +1 -1
- package/dist/static-BaGdAVF1.d.mts +66 -0
- package/dist/vercel.d.mts +49 -3
- package/dist/vercel.mjs +34 -15
- package/dist/vite.d.mts +2 -59
- package/dist/vite.mjs +106 -39
- package/docs/GUIDE.md +26 -9
- package/docs/llms.txt +19 -6
- package/docs/references/csp.md +12 -3
- package/docs/references/deploy.md +158 -1
- package/docs/references/routing.md +46 -7
- package/package.json +18 -11
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
|
|
37
|
-
|
|
|
38
|
-
| `@k8ordo/router`
|
|
39
|
-
| `react`
|
|
40
|
-
| `react-dom`
|
|
41
|
-
| `vite`
|
|
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
|
|
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.
|
package/dist/generated.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { defineRoutes };
|
|
1
|
+
export { defineRoutes } from "@k8ordo/router";
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
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
|
|
17
|
-
* lends it a way to apply what comes back, and to drop the
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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();
|
package/dist/runtime/guard.d.mts
CHANGED
|
@@ -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 {};
|
package/dist/runtime/route.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
61
|
-
* not write to its shell, the payload URL
|
|
62
|
-
* to HTML — and what nothing rewrote
|
|
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
|
|
133
|
-
* behind: Vercel never reads it, and
|
|
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 {
|
|
2
|
-
import "
|
|
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
|
/**
|