@pyreon/zero 0.19.0 → 0.21.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 +1 -1
- package/lib/{api-routes-CQiOi3q5.js → api-routes-CMsLztoj.js} +1 -1
- package/lib/favicon.js +120 -6
- package/lib/{fs-router-BVY4lTH_.js → fs-router-Bacdhsq-.js} +2 -2
- package/lib/image-plugin.js +14 -7
- package/lib/image-types.js +0 -0
- package/lib/server.js +2741 -144
- package/lib/types/favicon.d.ts +34 -4
- package/lib/types/i18n-routing.d.ts +2 -4
- package/lib/types/image-types.d.ts +55 -0
- package/lib/types/index.d.ts +3 -5
- package/lib/types/link.d.ts +2 -4
- package/lib/types/server.d.ts +19 -7
- package/lib/types/theme.d.ts +1 -2
- package/package.json +15 -10
- package/src/favicon.ts +189 -12
- package/src/image-plugin.ts +59 -14
- package/src/image-types.ts +60 -0
- package/lib/vite-plugin-8TXXFqdP.js +0 -2491
- package/src/image-types.d.ts +0 -51
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ bun add @pyreon/zero
|
|
|
18
18
|
- **Components** — `<Image>` (lazy load, srcset, blur-up), `<Link>` (prefetch, active state), `<Script>` (loading strategies)
|
|
19
19
|
- **Theme** — Dark/light/system with `theme` signal, `<ThemeToggle>`, and anti-flash inline script
|
|
20
20
|
- **Fonts** — Google Fonts self-hosting at build time, local fonts, size-adjusted fallbacks
|
|
21
|
-
- **Image optimization** — Build-time processing via `?optimize` imports (WebP/AVIF, blur placeholders)
|
|
21
|
+
- **Image optimization** — Build-time processing via `?optimize` imports (WebP/AVIF, blur placeholders). Type the custom queries with one line — `/// <reference types="@pyreon/zero/image-types" />` — which ships ambient `declare module "*?optimize"` / `"*?component"` / `"*?raw"` reusing the plugin's own `ProcessedImage`.
|
|
22
22
|
- **SEO** — Sitemap, robots.txt, JSON-LD helpers (Vite plugin + dev middleware)
|
|
23
23
|
- **Middleware** — `cacheMiddleware()`, `securityHeaders()`, `corsMiddleware()`, `rateLimitMiddleware()`, `compressionMiddleware()`
|
|
24
24
|
- **Adapters** — Node.js, Bun, static, Vercel, Cloudflare Pages, Netlify Functions
|
|
@@ -145,4 +145,4 @@ function generateApiRouteModule(files, routesDir) {
|
|
|
145
145
|
|
|
146
146
|
//#endregion
|
|
147
147
|
export { matchApiRoute as i, createApiMiddleware as n, generateApiRouteModule as r, api_routes_exports as t };
|
|
148
|
-
//# sourceMappingURL=api-routes-
|
|
148
|
+
//# sourceMappingURL=api-routes-CMsLztoj.js.map
|
package/lib/favicon.js
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
//#region src/favicon.ts
|
|
6
|
+
/**
|
|
7
|
+
* Stable content hash (FNV-1a, 32-bit) of the favicon source file(s),
|
|
8
|
+
* rendered as a `?v=<hex>` cache-bust query for the injected `<head>`
|
|
9
|
+
* links. Browsers cache favicons extremely aggressively (often per-
|
|
10
|
+
* session / effectively forever), so with a stable URL a changed icon
|
|
11
|
+
* is never re-fetched by returning visitors. Same source bytes →
|
|
12
|
+
* identical query (no needless cache churn); changed bytes → new query
|
|
13
|
+
* → browser re-downloads. Falls back to `''` (no query, prior
|
|
14
|
+
* behaviour) if a source can't be read — never break the build over a
|
|
15
|
+
* cache-bust nicety. NOTE: this versions everything referenced via
|
|
16
|
+
* `<link>` (svg/png/apple-touch/manifest). The bare `/favicon.ico`
|
|
17
|
+
* convention request (browsers fetch it with no link tag) and the
|
|
18
|
+
* `site.webmanifest`'s internal icon entries keep stable URLs — those
|
|
19
|
+
* rely on host cache headers / are re-resolved on PWA (re)install.
|
|
20
|
+
*/
|
|
21
|
+
function faviconVersionQuery(paths) {
|
|
22
|
+
let h = 2166136261;
|
|
23
|
+
let any = false;
|
|
24
|
+
for (const p of paths) {
|
|
25
|
+
let buf;
|
|
26
|
+
try {
|
|
27
|
+
buf = readFileSync(p);
|
|
28
|
+
} catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
any = true;
|
|
32
|
+
for (let i = 0; i < buf.length; i++) {
|
|
33
|
+
h ^= buf[i];
|
|
34
|
+
h = Math.imul(h, 16777619);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!any) return "";
|
|
38
|
+
return `?v=${(h >>> 0).toString(16).padStart(8, "0")}`;
|
|
39
|
+
}
|
|
6
40
|
let sharpWarned = false;
|
|
7
41
|
function warnSharpMissing() {
|
|
8
42
|
if (sharpWarned) return;
|
|
@@ -53,6 +87,15 @@ function faviconPlugin(config) {
|
|
|
53
87
|
const generateManifest = config.manifest !== false;
|
|
54
88
|
let root = "";
|
|
55
89
|
let isBuild = false;
|
|
90
|
+
let versionQuery = null;
|
|
91
|
+
function getVersionQuery() {
|
|
92
|
+
if (versionQuery === null) {
|
|
93
|
+
const paths = [join(root, config.source)];
|
|
94
|
+
if (config.darkSource) paths.push(join(root, config.darkSource));
|
|
95
|
+
versionQuery = faviconVersionQuery(paths);
|
|
96
|
+
}
|
|
97
|
+
return versionQuery;
|
|
98
|
+
}
|
|
56
99
|
return {
|
|
57
100
|
name: "pyreon-zero-favicon",
|
|
58
101
|
enforce: "pre",
|
|
@@ -73,11 +116,25 @@ function faviconPlugin(config) {
|
|
|
73
116
|
return defaultSource;
|
|
74
117
|
}
|
|
75
118
|
server.middlewares.use(async (req, res, next) => {
|
|
76
|
-
const url = req.url ?? "";
|
|
119
|
+
const url = (req.url ?? "").split("?")[0];
|
|
77
120
|
const localeSource = resolveLocaleSource(url, config, root);
|
|
78
121
|
const svgUrl = localeSource ? localeSource.url : url;
|
|
79
122
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
|
|
80
123
|
const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
|
|
124
|
+
if (isSvgSource && (svgUrl.endsWith("/favicon-light.svg") || svgUrl.endsWith("/favicon-dark.svg"))) {
|
|
125
|
+
const isDarkVariant = svgUrl.endsWith("/favicon-dark.svg");
|
|
126
|
+
const variantPath = isDarkVariant ? darkPath ?? svgPath : svgPath;
|
|
127
|
+
try {
|
|
128
|
+
let content = await readFile(variantPath, "utf-8");
|
|
129
|
+
if (!isDarkVariant) {
|
|
130
|
+
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
131
|
+
else if (devSourcePath && existsSync(devSourcePath)) content = await readFile(devSourcePath, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
134
|
+
res.end(content);
|
|
135
|
+
return;
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
81
138
|
if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
|
|
82
139
|
let content = await readFile(svgPath, "utf-8");
|
|
83
140
|
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
@@ -154,7 +211,27 @@ function faviconPlugin(config) {
|
|
|
154
211
|
const isSvg = config.source.endsWith(".svg");
|
|
155
212
|
const hasDark = !!config.darkSource;
|
|
156
213
|
const tags = [];
|
|
157
|
-
if (isSvg) tags.push({
|
|
214
|
+
if (isSvg && hasDark) tags.push({
|
|
215
|
+
tag: "link",
|
|
216
|
+
attrs: {
|
|
217
|
+
rel: "icon",
|
|
218
|
+
type: "image/svg+xml",
|
|
219
|
+
href: "/favicon-light.svg",
|
|
220
|
+
"data-favicon-theme": "light"
|
|
221
|
+
},
|
|
222
|
+
injectTo: "head"
|
|
223
|
+
}, {
|
|
224
|
+
tag: "link",
|
|
225
|
+
attrs: {
|
|
226
|
+
rel: "icon",
|
|
227
|
+
type: "image/svg+xml",
|
|
228
|
+
href: "/favicon-dark.svg",
|
|
229
|
+
"data-favicon-theme": "dark",
|
|
230
|
+
media: "not all"
|
|
231
|
+
},
|
|
232
|
+
injectTo: "head"
|
|
233
|
+
});
|
|
234
|
+
else if (isSvg) tags.push({
|
|
158
235
|
tag: "link",
|
|
159
236
|
attrs: {
|
|
160
237
|
rel: "icon",
|
|
@@ -277,10 +354,21 @@ function faviconPlugin(config) {
|
|
|
277
354
|
injectTo: "head",
|
|
278
355
|
children: `(function(){try{var t=localStorage.getItem("zero-theme");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.querySelectorAll("[data-favicon-theme]").forEach(function(l){l.media=l.dataset.faviconTheme===r?"":"not all"})}catch(e){}})()`
|
|
279
356
|
});
|
|
357
|
+
const v = getVersionQuery();
|
|
358
|
+
if (v) {
|
|
359
|
+
for (const t of tags) if (t.tag === "link" && t.attrs.href) t.attrs.href += v;
|
|
360
|
+
}
|
|
280
361
|
return tags;
|
|
281
362
|
},
|
|
282
363
|
async generateBundle() {
|
|
283
364
|
if (!isBuild) return;
|
|
365
|
+
try {
|
|
366
|
+
await import("sharp");
|
|
367
|
+
} catch {
|
|
368
|
+
this.error(`[Pyreon] faviconPlugin: a favicon \`source\` is configured but \`sharp\` is not installed — NO favicons would be generated and the production build would silently ship none.
|
|
369
|
+
Fix: bun add -D sharp (or: npm i -D sharp)
|
|
370
|
+
Source: ${config.source}\nTo intentionally build without favicons, remove faviconPlugin() from your Vite plugins.`);
|
|
371
|
+
}
|
|
284
372
|
await generateFaviconSet.call(this, root, config.source, config.darkSource, "", config, themeColor, backgroundColor, generateManifest);
|
|
285
373
|
if (config.locales) for (const [locale, localeConfig] of Object.entries(config.locales)) await generateFaviconSet.call(this, root, localeConfig.source, localeConfig.darkSource, `${locale}/`, config, themeColor, backgroundColor, generateManifest);
|
|
286
374
|
}
|
|
@@ -335,7 +423,20 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
|
|
|
335
423
|
let finalSvg = svgContent;
|
|
336
424
|
if (darkSource) {
|
|
337
425
|
const darkPath = join(rootDir, darkSource);
|
|
338
|
-
if (existsSync(darkPath))
|
|
426
|
+
if (existsSync(darkPath)) {
|
|
427
|
+
const darkSvg = await readFile(darkPath, "utf-8");
|
|
428
|
+
finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg);
|
|
429
|
+
this.emitFile({
|
|
430
|
+
type: "asset",
|
|
431
|
+
fileName: `${prefix}favicon-light.svg`,
|
|
432
|
+
source: svgContent
|
|
433
|
+
});
|
|
434
|
+
this.emitFile({
|
|
435
|
+
type: "asset",
|
|
436
|
+
fileName: `${prefix}favicon-dark.svg`,
|
|
437
|
+
source: darkSvg
|
|
438
|
+
});
|
|
439
|
+
}
|
|
339
440
|
}
|
|
340
441
|
this.emitFile({
|
|
341
442
|
type: "asset",
|
|
@@ -425,8 +526,21 @@ function faviconLinks(locale, config) {
|
|
|
425
526
|
const hasLocaleOverride = locale && config.locales?.[locale];
|
|
426
527
|
const prefix = hasLocaleOverride ? `/${locale}` : "";
|
|
427
528
|
const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
|
|
529
|
+
const hasDark = !!config.darkSource;
|
|
428
530
|
const links = [];
|
|
429
|
-
if (isSvg) links.push({
|
|
531
|
+
if (isSvg && hasDark) links.push({
|
|
532
|
+
rel: "icon",
|
|
533
|
+
type: "image/svg+xml",
|
|
534
|
+
href: `${prefix}/favicon-light.svg`,
|
|
535
|
+
"data-favicon-theme": "light"
|
|
536
|
+
}, {
|
|
537
|
+
rel: "icon",
|
|
538
|
+
type: "image/svg+xml",
|
|
539
|
+
href: `${prefix}/favicon-dark.svg`,
|
|
540
|
+
"data-favicon-theme": "dark",
|
|
541
|
+
media: "not all"
|
|
542
|
+
});
|
|
543
|
+
else if (isSvg) links.push({
|
|
430
544
|
rel: "icon",
|
|
431
545
|
type: "image/svg+xml",
|
|
432
546
|
href: `${prefix}/favicon.svg`
|
|
@@ -571,5 +685,5 @@ async function addDevBadgeToPng(pngBuffer, size) {
|
|
|
571
685
|
}
|
|
572
686
|
|
|
573
687
|
//#endregion
|
|
574
|
-
export { createIcoFromPngs, faviconLinks, faviconPlugin };
|
|
688
|
+
export { createIcoFromPngs, faviconLinks, faviconPlugin, faviconVersionQuery };
|
|
575
689
|
//# sourceMappingURL=favicon.js.map
|
|
@@ -969,7 +969,7 @@ async function scanRouteFiles(routesDir) {
|
|
|
969
969
|
*/
|
|
970
970
|
async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
971
971
|
const { readFile } = await import("node:fs/promises");
|
|
972
|
-
const { isApiRoute } = await import("./api-routes-
|
|
972
|
+
const { isApiRoute } = await import("./api-routes-CMsLztoj.js").then((n) => n.t);
|
|
973
973
|
const files = (await scanRouteFiles(routesDir)).filter((f) => !isApiRoute(f));
|
|
974
974
|
const exportsMap = /* @__PURE__ */ new Map();
|
|
975
975
|
await Promise.all(files.map(async (filePath) => {
|
|
@@ -985,4 +985,4 @@ async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
|
985
985
|
|
|
986
986
|
//#endregion
|
|
987
987
|
export { generateRouteModuleFromRoutes as a, scanRouteFilesWithExports as c, generateRouteModule as i, fs_router_exports as n, parseFileRoutes as o, generateMiddlewareModule as r, scanRouteFiles as s, filePathToUrlPath as t };
|
|
988
|
-
//# sourceMappingURL=fs-router-
|
|
988
|
+
//# sourceMappingURL=fs-router-Bacdhsq-.js.map
|
package/lib/image-plugin.js
CHANGED
|
@@ -77,15 +77,22 @@ function imagePlugin(config = {}) {
|
|
|
77
77
|
outDir = resolvedConfig.build.outDir;
|
|
78
78
|
isBuild = resolvedConfig.command === "build";
|
|
79
79
|
},
|
|
80
|
-
async resolveId(id) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return null;
|
|
80
|
+
async resolveId(id, importer) {
|
|
81
|
+
const isSvgComponent = svgOpts && id.includes("?component") && id.split("?")[0].endsWith(".svg");
|
|
82
|
+
const isOptimize = id.includes("?optimize") && include.test(id.split("?")[0]);
|
|
83
|
+
if (!isSvgComponent && !isOptimize) return null;
|
|
84
|
+
const qIdx = id.indexOf("?");
|
|
85
|
+
const bare = qIdx === -1 ? id : id.slice(0, qIdx);
|
|
86
|
+
const query = qIdx === -1 ? "" : id.slice(qIdx);
|
|
87
|
+
const resolved = await this.resolve(bare, importer, { skipSelf: true });
|
|
88
|
+
const carried = resolved ? `${resolved.id}${query}` : id;
|
|
89
|
+
if (isSvgComponent) return `\0virtual:zero-svg:${carried}`;
|
|
90
|
+
return `\0virtual:zero-image:${carried}`;
|
|
84
91
|
},
|
|
85
92
|
async load(id) {
|
|
86
93
|
if (id.startsWith("\0virtual:zero-svg:")) {
|
|
87
94
|
const rawPath = id.replace("\0virtual:zero-svg:", "").split("?")[0] ?? id;
|
|
88
|
-
const absPath = rawPath.startsWith("/") ? join(root, rawPath) : rawPath;
|
|
95
|
+
const absPath = existsSync(rawPath) ? rawPath : rawPath.startsWith("/") ? join(root, rawPath) : rawPath;
|
|
89
96
|
if (!existsSync(absPath)) return null;
|
|
90
97
|
let svg = await readFile(absPath, "utf-8");
|
|
91
98
|
if (svgOpts && svgOpts.currentColor !== false) svg = svg.replace(/fill="(?!none)[^"]*"/g, "fill=\"currentColor\"").replace(/stroke="(?!none)[^"]*"/g, "stroke=\"currentColor\"");
|
|
@@ -111,7 +118,7 @@ export default function SvgComponent(props) {
|
|
|
111
118
|
}
|
|
112
119
|
if (!id.startsWith("\0virtual:zero-image:")) return null;
|
|
113
120
|
const rawPath = id.replace("\0virtual:zero-image:", "").split("?")[0] ?? id;
|
|
114
|
-
const absPath = rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath;
|
|
121
|
+
const absPath = existsSync(rawPath) ? rawPath : rawPath.startsWith("/") ? join(root, "public", rawPath) : rawPath;
|
|
115
122
|
if (cdn) {
|
|
116
123
|
const metadata = await getImageMetadata(absPath);
|
|
117
124
|
const sources = defaultWidths.map((w) => ({
|
|
@@ -163,7 +170,7 @@ export default function SvgComponent(props) {
|
|
|
163
170
|
}
|
|
164
171
|
async function loadDevImage(absPath, rawPath, strategy, placeholderSize) {
|
|
165
172
|
const metadata = await getImageMetadata(absPath);
|
|
166
|
-
const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`;
|
|
173
|
+
const publicPath = rawPath.startsWith("/") && !existsSync(rawPath) ? rawPath : `/@fs/${absPath}`;
|
|
167
174
|
return {
|
|
168
175
|
src: publicPath,
|
|
169
176
|
srcset: "",
|
|
File without changes
|