@pyreon/zero 0.16.0 → 0.19.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/lib/{api-routes-Ci0kVmM4.js → api-routes-CQiOi3q5.js} +5 -3
- package/lib/api-routes.js +4 -2
- package/lib/{fs-router-MewHc5SB.js → fs-router-BVY4lTH_.js} +4 -3
- package/lib/image-plugin.js +77 -13
- package/lib/index.js +96 -3
- package/lib/rate-limit.js +5 -0
- package/lib/seo.js +11 -6
- package/lib/server.js +229 -25
- package/lib/testing.js +4 -2
- package/lib/types/config.d.ts +41 -0
- package/lib/types/image-plugin.d.ts +65 -7
- package/lib/types/index.d.ts +120 -2
- package/lib/types/server.d.ts +119 -1
- package/lib/{vite-plugin-xjWZwudX.js → vite-plugin-8TXXFqdP.js} +51 -14
- package/package.json +10 -10
- package/src/api-routes.ts +12 -2
- package/src/fs-router.ts +7 -1
- package/src/icon.tsx +182 -0
- package/src/icons-plugin.ts +296 -0
- package/src/image-plugin.ts +157 -20
- package/src/index.ts +2 -0
- package/src/isr.ts +54 -10
- package/src/manifest.ts +99 -0
- package/src/rate-limit.ts +16 -0
- package/src/seo.ts +19 -4
- package/src/server.ts +2 -0
- package/src/sharp.d.ts +6 -0
- package/src/ssg-plugin.ts +47 -8
- package/src/types.ts +41 -0
- package/src/vite-plugin.ts +63 -11
package/lib/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-
|
|
2
|
-
import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { _ as render404Page, a as detectLocaleFromHeader, c as vercelAdapter, d as netlifyAdapter, f as cloudflareAdapter, g as createServer, h as resolveConfig, i as createLocaleContext, l as staticAdapter, m as defineConfig, o as i18nRouting, p as bunAdapter, r as zeroPlugin, s as resolveAdapter, t as getZeroPluginConfig, u as nodeAdapter, v as createApp } from "./vite-plugin-8TXXFqdP.js";
|
|
2
|
+
import { i as generateRouteModule, o as parseFileRoutes, r as generateMiddlewareModule, s as scanRouteFiles, t as filePathToUrlPath } from "./fs-router-BVY4lTH_.js";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
|
|
7
7
|
//#region src/isr.ts
|
|
@@ -22,6 +22,10 @@ function createISRHandler(handler, config) {
|
|
|
22
22
|
const cache = /* @__PURE__ */ new Map();
|
|
23
23
|
const revalidating = /* @__PURE__ */ new Set();
|
|
24
24
|
const revalidateMs = config.revalidate * 1e3;
|
|
25
|
+
const REVALIDATE_TIMEOUT_MS = Math.max(1, config.revalidateTimeoutMs ?? 3e4);
|
|
26
|
+
function isCacheable(res) {
|
|
27
|
+
return res.status >= 200 && res.status < 300 && !res.headers.has("set-cookie");
|
|
28
|
+
}
|
|
25
29
|
const maxEntries = Math.max(1, config.maxEntries ?? 1e3);
|
|
26
30
|
const deriveKey = typeof config.cacheKey === "function" ? (req, _url) => config.cacheKey(req) : (_req, url) => url.pathname;
|
|
27
31
|
function set(key, entry) {
|
|
@@ -46,20 +50,23 @@ function createISRHandler(handler, config) {
|
|
|
46
50
|
if (revalidating.has(key)) return;
|
|
47
51
|
revalidating.add(key);
|
|
48
52
|
try {
|
|
49
|
-
const
|
|
53
|
+
const req = new Request(url.href, {
|
|
50
54
|
method: "GET",
|
|
51
55
|
headers: originalReq.headers
|
|
52
|
-
}));
|
|
53
|
-
const html = await res.text();
|
|
54
|
-
const headers = {};
|
|
55
|
-
res.headers.forEach((v, k) => {
|
|
56
|
-
headers[k] = v;
|
|
57
|
-
});
|
|
58
|
-
set(key, {
|
|
59
|
-
html,
|
|
60
|
-
headers,
|
|
61
|
-
timestamp: Date.now()
|
|
62
56
|
});
|
|
57
|
+
const res = await Promise.race([handler(req), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("[Pyreon ISR] revalidation timeout")), REVALIDATE_TIMEOUT_MS))]);
|
|
58
|
+
if (isCacheable(res)) {
|
|
59
|
+
const html = await res.text();
|
|
60
|
+
const headers = {};
|
|
61
|
+
res.headers.forEach((v, k) => {
|
|
62
|
+
headers[k] = v;
|
|
63
|
+
});
|
|
64
|
+
set(key, {
|
|
65
|
+
html,
|
|
66
|
+
headers,
|
|
67
|
+
timestamp: Date.now()
|
|
68
|
+
});
|
|
69
|
+
}
|
|
63
70
|
} catch {} finally {
|
|
64
71
|
revalidating.delete(key);
|
|
65
72
|
}
|
|
@@ -88,6 +95,14 @@ function createISRHandler(handler, config) {
|
|
|
88
95
|
res.headers.forEach((v, k) => {
|
|
89
96
|
headers[k] = v;
|
|
90
97
|
});
|
|
98
|
+
if (!isCacheable(res)) return new Response(html, {
|
|
99
|
+
status: res.status,
|
|
100
|
+
statusText: res.statusText,
|
|
101
|
+
headers: {
|
|
102
|
+
...headers,
|
|
103
|
+
"x-isr-cache": "BYPASS"
|
|
104
|
+
}
|
|
105
|
+
});
|
|
91
106
|
set(key, {
|
|
92
107
|
html,
|
|
93
108
|
headers,
|
|
@@ -817,6 +832,190 @@ async function addDevBadgeToPng(pngBuffer, size) {
|
|
|
817
832
|
}
|
|
818
833
|
}
|
|
819
834
|
|
|
835
|
+
//#endregion
|
|
836
|
+
//#region src/icons-plugin.ts
|
|
837
|
+
/** Set key → exported component name. `ui` → `UiIcon`, `brand-marks` → `BrandMarksIcon`. */
|
|
838
|
+
function componentNameFromSetKey(key) {
|
|
839
|
+
const safe = key.split(/[-_/\s]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("").replace(/[^A-Za-z0-9_$]/g, "");
|
|
840
|
+
return `${/^[A-Za-z_$]/.test(safe) ? safe : `Set${safe}`}Icon`;
|
|
841
|
+
}
|
|
842
|
+
/** Filename stem → registry key. `Check-Circle.svg` → `check-circle`. */
|
|
843
|
+
function iconNameFromFile(file) {
|
|
844
|
+
return basename(file, ".svg").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
845
|
+
}
|
|
846
|
+
/** Registry key → safe JS import binding. `check-circle` → `checkCircle`. */
|
|
847
|
+
function bindingFromName(name) {
|
|
848
|
+
const safe = name.replace(/[-/](.)/g, (_, c) => c.toUpperCase()).replace(/[^A-Za-z0-9_$]/g, "_");
|
|
849
|
+
return /^[A-Za-z_$]/.test(safe) ? safe : `_${safe}`;
|
|
850
|
+
}
|
|
851
|
+
/** List the `*.svg` filenames in `dir` (sorted, stable). Empty if missing. */
|
|
852
|
+
function scanIconDir(dir) {
|
|
853
|
+
if (!existsSync(dir)) return [];
|
|
854
|
+
return readdirSync(dir).filter((f) => f.toLowerCase().endsWith(".svg")).sort();
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Render the generated `.tsx` source for a set of svg filenames. Pure —
|
|
858
|
+
* unit-tested directly; the plugin only adds fs + watch around it.
|
|
859
|
+
*/
|
|
860
|
+
function generateIconSetSource(files, opts) {
|
|
861
|
+
const query = opts.mode === "image" ? "" : "?raw";
|
|
862
|
+
const seen = /* @__PURE__ */ new Map();
|
|
863
|
+
const entries = [];
|
|
864
|
+
for (const file of files) {
|
|
865
|
+
const key = iconNameFromFile(file);
|
|
866
|
+
let binding = bindingFromName(key);
|
|
867
|
+
while (seen.has(binding)) binding = `${binding}_`;
|
|
868
|
+
seen.set(binding, key);
|
|
869
|
+
entries.push({
|
|
870
|
+
key,
|
|
871
|
+
binding,
|
|
872
|
+
file
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
const header = [
|
|
876
|
+
"// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.",
|
|
877
|
+
`// Add / remove .svg files in ${opts.importDir} and this regenerates.`,
|
|
878
|
+
"/// <reference types=\"vite/client\" />",
|
|
879
|
+
"import { createNamedIcon } from '@pyreon/zero'"
|
|
880
|
+
];
|
|
881
|
+
const imports = entries.map((e) => `import ${e.binding} from '${opts.importDir}/${e.file}${query}'`);
|
|
882
|
+
const registry = [
|
|
883
|
+
"const REGISTRY = {",
|
|
884
|
+
...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
|
|
885
|
+
"} as const"
|
|
886
|
+
];
|
|
887
|
+
const tail = [
|
|
888
|
+
"export type IconName = keyof typeof REGISTRY",
|
|
889
|
+
`export const Icon = createNamedIcon(REGISTRY${opts.mode === "image" ? ", { mode: 'image' }" : ""})`,
|
|
890
|
+
""
|
|
891
|
+
];
|
|
892
|
+
return [
|
|
893
|
+
...header,
|
|
894
|
+
"",
|
|
895
|
+
...imports,
|
|
896
|
+
"",
|
|
897
|
+
...registry,
|
|
898
|
+
"",
|
|
899
|
+
...tail
|
|
900
|
+
].join("\n");
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Render the generated `.tsx` for the NAMED MULTI-SET form. One file, one
|
|
904
|
+
* `createNamedIcon` import, one strictly-typed component PER set with
|
|
905
|
+
* namespaced types (`UiIcon`/`UiIconName`, `BrandIcon`/`BrandIconName`) so
|
|
906
|
+
* sets never clash. Bindings are per-set-prefixed so two sets sharing a
|
|
907
|
+
* glyph filename don't collide.
|
|
908
|
+
*/
|
|
909
|
+
function generateNamedIconSetsSource(sets) {
|
|
910
|
+
const header = [
|
|
911
|
+
"// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.",
|
|
912
|
+
"// Add / remove .svg files in the configured set folders and this regenerates.",
|
|
913
|
+
"/// <reference types=\"vite/client\" />",
|
|
914
|
+
"import { createNamedIcon } from '@pyreon/zero'"
|
|
915
|
+
];
|
|
916
|
+
const blocks = [];
|
|
917
|
+
for (const set of sets) {
|
|
918
|
+
const component = componentNameFromSetKey(set.key);
|
|
919
|
+
const typeName = `${component}Name`;
|
|
920
|
+
const registry = `${component}_REGISTRY`;
|
|
921
|
+
const query = set.mode === "image" ? "" : "?raw";
|
|
922
|
+
const seen = /* @__PURE__ */ new Set();
|
|
923
|
+
const entries = [];
|
|
924
|
+
for (const file of set.files) {
|
|
925
|
+
const k = iconNameFromFile(file);
|
|
926
|
+
let binding = `${bindingFromName(set.key)}_${bindingFromName(k)}`;
|
|
927
|
+
while (seen.has(binding)) binding = `${binding}_`;
|
|
928
|
+
seen.add(binding);
|
|
929
|
+
entries.push({
|
|
930
|
+
key: k,
|
|
931
|
+
binding,
|
|
932
|
+
file
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
const imports = entries.map((e) => `import ${e.binding} from '${set.importDir}/${e.file}${query}'`);
|
|
936
|
+
blocks.push([
|
|
937
|
+
`// ── set "${set.key}" → <${component} name="…" /> ──`,
|
|
938
|
+
...imports,
|
|
939
|
+
`const ${registry} = {`,
|
|
940
|
+
...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
|
|
941
|
+
"} as const",
|
|
942
|
+
`export type ${typeName} = keyof typeof ${registry}`,
|
|
943
|
+
`export const ${component} = createNamedIcon(${registry}${set.mode === "image" ? ", { mode: 'image' }" : ""})`
|
|
944
|
+
].join("\n"));
|
|
945
|
+
}
|
|
946
|
+
return [
|
|
947
|
+
...header,
|
|
948
|
+
"",
|
|
949
|
+
blocks.join("\n\n"),
|
|
950
|
+
""
|
|
951
|
+
].join("\n");
|
|
952
|
+
}
|
|
953
|
+
function resolveOut(cfg, root) {
|
|
954
|
+
if (cfg.out) return join(root, cfg.out);
|
|
955
|
+
if (cfg.dir) {
|
|
956
|
+
const dir = join(root, cfg.dir);
|
|
957
|
+
return join(dirname(dir), `${basename(dir)}.gen.tsx`);
|
|
958
|
+
}
|
|
959
|
+
return join(root, "src", "icons.gen.tsx");
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Vite plugin: scan `dir` for `*.svg`, write a strictly-typed
|
|
963
|
+
* `icons.gen.tsx`, regenerate on add / unlink in dev.
|
|
964
|
+
*/
|
|
965
|
+
function iconsPlugin(cfg) {
|
|
966
|
+
const hasDir = typeof cfg.dir === "string";
|
|
967
|
+
const hasSets = !!cfg.sets && Object.keys(cfg.sets).length > 0;
|
|
968
|
+
if (hasDir === hasSets) throw new Error("[Pyreon] iconsPlugin: provide EXACTLY ONE of `dir` (single set) or `sets` (named multi-set). " + (hasDir ? "Both were given." : "Neither was given (or `sets` is empty)."));
|
|
969
|
+
let root = process.cwd();
|
|
970
|
+
const mode = cfg.mode ?? "inline";
|
|
971
|
+
/** Relative `./…` import dir from the generated file to a scanned folder. */
|
|
972
|
+
function rel(out, scanned) {
|
|
973
|
+
const r = relative(dirname(out), scanned).split("\\").join("/");
|
|
974
|
+
return r.startsWith(".") ? r : `./${r}`;
|
|
975
|
+
}
|
|
976
|
+
async function regenerate() {
|
|
977
|
+
const out = resolveOut(cfg, root);
|
|
978
|
+
let source;
|
|
979
|
+
if (hasSets) source = generateNamedIconSetsSource(Object.entries(cfg.sets ?? {}).map(([key, sc]) => {
|
|
980
|
+
const scanned = join(root, sc.dir);
|
|
981
|
+
return {
|
|
982
|
+
key,
|
|
983
|
+
files: scanIconDir(scanned),
|
|
984
|
+
mode: sc.mode ?? "inline",
|
|
985
|
+
importDir: rel(out, scanned)
|
|
986
|
+
};
|
|
987
|
+
}));
|
|
988
|
+
else {
|
|
989
|
+
const scanned = join(root, cfg.dir);
|
|
990
|
+
source = generateIconSetSource(scanIconDir(scanned), {
|
|
991
|
+
mode,
|
|
992
|
+
importDir: rel(out, scanned)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if ((existsSync(out) ? await readFile(out, "utf8") : null) !== source) await writeFile(out, source, "utf8");
|
|
996
|
+
}
|
|
997
|
+
const watchDirs = () => hasSets ? Object.values(cfg.sets ?? {}).map((s) => join(root, s.dir)) : [join(root, cfg.dir)];
|
|
998
|
+
return {
|
|
999
|
+
name: "pyreon:zero-icons",
|
|
1000
|
+
async configResolved(resolved) {
|
|
1001
|
+
root = resolved.root;
|
|
1002
|
+
await regenerate();
|
|
1003
|
+
},
|
|
1004
|
+
async buildStart() {
|
|
1005
|
+
await regenerate();
|
|
1006
|
+
},
|
|
1007
|
+
configureServer(server) {
|
|
1008
|
+
const dirs = watchDirs();
|
|
1009
|
+
for (const d of dirs) server.watcher.add(d);
|
|
1010
|
+
const onChange = (file) => {
|
|
1011
|
+
if (file.toLowerCase().endsWith(".svg") && dirs.some((d) => file.startsWith(d))) regenerate();
|
|
1012
|
+
};
|
|
1013
|
+
server.watcher.on("add", onChange);
|
|
1014
|
+
server.watcher.on("unlink", onChange);
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
820
1019
|
//#endregion
|
|
821
1020
|
//#region src/seo.ts
|
|
822
1021
|
/**
|
|
@@ -830,7 +1029,7 @@ async function addDevBadgeToPng(pngBuffer, size) {
|
|
|
830
1029
|
*/
|
|
831
1030
|
function generateSitemap(routeFiles, config, i18n) {
|
|
832
1031
|
const { origin, exclude = [], changefreq = "weekly", priority = .7 } = config;
|
|
833
|
-
const
|
|
1032
|
+
const paths = routeFiles.filter((f) => {
|
|
834
1033
|
const name = f.split("/").pop()?.replace(/\.\w+$/, "");
|
|
835
1034
|
return name !== "_layout" && name !== "_error" && name !== "_loading";
|
|
836
1035
|
}).map((f) => {
|
|
@@ -839,11 +1038,16 @@ function generateSitemap(routeFiles, config, i18n) {
|
|
|
839
1038
|
path = path.replace(/\([\w-]+\)\//g, "");
|
|
840
1039
|
if (!path.startsWith("/")) path = `/${path}`;
|
|
841
1040
|
return path;
|
|
842
|
-
}).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e)))
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1041
|
+
}).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e)));
|
|
1042
|
+
const clusters = clusterPathsByLocale((() => {
|
|
1043
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1044
|
+
for (const e of [...paths.map((p) => ({
|
|
1045
|
+
path: p,
|
|
1046
|
+
changefreq,
|
|
1047
|
+
priority
|
|
1048
|
+
})), ...config.additionalPaths ?? []]) if (!byPath.has(e.path)) byPath.set(e.path, e);
|
|
1049
|
+
return [...byPath.values()];
|
|
1050
|
+
})(), i18n);
|
|
847
1051
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
848
1052
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${i18n != null && i18n.locales.length > 0 ? " xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"" : ""}>
|
|
849
1053
|
${clusters.map((cluster) => renderClusterEntry(cluster, origin, changefreq, priority, i18n)).join("\n")}
|
|
@@ -1049,7 +1253,7 @@ function seoPlugin(config = {}) {
|
|
|
1049
1253
|
},
|
|
1050
1254
|
async generateBundle(_, _bundle) {
|
|
1051
1255
|
if (config.sitemap && !useSsgPaths) {
|
|
1052
|
-
const { scanRouteFiles } = await import("./fs-router-
|
|
1256
|
+
const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
|
|
1053
1257
|
const routesDir = `${process.cwd()}/src/routes`;
|
|
1054
1258
|
try {
|
|
1055
1259
|
const files = await scanRouteFiles(routesDir);
|
|
@@ -1073,7 +1277,7 @@ function seoPlugin(config = {}) {
|
|
|
1073
1277
|
},
|
|
1074
1278
|
async closeBundle() {
|
|
1075
1279
|
if (!config.sitemap || !useSsgPaths) return;
|
|
1076
|
-
const { scanRouteFiles } = await import("./fs-router-
|
|
1280
|
+
const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
|
|
1077
1281
|
const routesDir = `${process.cwd()}/src/routes`;
|
|
1078
1282
|
const manifestPath = join(distDir, "_pyreon-ssg-paths.json");
|
|
1079
1283
|
try {
|
|
@@ -1111,7 +1315,7 @@ function seoMiddleware(config = {}) {
|
|
|
1111
1315
|
return async (ctx) => {
|
|
1112
1316
|
if (ctx.url.pathname === "/robots.txt" && config.robots) return new Response(generateRobots(config.robots), { headers: { "Content-Type": "text/plain" } });
|
|
1113
1317
|
if (ctx.url.pathname === "/sitemap.xml" && config.sitemap) try {
|
|
1114
|
-
const { scanRouteFiles } = await import("./fs-router-
|
|
1318
|
+
const { scanRouteFiles } = await import("./fs-router-BVY4lTH_.js").then((n) => n.n);
|
|
1115
1319
|
const sitemap = generateSitemap(await scanRouteFiles(`${process.cwd()}/src/routes`), config.sitemap);
|
|
1116
1320
|
return new Response(sitemap, { headers: { "Content-Type": "application/xml" } });
|
|
1117
1321
|
} catch {}
|
|
@@ -1756,5 +1960,5 @@ function capitalize(s) {
|
|
|
1756
1960
|
}
|
|
1757
1961
|
|
|
1758
1962
|
//#endregion
|
|
1759
|
-
export { _resetVercelRevalidateHandlerCache, aiPlugin, bunAdapter, cloudflareAdapter, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateRobots, generateRouteModule, generateSitemap, getContext, getZeroPluginConfig, i18nRouting, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter, vercelRevalidateHandler };
|
|
1963
|
+
export { _resetVercelRevalidateHandlerCache, aiPlugin, bunAdapter, cloudflareAdapter, componentNameFromSetKey, compose, createApp, createISRHandler, createLocaleContext, createServer, zeroPlugin as default, defineConfig, detectLocaleFromHeader, faviconLinks, faviconPlugin, filePathToUrlPath, generateIconSetSource, generateLlmsFullTxt, generateLlmsTxt, generateMiddlewareModule, generateNamedIconSetsSource, generateRobots, generateRouteModule, generateSitemap, getContext, getZeroPluginConfig, i18nRouting, iconNameFromFile, iconsPlugin, inferJsonLd, jsonLd, netlifyAdapter, nodeAdapter, ogImagePath, ogImagePlugin, parseFileRoutes, render404Page, resolveAdapter, resolveConfig, scanIconDir, scanRouteFiles, seoMiddleware, seoPlugin, staticAdapter, vercelAdapter, vercelRevalidateHandler };
|
|
1760
1964
|
//# sourceMappingURL=server.js.map
|
package/lib/testing.js
CHANGED
|
@@ -7,17 +7,19 @@ function matchApiRoute(pattern, path) {
|
|
|
7
7
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
8
8
|
const pathParts = path.split("/").filter(Boolean);
|
|
9
9
|
const params = {};
|
|
10
|
+
const isUnsafeParam = (name) => name === "__proto__" || name === "constructor" || name === "prototype";
|
|
10
11
|
for (let i = 0; i < patternParts.length; i++) {
|
|
11
12
|
const pp = patternParts[i];
|
|
12
13
|
if (!pp) continue;
|
|
13
14
|
if (pp.endsWith("*")) {
|
|
14
15
|
const paramName = pp.slice(1, -1);
|
|
15
|
-
params[paramName] = pathParts.slice(i).join("/");
|
|
16
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts.slice(i).join("/");
|
|
16
17
|
return params;
|
|
17
18
|
}
|
|
18
19
|
if (i >= pathParts.length) return null;
|
|
19
20
|
if (pp.startsWith(":")) {
|
|
20
|
-
|
|
21
|
+
const paramName = pp.slice(1);
|
|
22
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts[i];
|
|
21
23
|
continue;
|
|
22
24
|
}
|
|
23
25
|
if (pp !== pathParts[i]) return null;
|
package/lib/types/config.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ interface ISRConfig {
|
|
|
25
25
|
* space (e.g. `/user/:id` where `:id` is free-form).
|
|
26
26
|
*/
|
|
27
27
|
maxEntries?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Max wall-time (ms) for a single background revalidation before it is
|
|
30
|
+
* abandoned. Without a bound, a handler that hangs leaves its key
|
|
31
|
+
* pinned in the in-flight set forever — every later request for that
|
|
32
|
+
* key short-circuits the de-dupe guard and the entry can never
|
|
33
|
+
* recover from stale. Default: `30000` (matches the Suspense
|
|
34
|
+
* streaming timeout).
|
|
35
|
+
*/
|
|
36
|
+
revalidateTimeoutMs?: number;
|
|
28
37
|
/**
|
|
29
38
|
* Cache-key derivation function. The default keys cache entries by
|
|
30
39
|
* `url.pathname` ONLY — query strings, cookies, and headers are
|
|
@@ -210,6 +219,38 @@ interface ZeroConfig {
|
|
|
210
219
|
currentPath: string;
|
|
211
220
|
elapsed: number;
|
|
212
221
|
}) => void | Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Route-level code splitting in SSG mode. Default `true`.
|
|
224
|
+
*
|
|
225
|
+
* When `true` (default), each route file becomes its own dynamic-import
|
|
226
|
+
* chunk via `lazy(() => import("..."))` — only the route the user
|
|
227
|
+
* lands on plus its dependencies ship in the initial bundle, the
|
|
228
|
+
* rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
|
|
229
|
+
* has always had; brings parity to SSG.
|
|
230
|
+
*
|
|
231
|
+
* When `false`, every route is bundled statically into the main
|
|
232
|
+
* client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
|
|
233
|
+
* sites (2-5 pages) where the single-chunk-then-instant-nav trade
|
|
234
|
+
* is preferable — the chunk-fetch cost on navigation is gone, and
|
|
235
|
+
* the marginal bytes are negligible.
|
|
236
|
+
*
|
|
237
|
+
* Crossover point: ~5-8 routes. Below that, single-chunk is fine.
|
|
238
|
+
* Above that, lazy() shrinks the initial bundle by a meaningful
|
|
239
|
+
* amount (a 50-route docs site might drop from 200 KB to 80 KB on
|
|
240
|
+
* first paint).
|
|
241
|
+
*
|
|
242
|
+
* Underlying mechanism is the same 3-tier generator zero already
|
|
243
|
+
* uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
|
|
244
|
+
* component + inlined metadata when possible, lazy + lazy-thunked
|
|
245
|
+
* function exports when not, namespace-import fallback for cases
|
|
246
|
+
* the literal-extractor can't reach.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ssg: {
|
|
250
|
+
* splitChunks: false, // bundle-everything for a 3-page marketing site
|
|
251
|
+
* }
|
|
252
|
+
*/
|
|
253
|
+
splitChunks?: boolean;
|
|
213
254
|
};
|
|
214
255
|
/** ISR config — only used when mode is "isr". */
|
|
215
256
|
isr?: ISRConfig;
|
|
@@ -17,8 +17,32 @@ declare const cdnProviders: {
|
|
|
17
17
|
readonly vercel: () => ImageCdnProvider; /** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */
|
|
18
18
|
readonly bunny: (pullZone: string) => ImageCdnProvider;
|
|
19
19
|
};
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Placeholder generation strategy.
|
|
22
|
+
*
|
|
23
|
+
* - `'blur'` — tiny downscaled + blurred WebP data URI (a few hundred bytes).
|
|
24
|
+
* The richest preview; faithfully previews the image's content.
|
|
25
|
+
* - `'color'` — the image's dominant colour as a ~200-byte flat SVG data
|
|
26
|
+
* URI. Constant size regardless of source complexity (a blurred WebP
|
|
27
|
+
* grows with image content; this doesn't), zero decode, instant paint,
|
|
28
|
+
* zero layout shift. For real photos it's far smaller than `'blur'`; for
|
|
29
|
+
* trivial/solid sources `'blur'` can be the smaller of the two. Best when
|
|
30
|
+
* you want a clean solid backdrop rather than a blurry preview.
|
|
31
|
+
* - `'none'` — no placeholder (`placeholder: ''`). Skips all placeholder work.
|
|
32
|
+
*
|
|
33
|
+
* `'dominant-color'` is a deprecated alias of `'color'` — it was typed from
|
|
34
|
+
* the plugin's inception but never implemented (the build + dev paths always
|
|
35
|
+
* fell through to blur). It now resolves to `'color'`; prefer the shorter
|
|
36
|
+
* name in new code.
|
|
37
|
+
*/
|
|
38
|
+
type PlaceholderStrategy = 'blur' | 'color' | 'dominant-color' | 'none';
|
|
39
|
+
/** Quality per output format (1-100), or a single number applied to all. */
|
|
40
|
+
type ImageQuality = number | Partial<Record<ImageFormat, number>>;
|
|
41
|
+
/**
|
|
42
|
+
* Normalize the public {@link PlaceholderStrategy} to an internal kind.
|
|
43
|
+
* @internal Exported for testing.
|
|
44
|
+
*/
|
|
45
|
+
declare function normalizePlaceholder(s: PlaceholderStrategy): 'blur' | 'color' | 'none';
|
|
22
46
|
/** SVG processing options for ?component imports. */
|
|
23
47
|
interface SvgOptions {
|
|
24
48
|
/** Replace fill/stroke with currentColor. Default: true */
|
|
@@ -33,11 +57,23 @@ interface ImagePluginConfig {
|
|
|
33
57
|
widths?: number[];
|
|
34
58
|
/** Output formats. Default: ["webp"] */
|
|
35
59
|
formats?: ImageFormat[];
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Quality for lossy formats (1-100). Default: 80.
|
|
62
|
+
*
|
|
63
|
+
* Accepts a single number applied to every format, OR a per-format map so
|
|
64
|
+
* you can tune each codec independently — AVIF tolerates a much lower
|
|
65
|
+
* number than WebP/JPEG for the same perceived quality:
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* imagePlugin({ formats: ['avif', 'webp'], quality: { avif: 55, webp: 75 } })
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Formats omitted from the map fall back to 80.
|
|
72
|
+
*/
|
|
73
|
+
quality?: ImageQuality;
|
|
74
|
+
/** Blur placeholder size in px (only used by the `'blur'` strategy). Default: 16 */
|
|
39
75
|
placeholderSize?: number;
|
|
40
|
-
/** Placeholder strategy. Default: "blur" */
|
|
76
|
+
/** Placeholder strategy. Default: `"blur"`. See {@link PlaceholderStrategy}. */
|
|
41
77
|
placeholder?: PlaceholderStrategy;
|
|
42
78
|
/** File patterns to process. Default: /\.(jpe?g|png|webp|avif)$/i */
|
|
43
79
|
include?: RegExp;
|
|
@@ -126,6 +162,28 @@ declare function parseWebPDimensions(buffer: Buffer): {
|
|
|
126
162
|
width: number;
|
|
127
163
|
height: number;
|
|
128
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
* Resolve the public {@link ImageQuality} config into a per-format lookup.
|
|
167
|
+
*
|
|
168
|
+
* - `undefined` → every format gets {@link DEFAULT_QUALITY}.
|
|
169
|
+
* - `number` → that number for every format (backward-compatible).
|
|
170
|
+
* - `Partial<Record<ImageFormat, number>>` → per-format; formats omitted
|
|
171
|
+
* from the map fall back to {@link DEFAULT_QUALITY}.
|
|
172
|
+
*
|
|
173
|
+
* @internal Exported for testing.
|
|
174
|
+
*/
|
|
175
|
+
declare function resolveQuality(q: ImageQuality | undefined): (format: ImageFormat) => number;
|
|
176
|
+
/**
|
|
177
|
+
* Dispatch placeholder generation by strategy. Single source of truth used
|
|
178
|
+
* by every code path (CDN / dev / build) — pre-fix each path open-coded
|
|
179
|
+
* `generateBlurPlaceholder`, so `'none'` was honoured only in the CDN path
|
|
180
|
+
* and `'dominant-color'` (typed since the plugin's inception) was never
|
|
181
|
+
* implemented anywhere — the exact typed-but-unimplemented bug class the
|
|
182
|
+
* `audit-types` gate exists to catch.
|
|
183
|
+
*
|
|
184
|
+
* @internal Exported for testing.
|
|
185
|
+
*/
|
|
186
|
+
declare function generatePlaceholder(input: string, strategy: 'blur' | 'color' | 'none', size: number): Promise<string>;
|
|
129
187
|
//#endregion
|
|
130
|
-
export { FormatSource, ImageCdnProvider, ImageFormat, ImagePluginConfig, PlaceholderStrategy, ProcessedImage, SvgOptions, cdnProviders, imagePlugin, parseJpegDimensions, parseWebPDimensions };
|
|
188
|
+
export { FormatSource, ImageCdnProvider, ImageFormat, ImagePluginConfig, ImageQuality, PlaceholderStrategy, ProcessedImage, SvgOptions, cdnProviders, generatePlaceholder, imagePlugin, normalizePlaceholder, parseJpegDimensions, parseWebPDimensions, resolveQuality };
|
|
131
189
|
//# sourceMappingURL=image-plugin2.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,9 +1,86 @@
|
|
|
1
1
|
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
|
-
import { ComponentFn, Ref, VNodeChild } from "@pyreon/core";
|
|
2
|
+
import { ComponentFn, Ref, SvgAttributes, VNodeChild } from "@pyreon/core";
|
|
3
3
|
import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
|
|
4
4
|
import { LoaderContext, NavigationGuard } from "@pyreon/router";
|
|
5
5
|
import { Middleware } from "@pyreon/server";
|
|
6
6
|
|
|
7
|
+
//#region src/icon.d.ts
|
|
8
|
+
/** An imported SVG component (`import X from './x.svg?component'`). */
|
|
9
|
+
type SvgComponent = (props: SvgAttributes) => VNodeChild;
|
|
10
|
+
/**
|
|
11
|
+
* Props for {@link Icon}. The standard `<svg>` attribute surface
|
|
12
|
+
* (`fill`, `class`, `style`, `aria-*`, `onClick`, …) — every one passed
|
|
13
|
+
* straight through and overriding the container-fill defaults — plus the
|
|
14
|
+
* two source props.
|
|
15
|
+
*/
|
|
16
|
+
interface IconProps extends SvgAttributes {
|
|
17
|
+
/**
|
|
18
|
+
* An imported SVG component, e.g. `import X from './icon.svg?component'`.
|
|
19
|
+
* Rendered directly with no host wrapper. Recommended over `svg`.
|
|
20
|
+
*/
|
|
21
|
+
as?: SvgComponent | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* A full `<svg>…</svg>` markup string, e.g.
|
|
24
|
+
* `import x from './icon.svg?raw'`. Inlined inside a single `<span>` host.
|
|
25
|
+
*/
|
|
26
|
+
svg?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Render a loaded SVG — container-filling, theme-aware, props-transparent.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* import Check from './check.svg?component'
|
|
33
|
+
* <span style="width:2rem"><Icon as={Check} /></span>
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* import check from './check.svg?raw'
|
|
37
|
+
* <span style="width:2rem"><Icon svg={check} /></span>
|
|
38
|
+
*/
|
|
39
|
+
declare function Icon(props: IconProps): VNodeChild;
|
|
40
|
+
/**
|
|
41
|
+
* Build a reusable icon component from a loaded svg — a markup string OR an
|
|
42
|
+
* imported SVG component. The result is still just `<Icon>`, so it's
|
|
43
|
+
* container-sizable + theme-aware with every prop passed through.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* import check from './check.svg?raw'
|
|
47
|
+
* export const Check = createIcon(check)
|
|
48
|
+
*
|
|
49
|
+
* import StarSvg from './star.svg?component'
|
|
50
|
+
* export const Star = createIcon(StarSvg)
|
|
51
|
+
*
|
|
52
|
+
* // …sized + themed entirely by the consumer:
|
|
53
|
+
* <span style="width:48px"><Check class="text-green-600" /></span>
|
|
54
|
+
*/
|
|
55
|
+
declare function createIcon(source: string | SvgComponent): (props: SvgAttributes) => VNodeChild;
|
|
56
|
+
/** How a named icon set renders each entry. */
|
|
57
|
+
type IconMode = 'inline' | 'image';
|
|
58
|
+
/** Props of a component built by {@link createNamedIcon}. */
|
|
59
|
+
type NamedIconProps<R extends Record<string, string>> = {
|
|
60
|
+
/** A name from the scanned set — strictly typed to the available files. */name: keyof R & string; /** `<img>` alt text (image mode). Defaults to `""` (decorative). */
|
|
61
|
+
alt?: string;
|
|
62
|
+
} & Omit<IconProps, 'as' | 'svg'>;
|
|
63
|
+
/**
|
|
64
|
+
* Build a strictly-typed `<Icon name="…" />` from a name→source registry.
|
|
65
|
+
*
|
|
66
|
+
* - `mode: 'inline'` (default) — `source` is raw `<svg>` markup; rendered via
|
|
67
|
+
* {@link Icon} so it's `currentColor`-themeable (system icons you recolor).
|
|
68
|
+
* - `mode: 'image'` — `source` is an asset URL; rendered as `<img>` with NO
|
|
69
|
+
* svg mutation, original colors preserved (colorful / brand icons).
|
|
70
|
+
*
|
|
71
|
+
* Either way it stays container-filling + props-transparent. Not called by
|
|
72
|
+
* hand normally — `iconsPlugin` emits the generated file that calls it.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // icons.gen.tsx (auto-generated):
|
|
76
|
+
* export const Icon = createNamedIcon({ 'check-circle': '<svg…' })
|
|
77
|
+
* // app:
|
|
78
|
+
* <span style="width:2rem"><Icon name="check-circle" /></span>
|
|
79
|
+
*/
|
|
80
|
+
declare function createNamedIcon<R extends Record<string, string>>(registry: R, options?: {
|
|
81
|
+
mode?: IconMode;
|
|
82
|
+
}): (props: NamedIconProps<R>) => VNodeChild;
|
|
83
|
+
//#endregion
|
|
7
84
|
//#region src/image-plugin.d.ts
|
|
8
85
|
/** Per-format source set for <picture> <source> elements. */
|
|
9
86
|
interface FormatSource {
|
|
@@ -443,6 +520,15 @@ interface ISRConfig {
|
|
|
443
520
|
* space (e.g. `/user/:id` where `:id` is free-form).
|
|
444
521
|
*/
|
|
445
522
|
maxEntries?: number;
|
|
523
|
+
/**
|
|
524
|
+
* Max wall-time (ms) for a single background revalidation before it is
|
|
525
|
+
* abandoned. Without a bound, a handler that hangs leaves its key
|
|
526
|
+
* pinned in the in-flight set forever — every later request for that
|
|
527
|
+
* key short-circuits the de-dupe guard and the entry can never
|
|
528
|
+
* recover from stale. Default: `30000` (matches the Suspense
|
|
529
|
+
* streaming timeout).
|
|
530
|
+
*/
|
|
531
|
+
revalidateTimeoutMs?: number;
|
|
446
532
|
/**
|
|
447
533
|
* Cache-key derivation function. The default keys cache entries by
|
|
448
534
|
* `url.pathname` ONLY — query strings, cookies, and headers are
|
|
@@ -628,6 +714,38 @@ interface ZeroConfig {
|
|
|
628
714
|
currentPath: string;
|
|
629
715
|
elapsed: number;
|
|
630
716
|
}) => void | Promise<void>;
|
|
717
|
+
/**
|
|
718
|
+
* Route-level code splitting in SSG mode. Default `true`.
|
|
719
|
+
*
|
|
720
|
+
* When `true` (default), each route file becomes its own dynamic-import
|
|
721
|
+
* chunk via `lazy(() => import("..."))` — only the route the user
|
|
722
|
+
* lands on plus its dependencies ship in the initial bundle, the
|
|
723
|
+
* rest fetch on navigation. Matches the SSR/SPA-mode behaviour zero
|
|
724
|
+
* has always had; brings parity to SSG.
|
|
725
|
+
*
|
|
726
|
+
* When `false`, every route is bundled statically into the main
|
|
727
|
+
* client chunk (the pre-2026-Q3 SSG behaviour). Useful for tiny
|
|
728
|
+
* sites (2-5 pages) where the single-chunk-then-instant-nav trade
|
|
729
|
+
* is preferable — the chunk-fetch cost on navigation is gone, and
|
|
730
|
+
* the marginal bytes are negligible.
|
|
731
|
+
*
|
|
732
|
+
* Crossover point: ~5-8 routes. Below that, single-chunk is fine.
|
|
733
|
+
* Above that, lazy() shrinks the initial bundle by a meaningful
|
|
734
|
+
* amount (a 50-route docs site might drop from 200 KB to 80 KB on
|
|
735
|
+
* first paint).
|
|
736
|
+
*
|
|
737
|
+
* Underlying mechanism is the same 3-tier generator zero already
|
|
738
|
+
* uses for SSR/SPA mode (`fs-router.ts:generateRouteEntry`): lazy
|
|
739
|
+
* component + inlined metadata when possible, lazy + lazy-thunked
|
|
740
|
+
* function exports when not, namespace-import fallback for cases
|
|
741
|
+
* the literal-extractor can't reach.
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* ssg: {
|
|
745
|
+
* splitChunks: false, // bundle-everything for a 3-page marketing site
|
|
746
|
+
* }
|
|
747
|
+
*/
|
|
748
|
+
splitChunks?: boolean;
|
|
631
749
|
};
|
|
632
750
|
/** ISR config — only used when mode is "isr". */
|
|
633
751
|
isr?: ISRConfig;
|
|
@@ -1135,5 +1253,5 @@ declare function ogImagePlugin(..._: unknown[]): never;
|
|
|
1135
1253
|
/** @deprecated Import from `@pyreon/zero/ai` instead */
|
|
1136
1254
|
declare function aiPlugin(..._: unknown[]): never;
|
|
1137
1255
|
//#endregion
|
|
1138
|
-
export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Image, type ImageProps, type ImageRenderProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptRenderProps, type ScriptStrategy, type Theme, ThemeToggle, type UseImageReturn, type UseLinkReturn, type UseScriptReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createImage, createLink, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
|
|
1256
|
+
export { type Adapter, type AdapterBuildOptions, type FileRoute, type I18nRoutingConfig, type ISRConfig, Icon, type IconMode, type IconProps, Image, type ImageProps, type ImageRenderProps, type ImageSource, Link, type LinkProps, type LinkRenderProps, type LoaderContext, type LocaleContext, Meta, type MetaProps, type NamedIconProps, type RenderMode, type RouteMeta, type RouteMiddlewareEntry, type RouteModule, Script, type ScriptProps, type ScriptRenderProps, type ScriptStrategy, type SvgComponent, type Theme, ThemeToggle, type UseImageReturn, type UseLinkReturn, type UseScriptReturn, type ZeroConfig, aiPlugin, buildLocalePath, buildMetaTags, createIcon, createImage, createLink, createNamedIcon, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
|
|
1139
1257
|
//# sourceMappingURL=index2.d.ts.map
|