@pyreon/zero 0.20.0 → 0.22.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/favicon.js +63 -3
- package/lib/image-plugin.js +1 -1
- package/lib/image-types.js +0 -0
- package/lib/server.js +63 -3
- package/lib/types/favicon.d.ts +17 -3
- package/lib/types/image-types.d.ts +55 -0
- package/lib/types/server.d.ts +17 -3
- package/package.json +15 -10
- package/src/favicon.ts +105 -10
- package/src/image-plugin.ts +16 -2
- package/src/image-types.ts +60 -0
- 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
|
package/lib/favicon.js
CHANGED
|
@@ -121,6 +121,20 @@ function faviconPlugin(config) {
|
|
|
121
121
|
const svgUrl = localeSource ? localeSource.url : url;
|
|
122
122
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
|
|
123
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
|
+
}
|
|
124
138
|
if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
|
|
125
139
|
let content = await readFile(svgPath, "utf-8");
|
|
126
140
|
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
@@ -197,7 +211,27 @@ function faviconPlugin(config) {
|
|
|
197
211
|
const isSvg = config.source.endsWith(".svg");
|
|
198
212
|
const hasDark = !!config.darkSource;
|
|
199
213
|
const tags = [];
|
|
200
|
-
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({
|
|
201
235
|
tag: "link",
|
|
202
236
|
attrs: {
|
|
203
237
|
rel: "icon",
|
|
@@ -389,7 +423,20 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
|
|
|
389
423
|
let finalSvg = svgContent;
|
|
390
424
|
if (darkSource) {
|
|
391
425
|
const darkPath = join(rootDir, darkSource);
|
|
392
|
-
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
|
+
}
|
|
393
440
|
}
|
|
394
441
|
this.emitFile({
|
|
395
442
|
type: "asset",
|
|
@@ -479,8 +526,21 @@ function faviconLinks(locale, config) {
|
|
|
479
526
|
const hasLocaleOverride = locale && config.locales?.[locale];
|
|
480
527
|
const prefix = hasLocaleOverride ? `/${locale}` : "";
|
|
481
528
|
const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
|
|
529
|
+
const hasDark = !!config.darkSource;
|
|
482
530
|
const links = [];
|
|
483
|
-
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({
|
|
484
544
|
rel: "icon",
|
|
485
545
|
type: "image/svg+xml",
|
|
486
546
|
href: `${prefix}/favicon.svg`
|
package/lib/image-plugin.js
CHANGED
|
@@ -170,7 +170,7 @@ export default function SvgComponent(props) {
|
|
|
170
170
|
}
|
|
171
171
|
async function loadDevImage(absPath, rawPath, strategy, placeholderSize) {
|
|
172
172
|
const metadata = await getImageMetadata(absPath);
|
|
173
|
-
const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`;
|
|
173
|
+
const publicPath = rawPath.startsWith("/") && !existsSync(rawPath) ? rawPath : `/@fs/${absPath}`;
|
|
174
174
|
return {
|
|
175
175
|
src: publicPath,
|
|
176
176
|
srcset: "",
|
|
File without changes
|
package/lib/server.js
CHANGED
|
@@ -2866,6 +2866,20 @@ function faviconPlugin(config) {
|
|
|
2866
2866
|
const svgUrl = localeSource ? localeSource.url : url;
|
|
2867
2867
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath;
|
|
2868
2868
|
const isSvgSource = localeSource ? localeSource.source.endsWith(".svg") : config.source.endsWith(".svg");
|
|
2869
|
+
if (isSvgSource && (svgUrl.endsWith("/favicon-light.svg") || svgUrl.endsWith("/favicon-dark.svg"))) {
|
|
2870
|
+
const isDarkVariant = svgUrl.endsWith("/favicon-dark.svg");
|
|
2871
|
+
const variantPath = isDarkVariant ? darkPath ?? svgPath : svgPath;
|
|
2872
|
+
try {
|
|
2873
|
+
let content = await readFile(variantPath, "utf-8");
|
|
2874
|
+
if (!isDarkVariant) {
|
|
2875
|
+
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
2876
|
+
else if (devSourcePath && existsSync(devSourcePath)) content = await readFile(devSourcePath, "utf-8");
|
|
2877
|
+
}
|
|
2878
|
+
res.setHeader("Content-Type", "image/svg+xml");
|
|
2879
|
+
res.end(content);
|
|
2880
|
+
return;
|
|
2881
|
+
} catch {}
|
|
2882
|
+
}
|
|
2869
2883
|
if (svgUrl.endsWith("/favicon.svg") && isSvgSource) try {
|
|
2870
2884
|
let content = await readFile(svgPath, "utf-8");
|
|
2871
2885
|
if (autoDevBadge) content = addDevBadgeToSvg(content);
|
|
@@ -2942,7 +2956,27 @@ function faviconPlugin(config) {
|
|
|
2942
2956
|
const isSvg = config.source.endsWith(".svg");
|
|
2943
2957
|
const hasDark = !!config.darkSource;
|
|
2944
2958
|
const tags = [];
|
|
2945
|
-
if (isSvg) tags.push({
|
|
2959
|
+
if (isSvg && hasDark) tags.push({
|
|
2960
|
+
tag: "link",
|
|
2961
|
+
attrs: {
|
|
2962
|
+
rel: "icon",
|
|
2963
|
+
type: "image/svg+xml",
|
|
2964
|
+
href: "/favicon-light.svg",
|
|
2965
|
+
"data-favicon-theme": "light"
|
|
2966
|
+
},
|
|
2967
|
+
injectTo: "head"
|
|
2968
|
+
}, {
|
|
2969
|
+
tag: "link",
|
|
2970
|
+
attrs: {
|
|
2971
|
+
rel: "icon",
|
|
2972
|
+
type: "image/svg+xml",
|
|
2973
|
+
href: "/favicon-dark.svg",
|
|
2974
|
+
"data-favicon-theme": "dark",
|
|
2975
|
+
media: "not all"
|
|
2976
|
+
},
|
|
2977
|
+
injectTo: "head"
|
|
2978
|
+
});
|
|
2979
|
+
else if (isSvg) tags.push({
|
|
2946
2980
|
tag: "link",
|
|
2947
2981
|
attrs: {
|
|
2948
2982
|
rel: "icon",
|
|
@@ -3134,7 +3168,20 @@ async function generateFaviconSet(rootDir, source, darkSource, prefix, config, t
|
|
|
3134
3168
|
let finalSvg = svgContent;
|
|
3135
3169
|
if (darkSource) {
|
|
3136
3170
|
const darkPath = join(rootDir, darkSource);
|
|
3137
|
-
if (existsSync(darkPath))
|
|
3171
|
+
if (existsSync(darkPath)) {
|
|
3172
|
+
const darkSvg = await readFile(darkPath, "utf-8");
|
|
3173
|
+
finalSvg = wrapSvgWithDarkMode(svgContent, darkSvg);
|
|
3174
|
+
this.emitFile({
|
|
3175
|
+
type: "asset",
|
|
3176
|
+
fileName: `${prefix}favicon-light.svg`,
|
|
3177
|
+
source: svgContent
|
|
3178
|
+
});
|
|
3179
|
+
this.emitFile({
|
|
3180
|
+
type: "asset",
|
|
3181
|
+
fileName: `${prefix}favicon-dark.svg`,
|
|
3182
|
+
source: darkSvg
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3138
3185
|
}
|
|
3139
3186
|
this.emitFile({
|
|
3140
3187
|
type: "asset",
|
|
@@ -3224,8 +3271,21 @@ function faviconLinks(locale, config) {
|
|
|
3224
3271
|
const hasLocaleOverride = locale && config.locales?.[locale];
|
|
3225
3272
|
const prefix = hasLocaleOverride ? `/${locale}` : "";
|
|
3226
3273
|
const isSvg = (hasLocaleOverride ? config.locales[locale].source : config.source).endsWith(".svg");
|
|
3274
|
+
const hasDark = !!config.darkSource;
|
|
3227
3275
|
const links = [];
|
|
3228
|
-
if (isSvg) links.push({
|
|
3276
|
+
if (isSvg && hasDark) links.push({
|
|
3277
|
+
rel: "icon",
|
|
3278
|
+
type: "image/svg+xml",
|
|
3279
|
+
href: `${prefix}/favicon-light.svg`,
|
|
3280
|
+
"data-favicon-theme": "light"
|
|
3281
|
+
}, {
|
|
3282
|
+
rel: "icon",
|
|
3283
|
+
type: "image/svg+xml",
|
|
3284
|
+
href: `${prefix}/favicon-dark.svg`,
|
|
3285
|
+
"data-favicon-theme": "dark",
|
|
3286
|
+
media: "not all"
|
|
3287
|
+
});
|
|
3288
|
+
else if (isSvg) links.push({
|
|
3229
3289
|
rel: "icon",
|
|
3230
3290
|
type: "image/svg+xml",
|
|
3231
3291
|
href: `${prefix}/favicon.svg`
|
package/lib/types/favicon.d.ts
CHANGED
|
@@ -35,9 +35,21 @@ interface FaviconPluginConfig {
|
|
|
35
35
|
/** Generate web manifest. Default: true */
|
|
36
36
|
manifest?: boolean;
|
|
37
37
|
/**
|
|
38
|
-
* Dark
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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).
|
|
41
53
|
*/
|
|
42
54
|
darkSource?: string;
|
|
43
55
|
/**
|
|
@@ -110,6 +122,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
|
|
|
110
122
|
type?: string;
|
|
111
123
|
sizes?: string;
|
|
112
124
|
href: string;
|
|
125
|
+
'data-favicon-theme'?: string;
|
|
126
|
+
media?: string;
|
|
113
127
|
}>;
|
|
114
128
|
interface IcoEntry {
|
|
115
129
|
buffer: Buffer;
|
|
@@ -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/server.d.ts
CHANGED
|
@@ -1036,9 +1036,21 @@ interface FaviconPluginConfig {
|
|
|
1036
1036
|
/** Generate web manifest. Default: true */
|
|
1037
1037
|
manifest?: boolean;
|
|
1038
1038
|
/**
|
|
1039
|
-
* Dark
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
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).
|
|
1042
1054
|
*/
|
|
1043
1055
|
darkSource?: string;
|
|
1044
1056
|
/**
|
|
@@ -1111,6 +1123,8 @@ declare function faviconLinks(locale: string | undefined, config: FaviconPluginC
|
|
|
1111
1123
|
type?: string;
|
|
1112
1124
|
sizes?: string;
|
|
1113
1125
|
href: string;
|
|
1126
|
+
'data-favicon-theme'?: string;
|
|
1127
|
+
media?: string;
|
|
1114
1128
|
}>;
|
|
1115
1129
|
//#endregion
|
|
1116
1130
|
//#region src/icon.d.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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.22.0",
|
|
177
|
+
"@pyreon/head": "^0.22.0",
|
|
178
|
+
"@pyreon/meta": "^0.22.0",
|
|
179
|
+
"@pyreon/reactivity": "^0.22.0",
|
|
180
|
+
"@pyreon/router": "^0.22.0",
|
|
181
|
+
"@pyreon/runtime-dom": "^0.22.0",
|
|
182
|
+
"@pyreon/runtime-server": "^0.22.0",
|
|
183
|
+
"@pyreon/server": "^0.22.0",
|
|
184
|
+
"@pyreon/vite-plugin": "^0.22.0",
|
|
180
185
|
"vite": "^8.0.0"
|
|
181
186
|
},
|
|
182
187
|
"devDependencies": {
|
package/src/favicon.ts
CHANGED
|
@@ -81,9 +81,21 @@ export interface FaviconPluginConfig {
|
|
|
81
81
|
/** Generate web manifest. Default: true */
|
|
82
82
|
manifest?: boolean
|
|
83
83
|
/**
|
|
84
|
-
* Dark
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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).
|
|
87
99
|
*/
|
|
88
100
|
darkSource?: string
|
|
89
101
|
/**
|
|
@@ -214,6 +226,34 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
214
226
|
const svgPath = localeSource ? localeSource.sourcePath : sourcePath
|
|
215
227
|
const isSvgSource = localeSource ? localeSource.source.endsWith('.svg') : config.source.endsWith('.svg')
|
|
216
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
|
+
|
|
217
257
|
// Serve favicon.svg — in dev, add dev badge overlay if configured
|
|
218
258
|
if (svgUrl.endsWith('/favicon.svg') && isSvgSource) {
|
|
219
259
|
try {
|
|
@@ -307,8 +347,22 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
307
347
|
injectTo: 'head'
|
|
308
348
|
}> = []
|
|
309
349
|
|
|
310
|
-
// SVG favicon
|
|
311
|
-
|
|
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) {
|
|
312
366
|
tags.push({
|
|
313
367
|
tag: 'link',
|
|
314
368
|
attrs: { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
|
|
@@ -502,6 +556,22 @@ async function generateFaviconSet(
|
|
|
502
556
|
if (existsSync(darkPath)) {
|
|
503
557
|
const darkSvg = await readFile(darkPath, 'utf-8')
|
|
504
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
|
+
})
|
|
505
575
|
}
|
|
506
576
|
}
|
|
507
577
|
|
|
@@ -599,14 +669,39 @@ async function generateFaviconSet(
|
|
|
599
669
|
export function faviconLinks(
|
|
600
670
|
locale: string | undefined,
|
|
601
671
|
config: FaviconPluginConfig,
|
|
602
|
-
): Array<{
|
|
672
|
+
): Array<{
|
|
673
|
+
rel: string
|
|
674
|
+
type?: string
|
|
675
|
+
sizes?: string
|
|
676
|
+
href: string
|
|
677
|
+
'data-favicon-theme'?: string
|
|
678
|
+
media?: string
|
|
679
|
+
}> {
|
|
603
680
|
const hasLocaleOverride = locale && config.locales?.[locale]
|
|
604
681
|
const prefix = hasLocaleOverride ? `/${locale}` : ''
|
|
605
682
|
const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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) {
|
|
610
705
|
links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })
|
|
611
706
|
}
|
|
612
707
|
|
package/src/image-plugin.ts
CHANGED
|
@@ -23,7 +23,13 @@ function warnSharpMissing() {
|
|
|
23
23
|
//
|
|
24
24
|
// Usage in code:
|
|
25
25
|
// import heroImg from "./hero.jpg?optimize"
|
|
26
|
-
// // → { src, srcset, width, height, placeholder }
|
|
26
|
+
// // → ProcessedImage { src, srcset, width, height, placeholder }
|
|
27
|
+
//
|
|
28
|
+
// Type the `?optimize` / `?component` / `?raw` imports out of the box —
|
|
29
|
+
// add ONE line to a tsconfig-covered `.d.ts` (e.g. `src/env.d.ts`):
|
|
30
|
+
// /// <reference types="@pyreon/zero/image-types" />
|
|
31
|
+
// (ships the ambient `declare module "*?optimize"` etc. — reuses this
|
|
32
|
+
// module's own `ProcessedImage`, so it never drifts.)
|
|
27
33
|
//
|
|
28
34
|
// Or use the component helper:
|
|
29
35
|
// import { Image } from "@pyreon/zero/image"
|
|
@@ -389,7 +395,15 @@ async function loadDevImage(
|
|
|
389
395
|
placeholderSize: number,
|
|
390
396
|
): Promise<ProcessedImage> {
|
|
391
397
|
const metadata = await getImageMetadata(absPath)
|
|
392
|
-
|
|
398
|
+
// `rawPath` is a public-dir web path (e.g. `/logo.png`, served from
|
|
399
|
+
// `public/` at the web root) ONLY when it does NOT resolve to a real
|
|
400
|
+
// file on disk — the same discriminator the `absPath` derivation uses
|
|
401
|
+
// above. `resolveId` now hands absolute fs paths for relative/aliased
|
|
402
|
+
// imports (`/Users/…/img.png`); those ARE real files and must be
|
|
403
|
+
// served through Vite's `/@fs/` prefix, not as a literal `/Users/…`
|
|
404
|
+
// URL (which 404s in dev — build mode was unaffected).
|
|
405
|
+
const isPublicWebPath = rawPath.startsWith('/') && !existsSync(rawPath)
|
|
406
|
+
const publicPath = isPublicWebPath ? rawPath : `/@fs/${absPath}`
|
|
393
407
|
|
|
394
408
|
return {
|
|
395
409
|
src: publicPath,
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
|
|
21
|
+
declare module '*.jpg?optimize' {
|
|
22
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage
|
|
23
|
+
export default image
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
declare module '*.jpeg?optimize' {
|
|
27
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage
|
|
28
|
+
export default image
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare module '*.png?optimize' {
|
|
32
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage
|
|
33
|
+
export default image
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare module '*.webp?optimize' {
|
|
37
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage
|
|
38
|
+
export default image
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare module '*.avif?optimize' {
|
|
42
|
+
const image: import('@pyreon/zero/image-plugin').ProcessedImage
|
|
43
|
+
export default image
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare module '*.svg?component' {
|
|
47
|
+
const component: import('@pyreon/core').ComponentFn<{
|
|
48
|
+
width?: number
|
|
49
|
+
height?: number
|
|
50
|
+
class?: string
|
|
51
|
+
style?: string
|
|
52
|
+
[key: string]: unknown
|
|
53
|
+
}>
|
|
54
|
+
export default component
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare module '*.svg?raw' {
|
|
58
|
+
const svg: string
|
|
59
|
+
export default svg
|
|
60
|
+
}
|
package/src/image-types.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type declarations for image imports processed by @pyreon/zero's imagePlugin.
|
|
3
|
-
*
|
|
4
|
-
* Add to your tsconfig.json:
|
|
5
|
-
* "types": ["@pyreon/zero/image-types"]
|
|
6
|
-
*
|
|
7
|
-
* Or reference directly:
|
|
8
|
-
* /// <reference types="@pyreon/zero/image-types" />
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
declare module '*.jpg?optimize' {
|
|
12
|
-
const image: import('./image-plugin').ProcessedImage
|
|
13
|
-
export default image
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
declare module '*.jpeg?optimize' {
|
|
17
|
-
const image: import('./image-plugin').ProcessedImage
|
|
18
|
-
export default image
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
declare module '*.png?optimize' {
|
|
22
|
-
const image: import('./image-plugin').ProcessedImage
|
|
23
|
-
export default image
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
declare module '*.webp?optimize' {
|
|
27
|
-
const image: import('./image-plugin').ProcessedImage
|
|
28
|
-
export default image
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
declare module '*.avif?optimize' {
|
|
32
|
-
const image: import('./image-plugin').ProcessedImage
|
|
33
|
-
export default image
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
declare module '*.svg?component' {
|
|
37
|
-
import type { ComponentFn } from '@pyreon/core'
|
|
38
|
-
const component: ComponentFn<{
|
|
39
|
-
width?: number
|
|
40
|
-
height?: number
|
|
41
|
-
class?: string
|
|
42
|
-
style?: string
|
|
43
|
-
[key: string]: unknown
|
|
44
|
-
}>
|
|
45
|
-
export default component
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
declare module '*.svg?raw' {
|
|
49
|
-
const svg: string
|
|
50
|
-
export default svg
|
|
51
|
-
}
|