@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.
- package/lib/{api-routes-CQiOi3q5.js → api-routes-CMsLztoj.js} +1 -1
- package/lib/favicon.js +57 -3
- package/lib/{fs-router-BVY4lTH_.js → fs-router-Bacdhsq-.js} +2 -2
- package/lib/image-plugin.js +13 -6
- package/lib/server.js +2716 -179
- package/lib/types/favicon.d.ts +17 -1
- package/lib/types/i18n-routing.d.ts +2 -4
- package/lib/types/index.d.ts +3 -5
- package/lib/types/link.d.ts +2 -4
- package/lib/types/server.d.ts +2 -4
- package/lib/types/theme.d.ts +1 -2
- package/package.json +10 -10
- package/src/favicon.ts +84 -2
- package/src/image-plugin.ts +43 -12
- package/lib/vite-plugin-8TXXFqdP.js +0 -2491
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;
|
|
@@ -102,5 +118,5 @@ interface IcoEntry {
|
|
|
102
118
|
/** @internal Exported for testing */
|
|
103
119
|
declare function createIcoFromPngs(entries: IcoEntry[]): Uint8Array;
|
|
104
120
|
//#endregion
|
|
105
|
-
export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin };
|
|
121
|
+
export { FaviconLocaleConfig, FaviconPluginConfig, IcoEntry, createIcoFromPngs, faviconLinks, faviconPlugin, faviconVersionQuery };
|
|
106
122
|
//# 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
|
*
|
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
|
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.20.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",
|
|
@@ -168,15 +168,15 @@
|
|
|
168
168
|
"lint": "oxlint ."
|
|
169
169
|
},
|
|
170
170
|
"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.
|
|
171
|
+
"@pyreon/core": "^0.20.0",
|
|
172
|
+
"@pyreon/head": "^0.20.0",
|
|
173
|
+
"@pyreon/meta": "^0.20.0",
|
|
174
|
+
"@pyreon/reactivity": "^0.20.0",
|
|
175
|
+
"@pyreon/router": "^0.20.0",
|
|
176
|
+
"@pyreon/runtime-dom": "^0.20.0",
|
|
177
|
+
"@pyreon/runtime-server": "^0.20.0",
|
|
178
|
+
"@pyreon/server": "^0.20.0",
|
|
179
|
+
"@pyreon/vite-plugin": "^0.20.0",
|
|
180
180
|
"vite": "^8.0.0"
|
|
181
181
|
},
|
|
182
182
|
"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
|
|
@@ -126,6 +161,17 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
126
161
|
|
|
127
162
|
let root = ''
|
|
128
163
|
let isBuild = false
|
|
164
|
+
// Lazily computed once per build/dev session (source rarely changes
|
|
165
|
+
// within a run; recomputing per index.html transform is wasteful).
|
|
166
|
+
let versionQuery: string | null = null
|
|
167
|
+
function getVersionQuery(): string {
|
|
168
|
+
if (versionQuery === null) {
|
|
169
|
+
const paths = [join(root, config.source)]
|
|
170
|
+
if (config.darkSource) paths.push(join(root, config.darkSource))
|
|
171
|
+
versionQuery = faviconVersionQuery(paths)
|
|
172
|
+
}
|
|
173
|
+
return versionQuery
|
|
174
|
+
}
|
|
129
175
|
|
|
130
176
|
return {
|
|
131
177
|
name: 'pyreon-zero-favicon',
|
|
@@ -156,7 +202,11 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
156
202
|
}
|
|
157
203
|
|
|
158
204
|
server.middlewares.use(async (req, res, next) => {
|
|
159
|
-
|
|
205
|
+
// Strip the `?v=<hash>` cache-bust query (and any query) before
|
|
206
|
+
// matching — the injected links carry it; dev serves fresh
|
|
207
|
+
// (`Cache-Control: no-cache`) so the version is irrelevant here,
|
|
208
|
+
// but a query in the path would break every name match below.
|
|
209
|
+
const url = (req.url ?? '').split('?')[0]!
|
|
160
210
|
|
|
161
211
|
// Resolve locale-specific source
|
|
162
212
|
const localeSource = resolveLocaleSource(url, config, root)
|
|
@@ -316,12 +366,44 @@ export function faviconPlugin(config: FaviconPluginConfig): Plugin {
|
|
|
316
366
|
} as any)
|
|
317
367
|
}
|
|
318
368
|
|
|
369
|
+
// Cache-bust: stamp the source content hash onto every injected
|
|
370
|
+
// favicon/manifest link href so a changed icon is actually
|
|
371
|
+
// re-downloaded by returning visitors (theme-swap toggles `media`,
|
|
372
|
+
// not `href`, so this is orthogonal to the light/dark variants).
|
|
373
|
+
const v = getVersionQuery()
|
|
374
|
+
if (v) {
|
|
375
|
+
for (const t of tags) {
|
|
376
|
+
if (t.tag === 'link' && t.attrs.href) t.attrs.href += v
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
319
380
|
return tags
|
|
320
381
|
},
|
|
321
382
|
|
|
322
383
|
async generateBundle() {
|
|
323
384
|
if (!isBuild) return
|
|
324
385
|
|
|
386
|
+
// `faviconPlugin` is in the plugin list and a `source` is configured
|
|
387
|
+
// (it's a required field), so the user clearly WANTS favicons. If
|
|
388
|
+
// `sharp` is missing, the old behaviour was a single swallow-able
|
|
389
|
+
// `console.warn` + emit nothing — i.e. silently ship a production
|
|
390
|
+
// site with zero favicons. That's the footgun. Fail the build loudly
|
|
391
|
+
// with an actionable message instead. Dev keeps the soft warning
|
|
392
|
+
// (see `warnSharpMissing`) so local iteration isn't blocked.
|
|
393
|
+
try {
|
|
394
|
+
await import('sharp')
|
|
395
|
+
} catch {
|
|
396
|
+
this.error(
|
|
397
|
+
'[Pyreon] faviconPlugin: a favicon `source` is configured but ' +
|
|
398
|
+
'`sharp` is not installed — NO favicons would be generated and ' +
|
|
399
|
+
'the production build would silently ship none.\n' +
|
|
400
|
+
' Fix: bun add -D sharp (or: npm i -D sharp)\n' +
|
|
401
|
+
` Source: ${config.source}\n` +
|
|
402
|
+
'To intentionally build without favicons, remove faviconPlugin() ' +
|
|
403
|
+
'from your Vite plugins.',
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
|
|
325
407
|
// Generate favicons for the base (default) source
|
|
326
408
|
await generateFaviconSet.call(this, root, config.source, config.darkSource, '', config, themeColor, backgroundColor, generateManifest)
|
|
327
409
|
|
package/src/image-plugin.ts
CHANGED
|
@@ -229,23 +229,45 @@ export function imagePlugin(config: ImagePluginConfig = {}): Plugin {
|
|
|
229
229
|
isBuild = resolvedConfig.command === 'build'
|
|
230
230
|
},
|
|
231
231
|
|
|
232
|
-
async resolveId(id) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
async resolveId(id, importer) {
|
|
233
|
+
const isSvgComponent =
|
|
234
|
+
svgOpts && id.includes('?component') && id.split('?')[0]!.endsWith('.svg')
|
|
235
|
+
const isOptimize =
|
|
236
|
+
id.includes('?optimize') && include.test(id.split('?')[0]!)
|
|
237
|
+
if (!isSvgComponent && !isOptimize) return null
|
|
238
|
+
|
|
239
|
+
// Resolve the bare specifier to an ABSOLUTE fs path the way Vite
|
|
240
|
+
// resolves `?url` — importer-relative + alias-aware. The old code
|
|
241
|
+
// embedded the raw unresolved id, so `load()` had to guess: a
|
|
242
|
+
// relative `./img.png?optimize` resolved against cwd (≠ the
|
|
243
|
+
// importer's dir → ENOENT), and an aliased `~/x.png?optimize`
|
|
244
|
+
// arrived already-absolute and got `join(root,'public',…)`-doubled.
|
|
245
|
+
// `this.resolve` (skipSelf so we don't recurse into our own
|
|
246
|
+
// resolveId) handles relative + aliases + extensions. A public-dir
|
|
247
|
+
// web path (`/foo.png?optimize`) doesn't resolve to a module →
|
|
248
|
+
// null → keep the original id so load()'s public/ fallback applies.
|
|
249
|
+
const qIdx = id.indexOf('?')
|
|
250
|
+
const bare = qIdx === -1 ? id : id.slice(0, qIdx)
|
|
251
|
+
const query = qIdx === -1 ? '' : id.slice(qIdx)
|
|
252
|
+
const resolved = await this.resolve(bare, importer, { skipSelf: true })
|
|
253
|
+
const carried = resolved ? `${resolved.id}${query}` : id
|
|
254
|
+
|
|
255
|
+
if (isSvgComponent) return `\0virtual:zero-svg:${carried}`
|
|
256
|
+
return `\0virtual:zero-image:${carried}`
|
|
242
257
|
},
|
|
243
258
|
|
|
244
259
|
async load(id) {
|
|
245
260
|
// SVG component loading
|
|
246
261
|
if (id.startsWith('\0virtual:zero-svg:')) {
|
|
247
262
|
const rawPath = id.replace('\0virtual:zero-svg:', '').split('?')[0] ?? id
|
|
248
|
-
|
|
263
|
+
// resolveId now carries an absolute fs path for relative/aliased
|
|
264
|
+
// imports → trust it if it exists. Only a public-dir web path
|
|
265
|
+
// (`/logo.svg`, unresolved) falls back to root-join.
|
|
266
|
+
const absPath = existsSync(rawPath)
|
|
267
|
+
? rawPath
|
|
268
|
+
: rawPath.startsWith('/')
|
|
269
|
+
? join(root, rawPath)
|
|
270
|
+
: rawPath
|
|
249
271
|
if (!existsSync(absPath)) return null
|
|
250
272
|
|
|
251
273
|
let svg = await readFile(absPath, 'utf-8')
|
|
@@ -287,7 +309,16 @@ export default function SvgComponent(props) {
|
|
|
287
309
|
if (!id.startsWith('\0virtual:zero-image:')) return null
|
|
288
310
|
|
|
289
311
|
const rawPath = id.replace('\0virtual:zero-image:', '').split('?')[0] ?? id
|
|
290
|
-
|
|
312
|
+
// resolveId now carries an absolute fs path for relative/aliased
|
|
313
|
+
// imports (the `./img.png?optimize` and `~/img.png?optimize` cases
|
|
314
|
+
// that used to ENOENT / double-`public`). Trust an existing
|
|
315
|
+
// absolute path; only an unresolved public-dir web path
|
|
316
|
+
// (`/foo.png?optimize`) falls back to `<root>/public/…`.
|
|
317
|
+
const absPath = existsSync(rawPath)
|
|
318
|
+
? rawPath
|
|
319
|
+
: rawPath.startsWith('/')
|
|
320
|
+
? join(root, 'public', rawPath)
|
|
321
|
+
: rawPath
|
|
291
322
|
|
|
292
323
|
// CDN mode — rewrite URLs, no local processing
|
|
293
324
|
if (cdn) {
|