@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/dist/vite.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { _ as pagePathFor, a as cloudflareCounts, b as NOT_FOUND_SEGMENT, c as rewriteFor, d as ENCODINGS, f as SUFFIX, g as isPayloadPath, i as builtRules, l as rulesOf, n as resolveStatic, o as cloudflareMatches, p as isCompressible, s as formatRedirects, t as readRules, u as shellRules, v as payloadPathFor, x as decodePathname, y as FALLBACK_SEGMENT } from "./static-host-dgtttTuZ.mjs";
|
|
2
2
|
import { t as sharedDir } from "./shared-dir-CkF4QCWc.mjs";
|
|
3
|
+
import { findPackageJSON } from "node:module";
|
|
3
4
|
import { normalizePathname, withBase, withoutBase } from "@k8ordo/router";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
6
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
@@ -412,7 +413,10 @@ const ANSWERED = "answered";
|
|
|
412
413
|
*/
|
|
413
414
|
const GENERATED_FROM = `${FRAMEWORK}/generated`;
|
|
414
415
|
const ROUTE_REQUEST_FROM = `${FRAMEWORK}/server`;
|
|
415
|
-
const
|
|
416
|
+
const I18N = "@k8ordo/i18n";
|
|
417
|
+
/** Where an application declares its locale set, from its root. */
|
|
418
|
+
const LOCALES_MODULE = "src/i18n.ts";
|
|
419
|
+
const bannerFrom = (...sources) => `// Generated by ${FRAMEWORK} from ${sources.join(" and ")}. Do not edit.`;
|
|
416
420
|
const sanitize = (path) => {
|
|
417
421
|
const base = path.replace(/\.[jt]sx?$/u, "").replaceAll(/[^A-Za-z0-9]+/gu, "_").replaceAll(/^_+|_+$/gu, "");
|
|
418
422
|
return base === "" ? "route" : base;
|
|
@@ -610,7 +614,7 @@ const emitRoutesModule = (tree, options) => {
|
|
|
610
614
|
"ParsedParams"
|
|
611
615
|
];
|
|
612
616
|
return [
|
|
613
|
-
|
|
617
|
+
bannerFrom("routes/"),
|
|
614
618
|
"",
|
|
615
619
|
`import { defineRoutes } from '${GENERATED_FROM}';`,
|
|
616
620
|
...searchReaders.length > 0 ? [`import { urlReader } from '@k8ordo/state';`] : [],
|
|
@@ -777,17 +781,22 @@ const namerFile = (namer, name) => {
|
|
|
777
781
|
* Both packages read the app's route table through declaration merging, so
|
|
778
782
|
* the app gets typed paths everywhere without writing the ceremony itself.
|
|
779
783
|
* The router's `Register` is augmented through the framework's re-export of
|
|
780
|
-
* it, which TypeScript merges into the router's own declaration.
|
|
784
|
+
* it, which TypeScript merges into the router's own declaration. The locale
|
|
785
|
+
* set reaches `@k8ordo/i18n`'s `Register` the same way, so every message is
|
|
786
|
+
* held to it.
|
|
781
787
|
*/
|
|
782
788
|
const emitRegisterModule = (options) => {
|
|
783
789
|
const wantsState = options.stateModule !== null && options.stateModule !== void 0;
|
|
790
|
+
const localesModule = options.localesModule ?? null;
|
|
784
791
|
const withRequest = options.mode === "server";
|
|
785
792
|
const lines = [
|
|
786
|
-
|
|
793
|
+
localesModule === null ? bannerFrom("routes/") : bannerFrom("routes/", LOCALES_MODULE),
|
|
787
794
|
"",
|
|
788
795
|
`import type { ParsedParamsMap } from '${GENERATED_FROM}';`,
|
|
789
796
|
...withRequest ? [`import type { RouteRequest } from '${ROUTE_REQUEST_FROM}';`] : [],
|
|
797
|
+
...localesModule === null ? [] : [`import type { LocaleOf } from '${I18N}';`],
|
|
790
798
|
...wantsState ? [`import type {} from '${options.stateModule}';`] : [],
|
|
799
|
+
...localesModule === null ? [] : [`import type { locales } from '${localesModule}';`],
|
|
791
800
|
`import type { paramSchemas, routes, searchReaders } from '${options.routesModule}';`,
|
|
792
801
|
"",
|
|
793
802
|
`declare module '${GENERATED_FROM}' {`,
|
|
@@ -806,6 +815,7 @@ const emitRegisterModule = (options) => {
|
|
|
806
815
|
"}"
|
|
807
816
|
];
|
|
808
817
|
if (wantsState) lines.push("", `declare module '${options.stateModule}' {`, " interface Register {", " routes: typeof routes;", " }", "}");
|
|
818
|
+
if (localesModule !== null) lines.push("", `declare module '${I18N}' {`, " interface Register {", " locale: LocaleOf<typeof locales>;", " }", "}");
|
|
809
819
|
lines.push("");
|
|
810
820
|
return lines.join("\n");
|
|
811
821
|
};
|
|
@@ -855,12 +865,13 @@ const scanRoutes = async (dir) => {
|
|
|
855
865
|
* person reads the file: `export const paramsSchema`, `export {
|
|
856
866
|
* paramsSchema }`, and a destructured `export const { paramsSchema } =
|
|
857
867
|
* locales` are all an export; the same words inside a string or a comment
|
|
858
|
-
* are not, and `export type paramsSchema` is a type.
|
|
859
|
-
*
|
|
860
|
-
*
|
|
868
|
+
* are not, and `export type paramsSchema` is a type. `file` is the module's
|
|
869
|
+
* name, whose extension says whether it is TSX. A file that does not parse
|
|
870
|
+
* exports nothing; the build reports the syntax error itself, where it can
|
|
871
|
+
* name the line.
|
|
861
872
|
*/
|
|
862
|
-
const exportsOf = (source) => {
|
|
863
|
-
const { errors, module } = parseSync(
|
|
873
|
+
const exportsOf = (source, file) => {
|
|
874
|
+
const { errors, module } = parseSync(file, source, { sourceType: "module" });
|
|
864
875
|
if (errors.length > 0) return /* @__PURE__ */ new Set();
|
|
865
876
|
return new Set(module.staticExports.flatMap((statement) => statement.entries.filter((entry) => !entry.isType).map((entry) => entry.exportName.kind === "None" ? "*" : entry.exportName.name ?? "default")));
|
|
866
877
|
};
|
|
@@ -876,7 +887,7 @@ const READS_EXPORTS = /* @__PURE__ */ new Set([
|
|
|
876
887
|
*/
|
|
877
888
|
const readExports = async (routesDir, files) => new Map(await Promise.all(files.filter((file) => READS_EXPORTS.has(slotOf(file) ?? "")).map(async (file) => {
|
|
878
889
|
try {
|
|
879
|
-
return [file, exportsOf(await readFile(path.join(routesDir, file), "utf8"))];
|
|
890
|
+
return [file, exportsOf(await readFile(path.join(routesDir, file), "utf8"), file)];
|
|
880
891
|
} catch {
|
|
881
892
|
return [file, /* @__PURE__ */ new Set()];
|
|
882
893
|
}
|
|
@@ -900,15 +911,41 @@ const silentRoutes = (exported) => [...exported].filter(([file, names]) => slotO
|
|
|
900
911
|
path: file,
|
|
901
912
|
message: `exports none of ${ROUTE_METHODS.join(", ")} — a route.ts answers the methods it exports`
|
|
902
913
|
}));
|
|
903
|
-
|
|
914
|
+
/** Whether the application's manifest lists `name`. */
|
|
915
|
+
const dependsOn = async (root, name) => {
|
|
904
916
|
try {
|
|
905
917
|
const manifest = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
|
|
906
|
-
return
|
|
918
|
+
return name in (manifest.dependencies ?? {}) || name in (manifest.devDependencies ?? {});
|
|
907
919
|
} catch {
|
|
908
920
|
return false;
|
|
909
921
|
}
|
|
910
922
|
};
|
|
911
923
|
/**
|
|
924
|
+
* Where the application declares its locale set: `null` when it does not
|
|
925
|
+
* depend on `@k8ordo/i18n`, the package does not resolve from the root, or
|
|
926
|
+
* there is no {@link LOCALES_MODULE}.
|
|
927
|
+
*/
|
|
928
|
+
const localeSetOf = async (root) => {
|
|
929
|
+
if (!await dependsOn(root, "@k8ordo/i18n")) return null;
|
|
930
|
+
const module = path.join(root, LOCALES_MODULE);
|
|
931
|
+
let source;
|
|
932
|
+
try {
|
|
933
|
+
findPackageJSON("@k8ordo/i18n", module);
|
|
934
|
+
source = await readFile(module, "utf8");
|
|
935
|
+
} catch {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
module,
|
|
940
|
+
exportsLocales: exportsOf(source, module).has("locales")
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
/**
|
|
944
|
+
* Without the generated `Register`, `@k8ordo/i18n` holds a message to no
|
|
945
|
+
* locale set, and nothing else would say so.
|
|
946
|
+
*/
|
|
947
|
+
const LOCALES_UNEXPORTED = `${LOCALES_MODULE} does not export \`locales\` by name (\`export *\` and a type-only export do not count), so @k8ordo/i18n's Register is not generated and a message missing a locale compiles — export the locale set from it as \`locales\``;
|
|
948
|
+
/**
|
|
912
949
|
* Writes what the application would otherwise hand-write: the route table
|
|
913
950
|
* and the type wiring. Nothing here decides anything the grammar has not
|
|
914
951
|
* already decided — problems come back for the caller to report.
|
|
@@ -917,7 +954,7 @@ const generate = async (options) => {
|
|
|
917
954
|
const files = await scanRoutes(options.routesDir);
|
|
918
955
|
const parsed = parseRouteTree(files);
|
|
919
956
|
const exported = await readExports(options.routesDir, files);
|
|
920
|
-
const withState = await
|
|
957
|
+
const withState = await dependsOn(options.root, "@k8ordo/state");
|
|
921
958
|
const withSearch = pagesReadingSearch(exported);
|
|
922
959
|
const problems = [
|
|
923
960
|
...parsed.problems,
|
|
@@ -928,11 +965,13 @@ const generate = async (options) => {
|
|
|
928
965
|
const { tree } = parsed;
|
|
929
966
|
if (problems.length > 0) return {
|
|
930
967
|
problems,
|
|
968
|
+
warnings: [],
|
|
931
969
|
routesModule: path.join(options.outDir, "routes.gen.ts")
|
|
932
970
|
};
|
|
933
|
-
const toRoutes =
|
|
971
|
+
const toRoutes = importPath(options.outDir, options.routesDir);
|
|
972
|
+
const localeSet = await localeSetOf(options.root);
|
|
934
973
|
const routesSource = emitRoutesModule(tree, {
|
|
935
|
-
importPrefix: toRoutes
|
|
974
|
+
importPrefix: toRoutes,
|
|
936
975
|
mode: options.mode,
|
|
937
976
|
withParams: new Set([...exported].filter(([, names]) => names.has("paramsSchema")).map(([file]) => file)),
|
|
938
977
|
withSearch
|
|
@@ -940,6 +979,7 @@ const generate = async (options) => {
|
|
|
940
979
|
const registerSource = emitRegisterModule({
|
|
941
980
|
routesModule: "./routes.gen",
|
|
942
981
|
stateModule: withState ? "@k8ordo/state" : null,
|
|
982
|
+
localesModule: localeSet?.exportsLocales === true ? importPath(options.outDir, localeSet.module.replace(/\.ts$/u, "")) : null,
|
|
943
983
|
mode: options.mode
|
|
944
984
|
});
|
|
945
985
|
await mkdir(options.outDir, { recursive: true });
|
|
@@ -949,9 +989,15 @@ const generate = async (options) => {
|
|
|
949
989
|
await writeIfChanged(path.join(options.outDir, ".gitignore"), `# Generated by ${FRAMEWORK}.\n*\n`);
|
|
950
990
|
return {
|
|
951
991
|
problems,
|
|
992
|
+
warnings: localeSet?.exportsLocales === false ? [LOCALES_UNEXPORTED] : [],
|
|
952
993
|
routesModule
|
|
953
994
|
};
|
|
954
995
|
};
|
|
996
|
+
/** The relative specifier the generated files import `to` by, from `from`. */
|
|
997
|
+
const importPath = (from, to) => {
|
|
998
|
+
const relative = path.relative(from, to).replaceAll(path.sep, "/");
|
|
999
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
1000
|
+
};
|
|
955
1001
|
/** Rewriting an unchanged file would restart HMR for no reason. */
|
|
956
1002
|
const writeIfChanged = async (file, content) => {
|
|
957
1003
|
try {
|
|
@@ -971,7 +1017,7 @@ const SERVER_REFERENCES_ONLY = /dynamically imported by \S*virtual:vite-rsc\/ser
|
|
|
971
1017
|
const OUT_DIR = ".k8ordo";
|
|
972
1018
|
const ESM_ENTRY = "[name].js";
|
|
973
1019
|
/** Vite's own answer, so nothing downstream can disagree with it. */
|
|
974
|
-
const isProduction = (mode) => (process.env
|
|
1020
|
+
const isProduction = (mode) => (process.env.NODE_ENV ?? process.env.VITE_USER_NODE_ENV ?? mode) === "production";
|
|
975
1021
|
/**
|
|
976
1022
|
* Client components reach the browser as RSC references, discovered while
|
|
977
1023
|
* rendering rather than by crawling the client entry — so React itself is
|
|
@@ -1019,6 +1065,7 @@ const FAMILY = /^@k8ordo\//u;
|
|
|
1019
1065
|
const engine = (options, host) => {
|
|
1020
1066
|
let root = "";
|
|
1021
1067
|
let routesDir = "";
|
|
1068
|
+
let localesModule = "";
|
|
1022
1069
|
let outDir = "";
|
|
1023
1070
|
let logger;
|
|
1024
1071
|
let devServer;
|
|
@@ -1032,7 +1079,7 @@ const engine = (options, host) => {
|
|
|
1032
1079
|
mode: host.mode
|
|
1033
1080
|
});
|
|
1034
1081
|
return [
|
|
1035
|
-
react(),
|
|
1082
|
+
react({ compiler: options.reactCompiler === true }),
|
|
1036
1083
|
rsc(),
|
|
1037
1084
|
{
|
|
1038
1085
|
name: "k8ordo:engine",
|
|
@@ -1084,6 +1131,7 @@ const engine = (options, host) => {
|
|
|
1084
1131
|
if (!config.base.startsWith("/") || config.base.startsWith("//")) throw new Error(`k8ordo serves its pages under Vite's base, so base has to be a path from the root, like '/docs/' — got '${config.base}'`);
|
|
1085
1132
|
({root, logger} = config);
|
|
1086
1133
|
routesDir = path.resolve(root, options.routesDir ?? "src/routes");
|
|
1134
|
+
localesModule = path.resolve(root, LOCALES_MODULE);
|
|
1087
1135
|
outDir = path.resolve(root, OUT_DIR);
|
|
1088
1136
|
},
|
|
1089
1137
|
buildApp: {
|
|
@@ -1101,7 +1149,8 @@ const engine = (options, host) => {
|
|
|
1101
1149
|
}
|
|
1102
1150
|
},
|
|
1103
1151
|
async buildStart() {
|
|
1104
|
-
const { problems } = await generate(generateOptions());
|
|
1152
|
+
const { problems, warnings } = await generate(generateOptions());
|
|
1153
|
+
for (const warning of warnings) logger.warnOnce(`k8ordo: ${warning}`);
|
|
1105
1154
|
if (problems.length > 0) this.error(`routes/ is not a valid pathname space:\n${problems.map((problem) => ` routes/${problem.path}: ${problem.message}`).join("\n")}`);
|
|
1106
1155
|
},
|
|
1107
1156
|
configureServer(server) {
|
|
@@ -1109,11 +1158,14 @@ const engine = (options, host) => {
|
|
|
1109
1158
|
devServer = server;
|
|
1110
1159
|
},
|
|
1111
1160
|
async watchChange(file) {
|
|
1112
|
-
|
|
1161
|
+
const changed = path.resolve(file);
|
|
1162
|
+
if (!changed.startsWith(`${routesDir}${path.sep}`) && changed !== localesModule) return;
|
|
1113
1163
|
const run = generating.then(() => generate(generateOptions()));
|
|
1114
1164
|
generating = run.then(() => void 0, () => void 0);
|
|
1115
|
-
const { problems } = await run;
|
|
1165
|
+
const { problems, warnings } = await run;
|
|
1116
1166
|
for (const problem of problems) logger.error(`routes/${problem.path}: ${problem.message}`);
|
|
1167
|
+
for (const warning of warnings) logger.warnOnce(`k8ordo: ${warning}`);
|
|
1168
|
+
if (changed === localesModule) return;
|
|
1117
1169
|
for (const environment of Object.values(devServer?.environments ?? {})) {
|
|
1118
1170
|
const { moduleGraph } = environment;
|
|
1119
1171
|
for (const module of moduleGraph.getModulesByFile(routesModule()) ?? []) moduleGraph.invalidateModule(module);
|
|
@@ -1267,7 +1319,7 @@ const serverMode = () => ({
|
|
|
1267
1319
|
order: "post",
|
|
1268
1320
|
async handler(builder) {
|
|
1269
1321
|
const { root, logger } = builder.config;
|
|
1270
|
-
const clientOut = builder.environments
|
|
1322
|
+
const clientOut = builder.environments.client?.config.build.outDir;
|
|
1271
1323
|
if (clientOut === void 0) throw new Error("server build ran without the client build");
|
|
1272
1324
|
const client = path.resolve(root, clientOut);
|
|
1273
1325
|
const count = await precompress(client);
|
|
@@ -1299,10 +1351,12 @@ const redirectPage = (to) => {
|
|
|
1299
1351
|
const escaped = escapeMarkup(to);
|
|
1300
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`;
|
|
1301
1353
|
};
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
"
|
|
1305
|
-
"
|
|
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"]
|
|
1306
1360
|
]);
|
|
1307
1361
|
/** What decides scripts: `script-src`, or `default-src` without it. */
|
|
1308
1362
|
const scriptDirectiveOf = (policy) => {
|
|
@@ -1322,15 +1376,16 @@ const elementDirectivesOf = (policy) => {
|
|
|
1322
1376
|
};
|
|
1323
1377
|
/**
|
|
1324
1378
|
* What would make a policy wrong to write into every page, named at once:
|
|
1325
|
-
* a directive a
|
|
1379
|
+
* a directive that works only from a header, a source that is not one, and
|
|
1326
1380
|
* `'strict-dynamic'`, under which the framework's module script — allowed by
|
|
1327
1381
|
* where it comes from, since a file cannot sign it — would never load.
|
|
1328
1382
|
*/
|
|
1329
1383
|
const policyProblems = (policy) => {
|
|
1330
1384
|
const problems = [];
|
|
1331
1385
|
for (const [name, sources] of Object.entries(policy)) {
|
|
1386
|
+
const headerOnly = HEADER_ONLY.get(name);
|
|
1332
1387
|
if (!/^[a-z][a-z-]*$/u.test(name)) problems.push(`"${name}" is not a directive name`);
|
|
1333
|
-
else if (
|
|
1388
|
+
else if (headerOnly !== void 0) problems.push(`${name} ${headerOnly}`);
|
|
1334
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`);
|
|
1335
1390
|
}
|
|
1336
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`);
|
|
@@ -1928,7 +1983,7 @@ const staticMode = (options) => {
|
|
|
1928
1983
|
},
|
|
1929
1984
|
async configurePreviewServer(server) {
|
|
1930
1985
|
const { config } = server;
|
|
1931
|
-
const client = path.resolve(config.root, config.environments
|
|
1986
|
+
const client = path.resolve(config.root, config.environments.client?.build.outDir ?? "dist/client");
|
|
1932
1987
|
const rules = await readRules(client);
|
|
1933
1988
|
const answeredHere = async (request, response) => {
|
|
1934
1989
|
const url = new URL(`${ORIGIN}${request.url ?? "/"}`);
|
|
@@ -1990,7 +2045,7 @@ const staticMode = (options) => {
|
|
|
1990
2045
|
const file = path.relative(routesDir, module).split(path.sep).join("/");
|
|
1991
2046
|
const slot = file.startsWith("..") ? null : slotOf(file);
|
|
1992
2047
|
if (slot === "guard") throw new Error(guardRefusal([path.relative(root, module)]));
|
|
1993
|
-
const names = READ_BY_NAME.has(slot ?? "") ? exportsOf(await readFile(module, "utf8")) : /* @__PURE__ */ new Set();
|
|
2048
|
+
const names = READ_BY_NAME.has(slot ?? "") ? exportsOf(await readFile(module, "utf8"), file) : /* @__PURE__ */ new Set();
|
|
1994
2049
|
if (names.has("*")) throw new Error(reexportRefusal([path.relative(root, module)]));
|
|
1995
2050
|
const methods = slot === "route" ? unwritable(names) : [];
|
|
1996
2051
|
if (methods.length > 0) throw new Error(routeRefusal([[path.relative(root, module), methods]]));
|
|
@@ -2012,8 +2067,8 @@ const staticMode = (options) => {
|
|
|
2012
2067
|
const importers = [...serverImporters].toSorted();
|
|
2013
2068
|
const unanswerable = [...actions.length > 0 ? [actionRefusal(actions)] : [], ...importers.length > 0 ? [serverImportRefusal(importers)] : []];
|
|
2014
2069
|
if (unanswerable.length > 0) throw new Error(unanswerable.join("\n\n"));
|
|
2015
|
-
const rscOut = builder.environments
|
|
2016
|
-
const client = builder.environments
|
|
2070
|
+
const rscOut = builder.environments.rsc?.config.build.outDir;
|
|
2071
|
+
const client = builder.environments.client?.config;
|
|
2017
2072
|
if (rscOut === void 0 || client === void 0) throw new Error("static build ran without the rsc/client builds");
|
|
2018
2073
|
const clientDir = path.resolve(root, client.build.outDir);
|
|
2019
2074
|
const origin = options.site === void 0 ? ORIGIN : new URL(options.site).origin;
|
|
@@ -2033,10 +2088,13 @@ const staticMode = (options) => {
|
|
|
2033
2088
|
const failed = [];
|
|
2034
2089
|
const redirected = /* @__PURE__ */ new Set();
|
|
2035
2090
|
const unanswered = [];
|
|
2091
|
+
const types = /* @__PURE__ */ new Map();
|
|
2036
2092
|
await inParallel(plan.paths.flatMap((pathname) => {
|
|
2037
2093
|
if (byRoute.has(pathname)) return [async () => {
|
|
2038
|
-
const
|
|
2094
|
+
const file = path.join(clientDir, dirFor(pathname));
|
|
2095
|
+
const { status, type } = await writeRoute(file, handler, urlFor(pathname));
|
|
2039
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);
|
|
2040
2098
|
}];
|
|
2041
2099
|
const dir = path.join(clientDir, dirFor(pathname));
|
|
2042
2100
|
return [async () => {
|
|
@@ -2101,9 +2159,10 @@ const staticMode = (options) => {
|
|
|
2101
2159
|
client: clientDir,
|
|
2102
2160
|
base,
|
|
2103
2161
|
rewrites,
|
|
2104
|
-
notFound: unmatched !== null
|
|
2162
|
+
notFound: unmatched !== null,
|
|
2163
|
+
types
|
|
2105
2164
|
});
|
|
2106
|
-
else if (vercel === void 0 && process.env
|
|
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");
|
|
2107
2166
|
const shells = plan.shells.length === 0 ? "" : `, ${String(plan.shells.length)} ${plan.shells.length === 1 ? "shell" : "shells"} (${plan.shells.map((shell) => shell.location).join(", ")})`;
|
|
2108
2167
|
builder.config.logger.info(`k8ordo: wrote ${String(plan.paths.length)} routes${shells}${unmatched === null ? "" : " and 404.html"}${options.site === void 0 ? "" : " and sitemap.xml"}${plan.shells.length === 0 ? "" : " and _redirects"}`);
|
|
2109
2168
|
}
|
|
@@ -2157,19 +2216,27 @@ const write = async (file, handler, url, csp, { notFoundIsPage, shell }) => {
|
|
|
2157
2216
|
return written;
|
|
2158
2217
|
};
|
|
2159
2218
|
/**
|
|
2160
|
-
* Writes what a route.ts's GET answered, as the file at its pathname
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
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.
|
|
2163
2224
|
*/
|
|
2164
2225
|
const writeRoute = async (file, handler, url) => {
|
|
2165
2226
|
const response = await handler(new Request(url));
|
|
2166
2227
|
if (response.status !== 200) {
|
|
2167
2228
|
if (response.status === 500) console.error(`k8ordo: ${url} — ${await response.text()}`);
|
|
2168
|
-
return
|
|
2229
|
+
return {
|
|
2230
|
+
status: response.status,
|
|
2231
|
+
type: null
|
|
2232
|
+
};
|
|
2169
2233
|
}
|
|
2170
2234
|
await mkdir(path.dirname(file), { recursive: true });
|
|
2171
2235
|
await writeFile(file, Buffer.from(await response.arrayBuffer()));
|
|
2172
|
-
return
|
|
2236
|
+
return {
|
|
2237
|
+
status: response.status,
|
|
2238
|
+
type: response.headers.get("content-type")
|
|
2239
|
+
};
|
|
2173
2240
|
};
|
|
2174
2241
|
//#endregion
|
|
2175
2242
|
//#region src/vite.ts
|
package/docs/GUIDE.md
CHANGED
|
@@ -133,10 +133,13 @@ export default defineConfig({ plugins: [framework({ mode: 'static' })] });
|
|
|
133
133
|
// or framework({ mode: 'server' })
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
The framework writes the route table and its types into `.k8ordo
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
The framework writes the route table and its types into `.k8ordo/`,
|
|
137
|
+
including the `Register` augmentations an application would otherwise write
|
|
138
|
+
by hand — the table's, and `@k8ordo/i18n`'s when `src/i18n.ts` exports the
|
|
139
|
+
locale set by the name `locales`. Add it to `include` in `tsconfig.json` as
|
|
140
|
+
a glob, `.k8ordo/**/*.ts` (an entry naming only the dot-directory leaves its
|
|
141
|
+
files out), and `href()`'s paths and a page's `params` are typed, and with
|
|
142
|
+
that export every message's locales. The generated files import without file
|
|
140
143
|
extensions, so they resolve under `moduleResolution: "bundler"`, as Vite
|
|
141
144
|
does. A fresh clone has no `.k8ordo/` until `vite build` or `vite dev`
|
|
142
145
|
writes it, so CI runs the build before `tsc`
|
|
@@ -231,7 +234,9 @@ The package is split by where the code runs.
|
|
|
231
234
|
`dist/server.js` runs, for a server of your own to import. A static build
|
|
232
235
|
it serves as a static host does, its `_redirects` applied.
|
|
233
236
|
- `@k8ordo/framework/vercel` — `vercel()`, the plugin that writes a build
|
|
234
|
-
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`.
|
|
235
240
|
- `@k8ordo/framework/generated` — what the generated `.k8ordo/` files
|
|
236
241
|
import. Not for code written by hand.
|
|
237
242
|
|
|
@@ -242,10 +247,22 @@ dev dependencies.
|
|
|
242
247
|
|
|
243
248
|
## The options
|
|
244
249
|
|
|
245
|
-
`framework()` takes `mode`, which is required,
|
|
246
|
-
directory (default `src/routes`)
|
|
247
|
-
|
|
248
|
-
|
|
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:
|
|
249
266
|
|
|
250
267
|
- `paths` — the pathnames of routes with parameters, and where a
|
|
251
268
|
`fallback.tsx`'s shell stands
|
package/docs/llms.txt
CHANGED
|
@@ -52,14 +52,18 @@ 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
|
|
|
59
61
|
- [Routing](references/routing.md): the `routes/` grammar and every refusal
|
|
60
62
|
the build names, where a `fallback.tsx` may sit, how a page is sent in
|
|
61
63
|
each mode, `loading.tsx`, `route.ts` (methods and `405` under server
|
|
62
|
-
mode, a file under static mode), the generated `.k8ordo/` files
|
|
64
|
+
mode, a file under static mode), the generated `.k8ordo/` files (the
|
|
65
|
+
route table, and the `Register`s: the table's, and `@k8ordo/i18n`'s when
|
|
66
|
+
`src/i18n.ts` exports `locales` by name), titles,
|
|
63
67
|
and fetching the next page ahead (`data-k8ordo-prefetch`).
|
|
64
68
|
- [Parameters](references/params.md): `paramsSchema` — a refused value is a
|
|
65
69
|
pathname the pattern does not answer — and, under static mode, the `paths`
|
|
@@ -101,7 +105,8 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
101
105
|
`node dist/server.js` (`PORT`, `HOST`, every dependency bundled in, and
|
|
102
106
|
`resolve.external` for one that cannot be), `serve` (compression, `ETag`,
|
|
103
107
|
`Range`), the request handler under Node.js, Bun, Deno and Cloudflare
|
|
104
|
-
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;
|
|
105
110
|
a tab opened before a deploy, and Vite's `base`.
|
|
106
111
|
|
|
107
112
|
## Entry points
|
|
@@ -128,7 +133,7 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
128
133
|
`<head>`, with the hashes of the framework's own inline scripts on that
|
|
129
134
|
page added to `script-src` (or `default-src` made into it) and
|
|
130
135
|
`script-src-elem`; a policy with `'strict-dynamic'`, `frame-ancestors`,
|
|
131
|
-
`report-uri` or `sandbox` is refused. `FrameworkOptions` is
|
|
136
|
+
`report-uri`, `report-to` or `sandbox` is refused. `FrameworkOptions` is
|
|
132
137
|
`StaticOptions | ServerOptions`. This entry loads Vite and is for
|
|
133
138
|
`vite.config.ts` only.
|
|
134
139
|
- `@k8ordo/framework/server` — server mode only; a static build refuses any
|
|
@@ -177,7 +182,15 @@ Start with GUIDE.md, then follow the reference most relevant to the task.
|
|
|
177
182
|
`.vercel/output/` (Build Output API v3) for `vercel build` /
|
|
178
183
|
`vercel deploy --prebuilt` — the client build as static files at Vite's
|
|
179
184
|
`base` (`assets/` immutable), and under server mode the request handler
|
|
180
|
-
as one streaming Node.js function; under static mode no function,
|
|
181
|
-
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/`).
|
|
182
195
|
- `@k8ordo/framework/generated` — what the generated `.k8ordo/` files
|
|
183
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
|