@pyreon/zero 0.19.0 → 0.20.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.
@@ -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-CQiOi3q5.js.map
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,7 +116,7 @@ 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;
@@ -277,10 +320,21 @@ function faviconPlugin(config) {
277
320
  injectTo: "head",
278
321
  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
322
  });
323
+ const v = getVersionQuery();
324
+ if (v) {
325
+ for (const t of tags) if (t.tag === "link" && t.attrs.href) t.attrs.href += v;
326
+ }
280
327
  return tags;
281
328
  },
282
329
  async generateBundle() {
283
330
  if (!isBuild) return;
331
+ try {
332
+ await import("sharp");
333
+ } catch {
334
+ 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.
335
+ Fix: bun add -D sharp (or: npm i -D sharp)
336
+ Source: ${config.source}\nTo intentionally build without favicons, remove faviconPlugin() from your Vite plugins.`);
337
+ }
284
338
  await generateFaviconSet.call(this, root, config.source, config.darkSource, "", config, themeColor, backgroundColor, generateManifest);
285
339
  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
340
  }
@@ -571,5 +625,5 @@ async function addDevBadgeToPng(pngBuffer, size) {
571
625
  }
572
626
 
573
627
  //#endregion
574
- export { createIcoFromPngs, faviconLinks, faviconPlugin };
628
+ export { createIcoFromPngs, faviconLinks, faviconPlugin, faviconVersionQuery };
575
629
  //# 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-CQiOi3q5.js").then((n) => n.t);
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-BVY4lTH_.js.map
988
+ //# sourceMappingURL=fs-router-Bacdhsq-.js.map
@@ -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
- if (svgOpts && id.includes("?component") && id.split("?")[0].endsWith(".svg")) return `\0virtual:zero-svg:${id}`;
82
- if (id.includes("?optimize") && include.test(id.split("?")[0])) return `\0virtual:zero-image:${id}`;
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) => ({