@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/lib/types/favicon.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/favicon.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Stable content hash (FNV-1a, 32-bit) of the favicon source file(s),
|
|
6
|
+
* rendered as a `?v=<hex>` cache-bust query for the injected `<head>`
|
|
7
|
+
* links. Browsers cache favicons extremely aggressively (often per-
|
|
8
|
+
* session / effectively forever), so with a stable URL a changed icon
|
|
9
|
+
* is never re-fetched by returning visitors. Same source bytes →
|
|
10
|
+
* identical query (no needless cache churn); changed bytes → new query
|
|
11
|
+
* → browser re-downloads. Falls back to `''` (no query, prior
|
|
12
|
+
* behaviour) if a source can't be read — never break the build over a
|
|
13
|
+
* cache-bust nicety. NOTE: this versions everything referenced via
|
|
14
|
+
* `<link>` (svg/png/apple-touch/manifest). The bare `/favicon.ico`
|
|
15
|
+
* convention request (browsers fetch it with no link tag) and the
|
|
16
|
+
* `site.webmanifest`'s internal icon entries keep stable URLs — those
|
|
17
|
+
* rely on host cache headers / are re-resolved on PWA (re)install.
|
|
18
|
+
*/
|
|
19
|
+
declare function faviconVersionQuery(paths: string[]): string;
|
|
4
20
|
interface FaviconLocaleConfig {
|
|
5
21
|
/** Locale-specific source icon (SVG or PNG). */
|
|
6
22
|
source: string;
|
|
@@ -19,9 +35,21 @@ interface FaviconPluginConfig {
|
|
|
19
35
|
/** Generate web manifest. Default: true */
|
|
20
36
|
manifest?: boolean;
|
|
21
37
|
/**
|
|
22
|
-
* Dark
|
|
23
|
-
*
|
|
24
|
-
*
|
|
38
|
+
* Dark-mode favicon source.
|
|
39
|
+
*
|
|
40
|
+
* When provided, the plugin emits theme-aware `light`/`dark` variants
|
|
41
|
+
* (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
|
|
42
|
+
* `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
|
|
43
|
+
* `data-favicon-theme`. The injected blocking theme-swap script and
|
|
44
|
+
* `initTheme()` toggle their `media` attribute so the displayed
|
|
45
|
+
* favicon follows the app's resolved theme — including a manual
|
|
46
|
+
* in-app theme toggle, not just the OS `prefers-color-scheme`.
|
|
47
|
+
*
|
|
48
|
+
* For SVG sources a `favicon.svg` is also emitted that wraps both
|
|
49
|
+
* variants behind an OS `prefers-color-scheme` query — kept as the
|
|
50
|
+
* no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
|
|
51
|
+
* follow a manual toggle, which is why the `data-favicon-theme`
|
|
52
|
+
* variants above are what the reactive mechanism actually uses).
|
|
25
53
|
*/
|
|
26
54
|
darkSource?: string;
|
|
27
55
|
/**
|
|
@@ -94,6 +122,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
|
|
|
94
122
|
type?: string;
|
|
95
123
|
sizes?: string;
|
|
96
124
|
href: string;
|
|
125
|
+
'data-favicon-theme'?: string;
|
|
126
|
+
media?: string;
|
|
97
127
|
}>;
|
|
98
128
|
interface IcoEntry {
|
|
99
129
|
buffer: Buffer;
|
|
@@ -102,5 +132,5 @@ interface IcoEntry {
|
|
|
102
132
|
/** @internal Exported for testing */
|
|
103
133
|
declare function createIcoFromPngs(entries: IcoEntry[]): Uint8Array;
|
|
104
134
|
//#endregion
|
|
105
|
-
export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin };
|
|
135
|
+
export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin, faviconVersionQuery };
|
|
106
136
|
//# sourceMappingURL=favicon2.d.ts.map
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
|
-
import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
|
|
3
1
|
import { Plugin } from "vite";
|
|
4
2
|
//#region src/types.d.ts
|
|
5
3
|
type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
|
|
@@ -265,9 +263,9 @@ declare function createLocaleContext(locale: string, path: string, config: I18nR
|
|
|
265
263
|
*/
|
|
266
264
|
declare function i18nRouting(config: I18nRoutingConfig): Plugin;
|
|
267
265
|
/** @internal Context for the current locale. */
|
|
268
|
-
declare const LocaleCtx:
|
|
266
|
+
declare const LocaleCtx: import("@pyreon/core").Context<string>;
|
|
269
267
|
/** Current locale signal — set by the server middleware or client-side detection. */
|
|
270
|
-
declare const localeSignal:
|
|
268
|
+
declare const localeSignal: import("@pyreon/reactivity").Signal<string>;
|
|
271
269
|
/**
|
|
272
270
|
* Read the current locale reactively.
|
|
273
271
|
*
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient type declarations for the custom image-import queries that
|
|
3
|
+
* `@pyreon/zero`'s `imagePlugin` introduces (`?optimize` / `?component`
|
|
4
|
+
* / `?raw`). Shipped + exported so the documented usage type-checks out
|
|
5
|
+
* of the box — no consumer hand-authoring required.
|
|
6
|
+
*
|
|
7
|
+
* Add ONE line to any tsconfig-covered `.d.ts` (e.g. `src/env.d.ts`):
|
|
8
|
+
* /// <reference types="@pyreon/zero/image-types" />
|
|
9
|
+
*
|
|
10
|
+
* Or via tsconfig.json:
|
|
11
|
+
* "types": ["@pyreon/zero/image-types"]
|
|
12
|
+
*
|
|
13
|
+
* This is an ambient-only **script** (no top-level import/export) so
|
|
14
|
+
* every `declare module` below is a global module augmentation. The
|
|
15
|
+
* `ProcessedImage` shape is referenced via the package self-ref
|
|
16
|
+
* `import('@pyreon/zero/image-plugin')` (resolution-stable in the
|
|
17
|
+
* published layout, and re-uses the plugin's own type so it can never
|
|
18
|
+
* drift out of sync).
|
|
19
|
+
*/
|
|
20
|
+
//#region src/image-types.d.ts
|
|
21
|
+
declare module '*.jpg?optimize' {
|
|
22
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage;
|
|
23
|
+
export default image;
|
|
24
|
+
}
|
|
25
|
+
declare module '*.jpeg?optimize' {
|
|
26
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage;
|
|
27
|
+
export default image;
|
|
28
|
+
}
|
|
29
|
+
declare module '*.png?optimize' {
|
|
30
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage;
|
|
31
|
+
export default image;
|
|
32
|
+
}
|
|
33
|
+
declare module '*.webp?optimize' {
|
|
34
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage;
|
|
35
|
+
export default image;
|
|
36
|
+
}
|
|
37
|
+
declare module '*.avif?optimize' {
|
|
38
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage;
|
|
39
|
+
export default image;
|
|
40
|
+
}
|
|
41
|
+
declare module '*.svg?component' {
|
|
42
|
+
const component: import('@pyreon/core').ComponentFn<{
|
|
43
|
+
width?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
class?: string;
|
|
46
|
+
style?: string;
|
|
47
|
+
[key: string]: unknown;
|
|
48
|
+
}>;
|
|
49
|
+
export default component;
|
|
50
|
+
}
|
|
51
|
+
declare module '*.svg?raw' {
|
|
52
|
+
const svg: string;
|
|
53
|
+
export default svg;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=image-types2.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
1
|
import { ComponentFn, Ref, SvgAttributes, VNodeChild } from "@pyreon/core";
|
|
3
|
-
import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
|
|
4
2
|
import { LoaderContext, NavigationGuard } from "@pyreon/router";
|
|
5
3
|
import { Middleware } from "@pyreon/server";
|
|
6
4
|
|
|
@@ -272,7 +270,7 @@ interface LinkProps {
|
|
|
272
270
|
/** Props passed to a custom component via createLink. */
|
|
273
271
|
interface LinkRenderProps {
|
|
274
272
|
href: string;
|
|
275
|
-
ref:
|
|
273
|
+
ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
|
|
276
274
|
onClick: (e: MouseEvent) => void;
|
|
277
275
|
onMouseEnter: () => void;
|
|
278
276
|
onTouchStart: () => void;
|
|
@@ -289,7 +287,7 @@ interface LinkRenderProps {
|
|
|
289
287
|
/** Return type of useLink. */
|
|
290
288
|
interface UseLinkReturn {
|
|
291
289
|
/** Ref object — attach to the root element for viewport-based prefetch. */
|
|
292
|
-
ref:
|
|
290
|
+
ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
|
|
293
291
|
/** Click handler — performs client-side navigation. */
|
|
294
292
|
handleClick: (e: MouseEvent) => void;
|
|
295
293
|
/** Mouse enter handler — triggers hover prefetch. */
|
|
@@ -1191,7 +1189,7 @@ declare function buildMetaTags(props: Omit<MetaProps, 'title' | 'description' |
|
|
|
1191
1189
|
//#region src/theme.d.ts
|
|
1192
1190
|
type Theme = 'light' | 'dark' | 'system';
|
|
1193
1191
|
/** Reactive theme signal. */
|
|
1194
|
-
declare const theme:
|
|
1192
|
+
declare const theme: import("@pyreon/reactivity").Signal<Theme>;
|
|
1195
1193
|
/**
|
|
1196
1194
|
* Set the default theme for SSR (when `matchMedia` is unavailable).
|
|
1197
1195
|
* Call once at server startup before rendering.
|
package/lib/types/link.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
|
-
|
|
3
1
|
//#region src/link.d.ts
|
|
4
2
|
interface LinkProps {
|
|
5
3
|
/** Target URL path. */
|
|
@@ -26,7 +24,7 @@ interface LinkProps {
|
|
|
26
24
|
/** Props passed to a custom component via createLink. */
|
|
27
25
|
interface LinkRenderProps {
|
|
28
26
|
href: string;
|
|
29
|
-
ref:
|
|
27
|
+
ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
|
|
30
28
|
onClick: (e: MouseEvent) => void;
|
|
31
29
|
onMouseEnter: () => void;
|
|
32
30
|
onTouchStart: () => void;
|
|
@@ -43,7 +41,7 @@ interface LinkRenderProps {
|
|
|
43
41
|
/** Return type of useLink. */
|
|
44
42
|
interface UseLinkReturn {
|
|
45
43
|
/** Ref object — attach to the root element for viewport-based prefetch. */
|
|
46
|
-
ref:
|
|
44
|
+
ref: import('@pyreon/core').Ref<HTMLAnchorElement>;
|
|
47
45
|
/** Click handler — performs client-side navigation. */
|
|
48
46
|
handleClick: (e: MouseEvent) => void;
|
|
49
47
|
/** Mouse enter handler — triggers hover prefetch. */
|
package/lib/types/server.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import * as _$_pyreon_core0 from "@pyreon/core";
|
|
2
1
|
import { ComponentFn } from "@pyreon/core";
|
|
3
|
-
import * as _$_pyreon_router0 from "@pyreon/router";
|
|
4
2
|
import { RouteRecord } from "@pyreon/router";
|
|
5
3
|
import { Middleware, MiddlewareContext } from "@pyreon/server";
|
|
6
4
|
import { Plugin } from "vite";
|
|
@@ -36,8 +34,8 @@ interface CreateAppOptions {
|
|
|
36
34
|
* Used internally by entry-server and entry-client.
|
|
37
35
|
*/
|
|
38
36
|
declare function createApp(options: CreateAppOptions): {
|
|
39
|
-
App: () =>
|
|
40
|
-
router:
|
|
37
|
+
App: () => import("@pyreon/core").VNode;
|
|
38
|
+
router: import("@pyreon/router").Router<string>;
|
|
41
39
|
};
|
|
42
40
|
//#endregion
|
|
43
41
|
//#region src/api-routes.d.ts
|
|
@@ -1038,9 +1036,21 @@ interface FaviconPluginConfig {
|
|
|
1038
1036
|
/** Generate web manifest. Default: true */
|
|
1039
1037
|
manifest?: boolean;
|
|
1040
1038
|
/**
|
|
1041
|
-
* Dark
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1039
|
+
* Dark-mode favicon source.
|
|
1040
|
+
*
|
|
1041
|
+
* When provided, the plugin emits theme-aware `light`/`dark` variants
|
|
1042
|
+
* (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
|
|
1043
|
+
* `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
|
|
1044
|
+
* `data-favicon-theme`. The injected blocking theme-swap script and
|
|
1045
|
+
* `initTheme()` toggle their `media` attribute so the displayed
|
|
1046
|
+
* favicon follows the app's resolved theme — including a manual
|
|
1047
|
+
* in-app theme toggle, not just the OS `prefers-color-scheme`.
|
|
1048
|
+
*
|
|
1049
|
+
* For SVG sources a `favicon.svg` is also emitted that wraps both
|
|
1050
|
+
* variants behind an OS `prefers-color-scheme` query — kept as the
|
|
1051
|
+
* no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
|
|
1052
|
+
* follow a manual toggle, which is why the `data-favicon-theme`
|
|
1053
|
+
* variants above are what the reactive mechanism actually uses).
|
|
1044
1054
|
*/
|
|
1045
1055
|
darkSource?: string;
|
|
1046
1056
|
/**
|
|
@@ -1113,6 +1123,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
|
|
|
1113
1123
|
type?: string;
|
|
1114
1124
|
sizes?: string;
|
|
1115
1125
|
href: string;
|
|
1126
|
+
'data-favicon-theme'?: string;
|
|
1127
|
+
media?: string;
|
|
1116
1128
|
}>;
|
|
1117
1129
|
//#endregion
|
|
1118
1130
|
//#region src/icon.d.ts
|
package/lib/types/theme.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import * as _$_pyreon_reactivity0 from "@pyreon/reactivity";
|
|
2
1
|
import { VNodeChild } from "@pyreon/core";
|
|
3
2
|
|
|
4
3
|
//#region src/theme.d.ts
|
|
5
4
|
type Theme = 'light' | 'dark' | 'system';
|
|
6
5
|
/** Reactive theme signal. */
|
|
7
|
-
declare const theme:
|
|
6
|
+
declare const theme: import("@pyreon/reactivity").Signal<Theme>;
|
|
8
7
|
/**
|
|
9
8
|
* Set the default theme for SSR (when `matchMedia` is unavailable).
|
|
10
9
|
* Call once at server startup before rendering.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vit Bokisch",
|
|
@@ -84,6 +84,11 @@
|
|
|
84
84
|
"import": "./lib/image-plugin.js",
|
|
85
85
|
"types": "./lib/types/image-plugin.d.ts"
|
|
86
86
|
},
|
|
87
|
+
"./image-types": {
|
|
88
|
+
"bun": "./src/image-types.ts",
|
|
89
|
+
"import": "./lib/image-types.js",
|
|
90
|
+
"types": "./lib/types/image-types.d.ts"
|
|
91
|
+
},
|
|
87
92
|
"./actions": {
|
|
88
93
|
"bun": "./src/actions.ts",
|
|
89
94
|
"import": "./lib/actions.js",
|
|
@@ -168,15 +173,15 @@
|
|
|
168
173
|
"lint": "oxlint ."
|
|
169
174
|
},
|
|
170
175
|
"dependencies": {
|
|
171
|
-
"@pyreon/core": "^0.
|
|
172
|
-
"@pyreon/head": "^0.
|
|
173
|
-
"@pyreon/meta": "^0.
|
|
174
|
-
"@pyreon/reactivity": "^0.
|
|
175
|
-
"@pyreon/router": "^0.
|
|
176
|
-
"@pyreon/runtime-dom": "^0.
|
|
177
|
-
"@pyreon/runtime-server": "^0.
|
|
178
|
-
"@pyreon/server": "^0.
|
|
179
|
-
"@pyreon/vite-plugin": "^0.
|
|
176
|
+
"@pyreon/core": "^0.21.0",
|
|
177
|
+
"@pyreon/head": "^0.21.0",
|
|
178
|
+
"@pyreon/meta": "^0.21.0",
|
|
179
|
+
"@pyreon/reactivity": "^0.21.0",
|
|
180
|
+
"@pyreon/router": "^0.21.0",
|
|
181
|
+
"@pyreon/runtime-dom": "^0.21.0",
|
|
182
|
+
"@pyreon/runtime-server": "^0.21.0",
|
|
183
|
+
"@pyreon/server": "^0.21.0",
|
|
184
|
+
"@pyreon/vite-plugin": "^0.21.0",
|
|
180
185
|
"vite": "^8.0.0"
|
|
181
186
|
},
|
|
182
187
|
"devDependencies": {
|
package/src/favicon.ts
CHANGED
|
@@ -1,8 +1,43 @@
|
|
|
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
|
import type { Plugin } from 'vite'
|
|
5
5
|
|
|
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
|
+
export function faviconVersionQuery(paths: string[]): string {
|
|
22
|
+
let h = 0x811c9dc5
|
|
23
|
+
let any = false
|
|
24
|
+
for (const p of paths) {
|
|
25
|
+
let buf: Buffer
|
|
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, 0x01000193)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!any) return ''
|
|
38
|
+
return `?v=${(h >>> 0).toString(16).padStart(8, '0')}`
|
|
39
|
+
}
|
|
40
|
+
|
|
6
41
|
let sharpWarned = false
|
|
7
42
|
function warnSharpMissing() {
|
|
8
43
|
if (sharpWarned) return
|
|
@@ -46,9 +81,21 @@ export interface FaviconPluginConfig {
|
|
|
46
81
|
/** Generate web manifest. Default: true */
|
|
47
82
|
manifest?: boolean
|
|
48
83
|
/**
|
|
49
|
-
* Dark
|
|
50
|
-
*
|
|
51
|
-
*
|
|
84
|
+
* Dark-mode favicon source.
|
|
85
|
+
*
|
|
86
|
+
* When provided, the plugin emits theme-aware `light`/`dark` variants
|
|
87
|
+
* (`favicon-light.svg` / `favicon-dark.svg` for SVG sources, plus the
|
|
88
|
+
* `*-light-*` / `*-dark-*` PNG/apple-touch set) tagged with
|
|
89
|
+
* `data-favicon-theme`. The injected blocking theme-swap script and
|
|
90
|
+
* `initTheme()` toggle their `media` attribute so the displayed
|
|
91
|
+
* favicon follows the app's resolved theme — including a manual
|
|
92
|
+
* in-app theme toggle, not just the OS `prefers-color-scheme`.
|
|
93
|
+
*
|
|
94
|
+
* For SVG sources a `favicon.svg` is also emitted that wraps both
|
|
95
|
+
* variants behind an OS `prefers-color-scheme` query — kept as the
|
|
96
|
+
* no-JS / direct-`/favicon.svg`-reference fallback only (it cannot
|
|
97
|
+
* follow a manual toggle, which is why the `data-favicon-theme`
|
|
98
|
+
* variants above are what the reactive mechanism actually uses).
|
|
52
99
|
*/
|
|
53
100
|
darkSource?: string
|
|
54
101
|
/**
|
|
@@ -126,6 +173,17 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
126
173
|
|
|
127
174
|
let root = ''
|
|
128
175
|
let isBuild = false
|
|
176
|
+
// Lazily computed once per build/dev session (source rarely changes
|
|
177
|
+
// within a run; recomputing per index.html transform is wasteful).
|
|
178
|
+
let versionQuery: string | null = null
|
|
179
|
+
function getVersionQuery(): string {
|
|
180
|
+
if (versionQuery === null) {
|
|
181
|
+
const paths = [join(root, config.source)]
|
|
182
|
+
if (config.darkSource) paths.push(join(root, config.darkSource))
|
|
183
|
+
versionQuery = faviconVersionQuery(paths)
|
|
184
|
+
}
|
|
185
|
+
return versionQuery
|
|
186
|
+
}
|
|
129
187
|
|
|
130
188
|
return {
|
|
131
189
|
name: 'pyreon-zero-favicon',
|
|
@@ -156,7 +214,11 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
156
214
|
}
|
|
157
215
|
|
|
158
216
|
server.middlewares.use(async (req, res, next) => {
|
|
159
|
-
|
|
217
|
+
// Strip the `?v=<hash>` cache-bust query (and any query) before
|
|
218
|
+
// matching — the injected links carry it; dev serves fresh
|
|
219
|
+
// (`Cache-Control: no-cache`) so the version is irrelevant here,
|
|
220
|
+
// but a query in the path would break every name match below.
|
|
221
|
+
const url = (req.url ?? '').split('?')[0]!
|
|
160
222
|
|
|
161
223
|
// Resolve locale-specific source
|
|
162
224
|
const localeSource = resolveLocaleSource(url, config, root)
|
|
@@ -164,6 +226,34 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
164
226
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath
|
|
165
227
|
const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')
|
|
166
228
|
|
|
229
|
+
// Serve the per-theme SVG variants (the app-toggle path):
|
|
230
|
+
// /favicon-light.svg → source, /favicon-dark.svg → darkSource.
|
|
231
|
+
// Dev-badge / devSource override applies to the light variant
|
|
232
|
+
// only (it is the active default the swap toggles to), matching
|
|
233
|
+
// the /favicon.svg handler's intent.
|
|
234
|
+
if (
|
|
235
|
+
isSvgSource &&
|
|
236
|
+
(svgUrl.endsWith('/favicon-light.svg') ||
|
|
237
|
+
svgUrl.endsWith('/favicon-dark.svg'))
|
|
238
|
+
) {
|
|
239
|
+
const isDarkVariant = svgUrl.endsWith('/favicon-dark.svg')
|
|
240
|
+
const variantPath = isDarkVariant ? (darkPath ?? svgPath) : svgPath
|
|
241
|
+
try {
|
|
242
|
+
let content = await readFile(variantPath, 'utf-8')
|
|
243
|
+
if (!isDarkVariant) {
|
|
244
|
+
if (autoDevBadge) content = addDevBadgeToSvg(content)
|
|
245
|
+
else if (devSourcePath && existsSync(devSourcePath)) {
|
|
246
|
+
content = await readFile(devSourcePath, 'utf-8')
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
res.setHeader('Content-Type', 'image/svg+xml')
|
|
250
|
+
res.end(content)
|
|
251
|
+
return
|
|
252
|
+
} catch {
|
|
253
|
+
/* fall through */
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
167
257
|
// Serve favicon.svg — in dev, add dev badge overlay if configured
|
|
168
258
|
if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {
|
|
169
259
|
try {
|
|
@@ -257,8 +347,22 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
257
347
|
injectTo: 'head'
|
|
258
348
|
}> = []
|
|
259
349
|
|
|
260
|
-
// SVG favicon
|
|
261
|
-
|
|
350
|
+
// SVG favicon. Browsers prefer an SVG favicon over PNG when both
|
|
351
|
+
// are present, so the SVG link MUST carry the same
|
|
352
|
+
// `data-favicon-theme` contract the PNG dual-variant uses —
|
|
353
|
+
// otherwise the theme-swap script / initTheme() (which only touch
|
|
354
|
+
// `[data-favicon-theme]`) can never change the displayed icon and
|
|
355
|
+
// the whole reactive-favicon feature is silently dead in every
|
|
356
|
+
// SVG-capable browser. When a dark variant exists, emit TWO
|
|
357
|
+
// theme-aware SVG links (mirroring the PNG pattern); the static
|
|
358
|
+
// `/favicon.svg` (an OS `prefers-color-scheme` wrapped dual) stays
|
|
359
|
+
// emitted as the no-JS / direct-reference fallback only.
|
|
360
|
+
if (isSvg && hasDark) {
|
|
361
|
+
tags.push(
|
|
362
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon-light.svg', 'data-favicon-theme': 'light' }, injectTo: 'head' },
|
|
363
|
+
{ tag: 'link', attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon-dark.svg', 'data-favicon-theme': 'dark', media: 'not all' }, injectTo: 'head' },
|
|
364
|
+
)
|
|
365
|
+
} else if (isSvg) {
|
|
262
366
|
tags.push({
|
|
263
367
|
tag: 'link',
|
|
264
368
|
attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
|
@@ -316,12 +420,44 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
316
420
|
} as any)
|
|
317
421
|
}
|
|
318
422
|
|
|
423
|
+
// Cache-bust: stamp the source content hash onto every injected
|
|
424
|
+
// favicon/manifest link href so a changed icon is actually
|
|
425
|
+
// re-downloaded by returning visitors (theme-swap toggles `media`,
|
|
426
|
+
// not `href`, so this is orthogonal to the light/dark variants).
|
|
427
|
+
const v = getVersionQuery()
|
|
428
|
+
if (v) {
|
|
429
|
+
for (const t of tags) {
|
|
430
|
+
if (t.tag === 'link' && t.attrs.href) t.attrs.href += v
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
319
434
|
return tags
|
|
320
435
|
},
|
|
321
436
|
|
|
322
437
|
async generateBundle() {
|
|
323
438
|
if (!isBuild) return
|
|
324
439
|
|
|
440
|
+
// `faviconPlugin` is in the plugin list and a `source` is configured
|
|
441
|
+
// (it's a required field), so the user clearly WANTS favicons. If
|
|
442
|
+
// `sharp` is missing, the old behaviour was a single swallow-able
|
|
443
|
+
// `console.warn` + emit nothing — i.e. silently ship a production
|
|
444
|
+
// site with zero favicons. That's the footgun. Fail the build loudly
|
|
445
|
+
// with an actionable message instead. Dev keeps the soft warning
|
|
446
|
+
// (see `warnSharpMissing`) so local iteration isn't blocked.
|
|
447
|
+
try {
|
|
448
|
+
await import('sharp')
|
|
449
|
+
} catch {
|
|
450
|
+
this.error(
|
|
451
|
+
'[Pyreon] faviconPlugin: a favicon `source` is configured but ' +
|
|
452
|
+
'`sharp` is not installed — NO favicons would be generated and ' +
|
|
453
|
+
'the production build would silently ship none.\n' +
|
|
454
|
+
' Fix: bun add -D sharp (or: npm i -D sharp)\n' +
|
|
455
|
+
` Source: ${config.source}\n` +
|
|
456
|
+
'To intentionally build without favicons, remove faviconPlugin() ' +
|
|
457
|
+
'from your Vite plugins.',
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
|
|
325
461
|
// Generate favicons for the base (default) source
|
|
326
462
|
await generateFaviconSet.call(this, root, config.source, config.darkSource, '', config, themeColor, backgroundColor, generateManifest)
|
|
327
463
|
|
|
@@ -420,6 +556,22 @@ async function generateFaviconSet(
|
|
|
420
556
|
if (existsSync(darkPath)) {
|
|
421
557
|
const darkSvg = await readFile(darkPath, 'utf-8')
|
|
422
558
|
finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg)
|
|
559
|
+
// Per-theme SVG variants for the app-toggle path:
|
|
560
|
+
// transformIndexHtml / faviconLinks emit
|
|
561
|
+
// `/favicon-light.svg` + `/favicon-dark.svg` with
|
|
562
|
+
// `data-favicon-theme` so the theme-swap actually changes the
|
|
563
|
+
// SVG (the wrapped `favicon.svg` is OS-`prefers-color-scheme`
|
|
564
|
+
// only — kept above as the no-JS / direct-ref fallback).
|
|
565
|
+
this.emitFile({
|
|
566
|
+
type: 'asset',
|
|
567
|
+
fileName: `${prefix}favicon-light.svg`,
|
|
568
|
+
source: svgContent,
|
|
569
|
+
})
|
|
570
|
+
this.emitFile({
|
|
571
|
+
type: 'asset',
|
|
572
|
+
fileName: `${prefix}favicon-dark.svg`,
|
|
573
|
+
source: darkSvg,
|
|
574
|
+
})
|
|
423
575
|
}
|
|
424
576
|
}
|
|
425
577
|
|
|
@@ -517,14 +669,39 @@ async function generateFaviconSet(
|
|
|
517
669
|
export function faviconLinks(
|
|
518
670
|
locale: string | undefined,
|
|
519
671
|
config: FaviconPluginConfig,
|
|
520
|
-
): Array<{
|
|
672
|
+
): Array<{
|
|
673
|
+
rel: string
|
|
674
|
+
type?: string
|
|
675
|
+
sizes?: string
|
|
676
|
+
href: string
|
|
677
|
+
'data-favicon-theme'?: string
|
|
678
|
+
media?: string
|
|
679
|
+
}> {
|
|
521
680
|
const hasLocaleOverride = locale && config.locales?.[locale]
|
|
522
681
|
const prefix = hasLocaleOverride ? `/${locale}` : ''
|
|
523
682
|
const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
683
|
+
const hasDark = !!config.darkSource
|
|
684
|
+
|
|
685
|
+
const links: Array<{
|
|
686
|
+
rel: string
|
|
687
|
+
type?: string
|
|
688
|
+
sizes?: string
|
|
689
|
+
href: string
|
|
690
|
+
'data-favicon-theme'?: string
|
|
691
|
+
media?: string
|
|
692
|
+
}> = []
|
|
693
|
+
|
|
694
|
+
// Mirror transformIndexHtml: a single static SVG link would always
|
|
695
|
+
// win over the theme-toggled PNGs (browsers prefer SVG), silently
|
|
696
|
+
// killing reactive switching for SSR'd pages too. Emit the two
|
|
697
|
+
// theme-aware SVG variants so initTheme()'s `[data-favicon-theme]`
|
|
698
|
+
// swap reaches the SVG. `/favicon.svg` stays the no-JS fallback.
|
|
699
|
+
if (isSvg && hasDark) {
|
|
700
|
+
links.push(
|
|
701
|
+
{ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon-light.svg`, 'data-favicon-theme': 'light' },
|
|
702
|
+
{ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon-dark.svg`, 'data-favicon-theme': 'dark', media: 'not all' },
|
|
703
|
+
)
|
|
704
|
+
} else if (isSvg) {
|
|
528
705
|
links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })
|
|
529
706
|
}
|
|
530
707
|
|