@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/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 BANNER = `// Generated by ${FRAMEWORK} from routes/. Do not edit.`;
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
- BANNER,
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
- BANNER,
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. A file that does not
859
- * parse exports nothing; the build reports the syntax error itself, where it
860
- * can name the line.
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("route.tsx", source, { sourceType: "module" });
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
- const dependsOnState = async (root) => {
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 "@k8ordo/state" in (manifest.dependencies ?? {}) || "@k8ordo/state" in (manifest.devDependencies ?? {});
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 dependsOnState(options.root);
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 = path.relative(options.outDir, options.routesDir).replaceAll(path.sep, "/");
971
+ const toRoutes = importPath(options.outDir, options.routesDir);
972
+ const localeSet = await localeSetOf(options.root);
934
973
  const routesSource = emitRoutesModule(tree, {
935
- importPrefix: toRoutes.startsWith(".") ? toRoutes : `./${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["NODE_ENV"] ?? process.env["VITE_USER_NODE_ENV"] ?? mode) === "production";
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
- if (!path.resolve(file).startsWith(`${routesDir}${path.sep}`)) return;
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["client"]?.config.build.outDir;
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 HEADER_ONLY = /* @__PURE__ */ new Set([
1303
- "frame-ancestors",
1304
- "report-uri",
1305
- "sandbox"
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 `<meta>` ignores, a source that is not one, and
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 (HEADER_ONLY.has(name)) problems.push(`${name} is ignored in a <meta> — set it as a header at the host`);
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["client"]?.build.outDir ?? "dist/client");
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["rsc"]?.config.build.outDir;
2016
- const client = builder.environments["client"]?.config;
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 status = await writeRoute(path.join(clientDir, dirFor(pathname)), handler, urlFor(pathname));
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["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");
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 — a
2161
- * static host then serves it with the type its extension says. Anything but a
2162
- * 200 is not a file the site has, and nothing is written.
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 response.status;
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 response.status;
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/`. Add it
137
- to `include` in `tsconfig.json` as a glob, `.k8ordo/**/*.ts` (an entry
138
- naming only the dot-directory leaves its files out), and `href()`'s paths
139
- and a page's `params` are typed. The generated files import without file
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, and `routesDir`, the route
246
- directory (default `src/routes`). Three more describe files, so only
247
- `mode: 'static'` takes them — passing them with `mode: 'server'` is a type
248
- error:
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, titles,
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, and the
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.
@@ -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. Without `csp`, no policy
95
- is written. There is no `nonce()` for this mode: what an application signed
96
- with it would land in the file, and make every build differ.
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