@k8ordo/framework 1.0.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 +10 -7
- package/dist/generated.mjs +1 -2
- package/dist/index.mjs +1 -2
- 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/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 +29 -14
- package/docs/GUIDE.md +19 -5
- package/docs/llms.txt +16 -5
- package/docs/references/csp.md +12 -3
- package/docs/references/deploy.md +157 -0
- package/docs/references/routing.md +13 -4
- package/package.json +16 -9
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
|
|
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";
|
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 {};
|
|
@@ -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
|
/**
|
package/dist/vite.mjs
CHANGED
|
@@ -1079,7 +1079,7 @@ const engine = (options, host) => {
|
|
|
1079
1079
|
mode: host.mode
|
|
1080
1080
|
});
|
|
1081
1081
|
return [
|
|
1082
|
-
react(),
|
|
1082
|
+
react({ compiler: options.reactCompiler === true }),
|
|
1083
1083
|
rsc(),
|
|
1084
1084
|
{
|
|
1085
1085
|
name: "k8ordo:engine",
|
|
@@ -1351,10 +1351,12 @@ const redirectPage = (to) => {
|
|
|
1351
1351
|
const escaped = escapeMarkup(to);
|
|
1352
1352
|
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${escaped}"><link rel="canonical" href="${escaped}"><title>Redirecting</title></head><body><a href="${escaped}">${escaped}</a></body></html>\n`;
|
|
1353
1353
|
};
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1356
|
-
"
|
|
1357
|
-
"
|
|
1354
|
+
const IGNORED = "is ignored in a <meta> — set it as a header at the host";
|
|
1355
|
+
const HEADER_ONLY = /* @__PURE__ */ new Map([
|
|
1356
|
+
["frame-ancestors", IGNORED],
|
|
1357
|
+
["report-uri", IGNORED],
|
|
1358
|
+
["sandbox", IGNORED],
|
|
1359
|
+
["report-to", "names an endpoint only a Reporting-Endpoints response header defines — set the policy as a header at the host"]
|
|
1358
1360
|
]);
|
|
1359
1361
|
/** What decides scripts: `script-src`, or `default-src` without it. */
|
|
1360
1362
|
const scriptDirectiveOf = (policy) => {
|
|
@@ -1374,15 +1376,16 @@ const elementDirectivesOf = (policy) => {
|
|
|
1374
1376
|
};
|
|
1375
1377
|
/**
|
|
1376
1378
|
* What would make a policy wrong to write into every page, named at once:
|
|
1377
|
-
* a directive a
|
|
1379
|
+
* a directive that works only from a header, a source that is not one, and
|
|
1378
1380
|
* `'strict-dynamic'`, under which the framework's module script — allowed by
|
|
1379
1381
|
* where it comes from, since a file cannot sign it — would never load.
|
|
1380
1382
|
*/
|
|
1381
1383
|
const policyProblems = (policy) => {
|
|
1382
1384
|
const problems = [];
|
|
1383
1385
|
for (const [name, sources] of Object.entries(policy)) {
|
|
1386
|
+
const headerOnly = HEADER_ONLY.get(name);
|
|
1384
1387
|
if (!/^[a-z][a-z-]*$/u.test(name)) problems.push(`"${name}" is not a directive name`);
|
|
1385
|
-
else if (
|
|
1388
|
+
else if (headerOnly !== void 0) problems.push(`${name} ${headerOnly}`);
|
|
1386
1389
|
for (const source of sources) if (source === "" || /[\s;,]/u.test(source)) problems.push(`"${source}" in ${name} is not one source — give each its own entry`);
|
|
1387
1390
|
}
|
|
1388
1391
|
for (const [name, sources] of elementDirectivesOf(policy)) if (sources.includes("'strict-dynamic'")) problems.push(`'strict-dynamic' in ${name} would block the framework's module script, which a file allows by where it comes from ('self'), not by a hash`);
|
|
@@ -2085,10 +2088,13 @@ const staticMode = (options) => {
|
|
|
2085
2088
|
const failed = [];
|
|
2086
2089
|
const redirected = /* @__PURE__ */ new Set();
|
|
2087
2090
|
const unanswered = [];
|
|
2091
|
+
const types = /* @__PURE__ */ new Map();
|
|
2088
2092
|
await inParallel(plan.paths.flatMap((pathname) => {
|
|
2089
2093
|
if (byRoute.has(pathname)) return [async () => {
|
|
2090
|
-
const
|
|
2094
|
+
const file = path.join(clientDir, dirFor(pathname));
|
|
2095
|
+
const { status, type } = await writeRoute(file, handler, urlFor(pathname));
|
|
2091
2096
|
if (status !== 200) unanswered.push(`${pathname} (${String(status)})`);
|
|
2097
|
+
else if (type !== null) types.set(path.relative(clientDir, file).split(path.sep).join("/"), type);
|
|
2092
2098
|
}];
|
|
2093
2099
|
const dir = path.join(clientDir, dirFor(pathname));
|
|
2094
2100
|
return [async () => {
|
|
@@ -2153,7 +2159,8 @@ const staticMode = (options) => {
|
|
|
2153
2159
|
client: clientDir,
|
|
2154
2160
|
base,
|
|
2155
2161
|
rewrites,
|
|
2156
|
-
notFound: unmatched !== null
|
|
2162
|
+
notFound: unmatched !== null,
|
|
2163
|
+
types
|
|
2157
2164
|
});
|
|
2158
2165
|
else if (vercel === void 0 && process.env.VERCEL !== void 0 && plan.shells.length > 0) builder.config.logger.warn("k8ordo: this build runs on Vercel, which does not read _redirects — add vercel() from @k8ordo/framework/vercel, or the shells are never served");
|
|
2159
2166
|
const shells = plan.shells.length === 0 ? "" : `, ${String(plan.shells.length)} ${plan.shells.length === 1 ? "shell" : "shells"} (${plan.shells.map((shell) => shell.location).join(", ")})`;
|
|
@@ -2209,19 +2216,27 @@ const write = async (file, handler, url, csp, { notFoundIsPage, shell }) => {
|
|
|
2209
2216
|
return written;
|
|
2210
2217
|
};
|
|
2211
2218
|
/**
|
|
2212
|
-
* Writes what a route.ts's GET answered, as the file at its pathname
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2219
|
+
* Writes what a route.ts's GET answered, as the file at its pathname, and
|
|
2220
|
+
* says the type it answered with — a file keeps none, so a static host serves
|
|
2221
|
+
* it with the type its extension says, and only an adapter that can name a
|
|
2222
|
+
* file's type (`vercel()`) serves the GET's. Anything but a 200 is not a file
|
|
2223
|
+
* the site has, and nothing is written.
|
|
2215
2224
|
*/
|
|
2216
2225
|
const writeRoute = async (file, handler, url) => {
|
|
2217
2226
|
const response = await handler(new Request(url));
|
|
2218
2227
|
if (response.status !== 200) {
|
|
2219
2228
|
if (response.status === 500) console.error(`k8ordo: ${url} — ${await response.text()}`);
|
|
2220
|
-
return
|
|
2229
|
+
return {
|
|
2230
|
+
status: response.status,
|
|
2231
|
+
type: null
|
|
2232
|
+
};
|
|
2221
2233
|
}
|
|
2222
2234
|
await mkdir(path.dirname(file), { recursive: true });
|
|
2223
2235
|
await writeFile(file, Buffer.from(await response.arrayBuffer()));
|
|
2224
|
-
return
|
|
2236
|
+
return {
|
|
2237
|
+
status: response.status,
|
|
2238
|
+
type: response.headers.get("content-type")
|
|
2239
|
+
};
|
|
2225
2240
|
};
|
|
2226
2241
|
//#endregion
|
|
2227
2242
|
//#region src/vite.ts
|
package/docs/GUIDE.md
CHANGED
|
@@ -234,7 +234,9 @@ The package is split by where the code runs.
|
|
|
234
234
|
`dist/server.js` runs, for a server of your own to import. A static build
|
|
235
235
|
it serves as a static host does, its `_redirects` applied.
|
|
236
236
|
- `@k8ordo/framework/vercel` — `vercel()`, the plugin that writes a build
|
|
237
|
-
of either mode for Vercel
|
|
237
|
+
of either mode for Vercel, and the types `VercelOptions` and
|
|
238
|
+
`VercelRoute`. Under static mode it also takes routes of your own,
|
|
239
|
+
`beforeFiles` and `beforeNotFound`.
|
|
238
240
|
- `@k8ordo/framework/generated` — what the generated `.k8ordo/` files
|
|
239
241
|
import. Not for code written by hand.
|
|
240
242
|
|
|
@@ -245,10 +247,22 @@ dev dependencies.
|
|
|
245
247
|
|
|
246
248
|
## The options
|
|
247
249
|
|
|
248
|
-
`framework()` takes `mode`, which is required,
|
|
249
|
-
directory (default `src/routes`)
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
`framework()` takes `mode`, which is required, `routesDir`, the route
|
|
251
|
+
directory (default `src/routes`), and `reactCompiler`, in either mode.
|
|
252
|
+
|
|
253
|
+
`reactCompiler: true` compiles the client components with React Compiler —
|
|
254
|
+
the Rust one, `oxc-transform-react`, through `@vitejs/plugin-react`'s
|
|
255
|
+
`compiler` — in `vite dev` and in the build. Install it beside the framework
|
|
256
|
+
as a dev dependency, in the range the package's README gives under Peer
|
|
257
|
+
Dependencies. Without it, `vite dev` and `vite build` stop as soon as they
|
|
258
|
+
read the config, with React's plugin's own error
|
|
259
|
+
(`React Compiler requires the optional …`), which names
|
|
260
|
+
`react({ compiler: true })`: the option the framework sets for you. Only
|
|
261
|
+
what runs in the browser is compiled; Server Components, and the HTML a
|
|
262
|
+
client component renders on the server, are left as written.
|
|
263
|
+
|
|
264
|
+
Three more describe files, so only `mode: 'static'` takes them — passing
|
|
265
|
+
them with `mode: 'server'` is a type error:
|
|
252
266
|
|
|
253
267
|
- `paths` — the pathnames of routes with parameters, and where a
|
|
254
268
|
`fallback.tsx`'s shell stands
|
package/docs/llms.txt
CHANGED
|
@@ -52,7 +52,9 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
52
52
|
- [Design Guide](GUIDE.md): choosing a mode and what static mode refuses,
|
|
53
53
|
setup (install, `vite.config.ts`, `tsconfig.json`, the root layout as the
|
|
54
54
|
document), `vite dev` and `vite preview` against a static build, the
|
|
55
|
-
entries, and the plugin's options
|
|
55
|
+
entries, and the plugin's options — `reactCompiler` among them, React
|
|
56
|
+
Compiler on the client components, which needs `oxc-transform-react`
|
|
57
|
+
installed.
|
|
56
58
|
|
|
57
59
|
## References
|
|
58
60
|
|
|
@@ -103,7 +105,8 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
103
105
|
`node dist/server.js` (`PORT`, `HOST`, every dependency bundled in, and
|
|
104
106
|
`resolve.external` for one that cannot be), `serve` (compression, `ETag`,
|
|
105
107
|
`Range`), the request handler under Node.js, Bun, Deno and Cloudflare
|
|
106
|
-
Workers, the same-origin rule for a `POST`, and `vercel()` in either mode
|
|
108
|
+
Workers, the same-origin rule for a `POST`, and `vercel()` in either mode,
|
|
109
|
+
with routes of your own under static mode;
|
|
107
110
|
a tab opened before a deploy, and Vite's `base`.
|
|
108
111
|
|
|
109
112
|
## Entry points
|
|
@@ -130,7 +133,7 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
130
133
|
`<head>`, with the hashes of the framework's own inline scripts on that
|
|
131
134
|
page added to `script-src` (or `default-src` made into it) and
|
|
132
135
|
`script-src-elem`; a policy with `'strict-dynamic'`, `frame-ancestors`,
|
|
133
|
-
`report-uri` or `sandbox` is refused. `FrameworkOptions` is
|
|
136
|
+
`report-uri`, `report-to` or `sandbox` is refused. `FrameworkOptions` is
|
|
134
137
|
`StaticOptions | ServerOptions`. This entry loads Vite and is for
|
|
135
138
|
`vite.config.ts` only.
|
|
136
139
|
- `@k8ordo/framework/server` — server mode only; a static build refuses any
|
|
@@ -179,7 +182,15 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
179
182
|
`.vercel/output/` (Build Output API v3) for `vercel build` /
|
|
180
183
|
`vercel deploy --prebuilt` — the client build as static files at Vite's
|
|
181
184
|
`base` (`assets/` immutable), and under server mode the request handler
|
|
182
|
-
as one streaming Node.js function; under static mode no function,
|
|
183
|
-
shells' rewrites as routes tried after the files, then `404.html
|
|
185
|
+
as one streaming Node.js function; under static mode no function, the
|
|
186
|
+
shells' rewrites as routes tried after the files, then `404.html`, and
|
|
187
|
+
each file a `route.ts` wrote served with the `Content-Type` its `GET`
|
|
188
|
+
answered (`overrides`) where other hosts go by its extension.
|
|
189
|
+
Under static mode `vercel({ beforeFiles, beforeNotFound })` also takes
|
|
190
|
+
routes of the application's own (`VercelRoute`: `src`, `dest`, `status`,
|
|
191
|
+
`headers`, `continue`, `check`, `caseSensitive`, `has`), written as given
|
|
192
|
+
before the files and before `404.html`; Vercel alone applies them,
|
|
193
|
+
matching `src` against the whole pathname (`^/posts/.+$`, not
|
|
194
|
+
`^/posts/`).
|
|
184
195
|
- `@k8ordo/framework/generated` — what the generated `.k8ordo/` files
|
|
185
196
|
import (`defineRoutes`, `Register`, …). Not for code written by hand.
|
package/docs/references/csp.md
CHANGED
|
@@ -91,9 +91,18 @@ page — one that reached it from content — is refused. The framework's module
|
|
|
91
91
|
script is allowed by where it comes from (`'self'`), since nothing in a file
|
|
92
92
|
can sign it, which is why the build refuses a policy with `'strict-dynamic'`.
|
|
93
93
|
It refuses `frame-ancestors`, `report-uri` and `sandbox` too, which a
|
|
94
|
-
`<meta>` ignores: set those as headers at the host.
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
`<meta>` ignores: set those as headers at the host. It refuses `report-to`
|
|
95
|
+
as well: the endpoint it names is defined only by a `Reporting-Endpoints`
|
|
96
|
+
response header, so set a policy that reports as a header at the host.
|
|
97
|
+
That header cannot carry the hashes the build adds page by page, and a
|
|
98
|
+
script runs only where every policy allows it: wherever the header decides
|
|
99
|
+
a script element — `script-src` (`default-src` without it) and
|
|
100
|
+
`script-src-elem` — allow `'self'` and `'unsafe-inline'` with no hash or
|
|
101
|
+
nonce beside them (either turns `'unsafe-inline'` off), and keep `csp`,
|
|
102
|
+
without `report-to`, for the `<meta>`, which allows inline scripts only by
|
|
103
|
+
hash. Without `csp`, no policy is written. There is no `nonce()` for this
|
|
104
|
+
mode: what an application signed with it would land in the file, and make
|
|
105
|
+
every build differ.
|
|
97
106
|
|
|
98
107
|
A `fallback.tsx`'s shell draws its page in the browser from data the
|
|
99
108
|
browser fetches
|
|
@@ -505,6 +505,163 @@ case-sensitively, as the route table matches; then `404.html` under a `404`,
|
|
|
505
505
|
when the build wrote one. `vercel()` writes them once the last page is
|
|
506
506
|
written, whichever side of `framework()` it sits.
|
|
507
507
|
|
|
508
|
+
**A file a `route.ts` wrote is served with the type its `GET` answered**,
|
|
509
|
+
the `Content-Type` `vite dev` sends, written as that file's entry in the
|
|
510
|
+
`overrides` of `config.json`: a feed at `blog/feed` is
|
|
511
|
+
`application/rss+xml`, not the `application/octet-stream` of a file with no
|
|
512
|
+
extension. The type is written even where the extension says the same, so
|
|
513
|
+
no file is left to the type Vercel gives an extension; only a `GET` that
|
|
514
|
+
named none — a body of bytes with no `Content-Type` — leaves its file to it.
|
|
515
|
+
`vite preview` and `serve` still go by the extension
|
|
516
|
+
([A route.ts under static mode](routing.md#under-static-mode)).
|
|
517
|
+
|
|
518
|
+
### Routes of your own
|
|
519
|
+
|
|
520
|
+
Under `mode: 'static'`, `vercel()` also takes routes of the application's
|
|
521
|
+
own, in the language Vercel's Build Output API reads — `src`, a regular
|
|
522
|
+
expression for the pathname, with `dest`, `status`, `headers`, `continue`,
|
|
523
|
+
`check`, `caseSensitive` and `has` — and writes them into
|
|
524
|
+
`.vercel/output/config.json` as given, in two places among its own:
|
|
525
|
+
|
|
526
|
+
```text
|
|
527
|
+
...beforeFiles
|
|
528
|
+
{ handle: 'filesystem' } the files
|
|
529
|
+
...the shells' rewrites
|
|
530
|
+
...beforeNotFound
|
|
531
|
+
404.html under a 404 when the build wrote one
|
|
532
|
+
{ handle: 'hit' }
|
|
533
|
+
assets/ sent immutable
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`beforeFiles` is tried before any file, so it is where headers for every
|
|
537
|
+
answer go, and a redirect or a rewrite that has to win over a file that is
|
|
538
|
+
there. `beforeNotFound` is tried once neither a file nor a shell answered,
|
|
539
|
+
so a catch-all of yours there never hides a shell: it is where a URL the
|
|
540
|
+
build wrote nothing for is proxied to another origin, or answered with a 404
|
|
541
|
+
of another body.
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
// vite.config.ts
|
|
545
|
+
import { vercel } from '@k8ordo/framework/vercel';
|
|
546
|
+
import { framework } from '@k8ordo/framework/vite';
|
|
547
|
+
import { defineConfig } from 'vite';
|
|
548
|
+
|
|
549
|
+
const acceptsMarkdown = [
|
|
550
|
+
{ type: 'header', key: 'accept', value: '.*text/markdown.*' },
|
|
551
|
+
] as const;
|
|
552
|
+
|
|
553
|
+
export default defineConfig({
|
|
554
|
+
plugins: [
|
|
555
|
+
framework({ mode: 'static' }),
|
|
556
|
+
vercel({
|
|
557
|
+
beforeFiles: [
|
|
558
|
+
{
|
|
559
|
+
src: '^/.*$',
|
|
560
|
+
headers: { 'X-Content-Type-Options': 'nosniff' },
|
|
561
|
+
continue: true,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
src: '^/((?:[^/]+/)*[^/]+)/$',
|
|
565
|
+
status: 308,
|
|
566
|
+
headers: { Location: '/$1' },
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
src: '^/posts/([a-z0-9-]+)$',
|
|
570
|
+
has: acceptsMarkdown,
|
|
571
|
+
dest: '/posts/$1/article.md',
|
|
572
|
+
check: true,
|
|
573
|
+
caseSensitive: true,
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
src: '^/feed$',
|
|
577
|
+
headers: { 'Vercel-CDN-Cache-Control': 'max-age=3600' },
|
|
578
|
+
continue: true,
|
|
579
|
+
caseSensitive: true,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
beforeNotFound: [
|
|
583
|
+
{
|
|
584
|
+
src: '^/feed$',
|
|
585
|
+
dest: 'https://api.example.com/feed',
|
|
586
|
+
caseSensitive: true,
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
src: '^/.*$',
|
|
590
|
+
has: acceptsMarkdown,
|
|
591
|
+
dest: '/not-found.md',
|
|
592
|
+
status: 404,
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
}),
|
|
596
|
+
],
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
- A route that only adds headers says `continue: true`, and comes before
|
|
601
|
+
the route that answers: an answering route ends the matching. What
|
|
602
|
+
`beforeFiles` adds this way is on every answer the routes lead to — a
|
|
603
|
+
file, a redirect, `404.html`, a `dest` with a `status`, an answer proxied
|
|
604
|
+
from another origin.
|
|
605
|
+
- The redirect takes the trailing slash off and keeps the query:
|
|
606
|
+
`/about/?x=1` goes to `/about?x=1`, which Vercel answers from
|
|
607
|
+
`about/index.html` as it is. Its pattern takes no empty segment, so it
|
|
608
|
+
never answers with `Location: //evil.test`, which a browser reads as
|
|
609
|
+
another origin. Vercel itself answers `//evil.test/` before any route, with
|
|
610
|
+
a 308 to `/evil.test/` that carries none of your headers.
|
|
611
|
+
- `check: true` looks for `dest` among the files. When none is there, the
|
|
612
|
+
matching goes on with `dest` as the pathname: `beforeNotFound` sees
|
|
613
|
+
`/posts/nope/article.md`, not `/posts/nope`. Here a `route.ts` at
|
|
614
|
+
`routes/posts/[slug]/article.md/` wrote each post's markdown beside its
|
|
615
|
+
page, and a client asking for markdown is answered with it. Have `paths`
|
|
616
|
+
give the pages and their markdown from one list of posts, so no post is
|
|
617
|
+
written without the other.
|
|
618
|
+
- `has` matches the header's text against `value`, a regular expression,
|
|
619
|
+
and negotiates nothing: `.*text/markdown.*` takes
|
|
620
|
+
`Accept: text/markdown, text/html;q=0.9`, and not a browser's
|
|
621
|
+
`text/html,application/xhtml+xml,*/*;q=0.8`, though `*/*` would take
|
|
622
|
+
markdown.
|
|
623
|
+
- A proxy is two routes: its headers ahead of the files, and the `dest` that
|
|
624
|
+
answers after them. The `dest` is asked with the request's query:
|
|
625
|
+
`/feed?page=2` fetches `https://api.example.com/feed?page=2`.
|
|
626
|
+
- `dest` with `status` answers with that file under that status, as the
|
|
627
|
+
build's own `404.html` route does.
|
|
628
|
+
|
|
629
|
+
What to keep in mind:
|
|
630
|
+
|
|
631
|
+
- **`src` matches the whole pathname**, never just its start, and never the
|
|
632
|
+
query: `^/posts/` does not match `/posts/first`, and `^/posts/.+$` does.
|
|
633
|
+
- **Only Vercel applies them.** `vite dev`, `vite preview` and `serve`
|
|
634
|
+
answer as if they were not there — no header, no redirect, nothing decided
|
|
635
|
+
by `Accept` — since an imitation of Vercel's routing that differed from it
|
|
636
|
+
would pass locally and fail once deployed. A URL only a route of yours
|
|
637
|
+
answers is a 404 there.
|
|
638
|
+
- **No `Cache-Control` in a route every path takes.** It reaches every file
|
|
639
|
+
too, those under `assets/` among them, which `vercel()` marks `immutable`
|
|
640
|
+
in the `hit` phase, after yours. Give a `Cache-Control` a `src` that names
|
|
641
|
+
the paths it is for.
|
|
642
|
+
- **Vercel matches `src` regardless of case** unless the route says
|
|
643
|
+
`caseSensitive: true`, while the routes `vercel()` writes itself are
|
|
644
|
+
case-sensitive, as the route table is. A route without it answers
|
|
645
|
+
`/POSTS/first` as well.
|
|
646
|
+
- **`has` names a header in lowercase**, as Vercel compares it. `vercel()`
|
|
647
|
+
refuses another spelling when it is called:
|
|
648
|
+
|
|
649
|
+
```text
|
|
650
|
+
vercel() route ^/posts/([a-z0-9-]+)$ has the header key "Accept" — write it lowercase ("accept"), as Vercel compares it
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
- **Nothing else is checked.** The routes are written as given — under a
|
|
654
|
+
`base` their `src` and `dest` carry it — and their patterns are Vercel's
|
|
655
|
+
(PCRE), which JavaScript's `RegExp` would misjudge, so a mistake shows once
|
|
656
|
+
deployed. Try a change on a preview deployment first.
|
|
657
|
+
- **Static mode only.** Under `mode: 'server'` the request handler answers
|
|
658
|
+
every URL no file names, and a `guard.ts` or a `route.ts` decides what it
|
|
659
|
+
sends. The build refuses them there:
|
|
660
|
+
|
|
661
|
+
```text
|
|
662
|
+
vercel() takes beforeFiles and beforeNotFound under mode: 'static' only — under mode: 'server' the handler answers every URL no file names
|
|
663
|
+
```
|
|
664
|
+
|
|
508
665
|
## A tab opened before a deploy
|
|
509
666
|
|
|
510
667
|
A tab keeps running the script it loaded, while every payload it fetches
|
|
@@ -280,10 +280,14 @@ with params needs `paths`, as a page's does
|
|
|
280
280
|
([Parameters](params.md#routes-with-parameters)) — with a request whose
|
|
281
281
|
origin is `site` when that option is given (an RSS feed's links are
|
|
282
282
|
absolute), and writes what it answered as the file at that pathname:
|
|
283
|
-
`dist/client/feed.xml`. A
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
`dist/client/feed.xml`. A file keeps no type of its own, so a static host
|
|
284
|
+
serves it with the type its extension says — `vite preview` and `serve` too,
|
|
285
|
+
which send a file with no extension as `application/octet-stream`. On
|
|
286
|
+
Vercel, `vercel()` serves it with the `Content-Type` its `GET` answered, the
|
|
287
|
+
type `vite dev` sends ([Deploying to Vercel](deploy.md#deploying-to-vercel)).
|
|
288
|
+
The `GET` has to answer `200`; anything else stops the build naming the
|
|
289
|
+
pathname. A file answers nothing but `GET`, so a `route.ts` exporting
|
|
290
|
+
another method is refused by name, in the build and in `vite dev`:
|
|
287
291
|
|
|
288
292
|
```
|
|
289
293
|
static build writes a route.ts as the file its GET answers, and a file cannot answer another method — these export one:
|
|
@@ -299,6 +303,11 @@ A `route.ts` at `/` cannot be a file (the host serves `/` from `index.html`),
|
|
|
299
303
|
nor one with pages written below it, which would need a directory of the same
|
|
300
304
|
name. What it writes is not a page, so `sitemap.xml` leaves it out.
|
|
301
305
|
|
|
306
|
+
Name the type in the `Response`. A string body with none is sent as
|
|
307
|
+
`text/plain;charset=UTF-8` — the `Response` sets it — so a `sitemap.xml`
|
|
308
|
+
returned that way is plain text on Vercel and in `vite dev`, whatever its
|
|
309
|
+
extension says. `vite dev` shows it before a deploy does.
|
|
310
|
+
|
|
302
311
|
## The generated files
|
|
303
312
|
|
|
304
313
|
The framework writes `.k8ordo/` and keeps it in step with the directories.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k8ordo/framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The k8ordo framework: routes/ compiled into React Server Components, run with or without a server.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"k8ordo",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@vitejs/plugin-react": "6.1.2",
|
|
65
|
-
"@vitejs/plugin-rsc": "0.5.
|
|
65
|
+
"@vitejs/plugin-rsc": "0.5.38",
|
|
66
66
|
"compressible": "2.0.18",
|
|
67
67
|
"fresh": "2.0.0",
|
|
68
68
|
"mime-types": "3.0.2",
|
|
@@ -73,26 +73,33 @@
|
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@arethetypeswrong/cli": "0.18.5",
|
|
75
75
|
"@k8ordo/framework-engine": "^0.1.1",
|
|
76
|
-
"@k8ordo/router": "^1.
|
|
76
|
+
"@k8ordo/router": "^1.1.0",
|
|
77
77
|
"@types/compressible": "2.0.3",
|
|
78
78
|
"@types/fresh": "0.5.3",
|
|
79
79
|
"@types/mime-types": "3.0.1",
|
|
80
80
|
"@types/range-parser": "1.2.7",
|
|
81
81
|
"@types/react": "19.3.0",
|
|
82
|
+
"oxc-transform-react": "0.154.0",
|
|
82
83
|
"publint": "0.3.25",
|
|
83
|
-
"react": "19.3.
|
|
84
|
-
"react-dom": "19.3.
|
|
84
|
+
"react": "19.3.1",
|
|
85
|
+
"react-dom": "19.3.1",
|
|
85
86
|
"typescript": "7.0.2",
|
|
86
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
87
|
-
"vite-plus": "1.
|
|
88
|
-
"vitest": "5.0.
|
|
87
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.3.0",
|
|
88
|
+
"vite-plus": "1.3.0",
|
|
89
|
+
"vitest": "5.0.5"
|
|
89
90
|
},
|
|
90
91
|
"peerDependencies": {
|
|
91
|
-
"@k8ordo/router": "^1.
|
|
92
|
+
"@k8ordo/router": "^1.1.0",
|
|
93
|
+
"oxc-transform-react": "^0.152.0",
|
|
92
94
|
"react": ">=19.3.0",
|
|
93
95
|
"react-dom": ">=19.3.0",
|
|
94
96
|
"vite": ">=8.0.0"
|
|
95
97
|
},
|
|
98
|
+
"peerDependenciesMeta": {
|
|
99
|
+
"oxc-transform-react": {
|
|
100
|
+
"optional": true
|
|
101
|
+
}
|
|
102
|
+
},
|
|
96
103
|
"engines": {
|
|
97
104
|
"node": ">=24.0.0"
|
|
98
105
|
},
|