@pyreon/zero 0.18.0 → 0.19.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-Ci0kVmM4.js → api-routes-CQiOi3q5.js} +5 -3
- package/lib/api-routes.js +4 -2
- package/lib/{fs-router-MewHc5SB.js → fs-router-BVY4lTH_.js} +4 -3
- package/lib/image-plugin.js +77 -13
- package/lib/index.js +96 -3
- package/lib/rate-limit.js +5 -0
- package/lib/seo.js +11 -6
- package/lib/server.js +229 -25
- package/lib/testing.js +4 -2
- package/lib/types/config.d.ts +9 -0
- package/lib/types/image-plugin.d.ts +65 -7
- package/lib/types/index.d.ts +88 -2
- package/lib/types/server.d.ts +87 -1
- package/lib/{vite-plugin-y0NmCLJA.js → vite-plugin-8TXXFqdP.js} +27 -12
- package/package.json +10 -10
- package/src/api-routes.ts +12 -2
- package/src/fs-router.ts +7 -1
- package/src/icon.tsx +182 -0
- package/src/icons-plugin.ts +296 -0
- package/src/image-plugin.ts +157 -20
- package/src/index.ts +2 -0
- package/src/isr.ts +54 -10
- package/src/manifest.ts +99 -0
- package/src/rate-limit.ts +16 -0
- package/src/seo.ts +19 -4
- package/src/server.ts +2 -0
- package/src/sharp.d.ts +6 -0
- package/src/ssg-plugin.ts +47 -8
- package/src/types.ts +9 -0
|
@@ -16,17 +16,19 @@ function matchApiRoute(pattern, path) {
|
|
|
16
16
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
17
17
|
const pathParts = path.split("/").filter(Boolean);
|
|
18
18
|
const params = {};
|
|
19
|
+
const isUnsafeParam = (name) => name === "__proto__" || name === "constructor" || name === "prototype";
|
|
19
20
|
for (let i = 0; i < patternParts.length; i++) {
|
|
20
21
|
const pp = patternParts[i];
|
|
21
22
|
if (!pp) continue;
|
|
22
23
|
if (pp.endsWith("*")) {
|
|
23
24
|
const paramName = pp.slice(1, -1);
|
|
24
|
-
params[paramName] = pathParts.slice(i).join("/");
|
|
25
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts.slice(i).join("/");
|
|
25
26
|
return params;
|
|
26
27
|
}
|
|
27
28
|
if (i >= pathParts.length) return null;
|
|
28
29
|
if (pp.startsWith(":")) {
|
|
29
|
-
|
|
30
|
+
const paramName = pp.slice(1);
|
|
31
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts[i];
|
|
30
32
|
continue;
|
|
31
33
|
}
|
|
32
34
|
if (pp !== pathParts[i]) return null;
|
|
@@ -143,4 +145,4 @@ function generateApiRouteModule(files, routesDir) {
|
|
|
143
145
|
|
|
144
146
|
//#endregion
|
|
145
147
|
export { matchApiRoute as i, createApiMiddleware as n, generateApiRouteModule as r, api_routes_exports as t };
|
|
146
|
-
//# sourceMappingURL=api-routes-
|
|
148
|
+
//# sourceMappingURL=api-routes-CQiOi3q5.js.map
|
package/lib/api-routes.js
CHANGED
|
@@ -7,17 +7,19 @@ function matchApiRoute(pattern, path) {
|
|
|
7
7
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
8
8
|
const pathParts = path.split("/").filter(Boolean);
|
|
9
9
|
const params = {};
|
|
10
|
+
const isUnsafeParam = (name) => name === "__proto__" || name === "constructor" || name === "prototype";
|
|
10
11
|
for (let i = 0; i < patternParts.length; i++) {
|
|
11
12
|
const pp = patternParts[i];
|
|
12
13
|
if (!pp) continue;
|
|
13
14
|
if (pp.endsWith("*")) {
|
|
14
15
|
const paramName = pp.slice(1, -1);
|
|
15
|
-
params[paramName] = pathParts.slice(i).join("/");
|
|
16
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts.slice(i).join("/");
|
|
16
17
|
return params;
|
|
17
18
|
}
|
|
18
19
|
if (i >= pathParts.length) return null;
|
|
19
20
|
if (pp.startsWith(":")) {
|
|
20
|
-
|
|
21
|
+
const paramName = pp.slice(1);
|
|
22
|
+
if (!isUnsafeParam(paramName)) params[paramName] = pathParts[i];
|
|
21
23
|
continue;
|
|
22
24
|
}
|
|
23
25
|
if (pp !== pathParts[i]) return null;
|
|
@@ -750,7 +750,8 @@ function generateRouteModuleFromRoutes(routes, routesDir, options) {
|
|
|
750
750
|
const opts = [];
|
|
751
751
|
if (loadingName) opts.push(`loading: ${loadingName}`);
|
|
752
752
|
if (errorName) opts.push(`error: ${errorName}`);
|
|
753
|
-
|
|
753
|
+
opts.push(`hmrId: ${JSON.stringify(fullPath)}`);
|
|
754
|
+
const optsStr = `, { ${opts.join(", ")} }`;
|
|
754
755
|
imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`);
|
|
755
756
|
return name;
|
|
756
757
|
}
|
|
@@ -968,7 +969,7 @@ async function scanRouteFiles(routesDir) {
|
|
|
968
969
|
*/
|
|
969
970
|
async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
970
971
|
const { readFile } = await import("node:fs/promises");
|
|
971
|
-
const { isApiRoute } = await import("./api-routes-
|
|
972
|
+
const { isApiRoute } = await import("./api-routes-CQiOi3q5.js").then((n) => n.t);
|
|
972
973
|
const files = (await scanRouteFiles(routesDir)).filter((f) => !isApiRoute(f));
|
|
973
974
|
const exportsMap = /* @__PURE__ */ new Map();
|
|
974
975
|
await Promise.all(files.map(async (filePath) => {
|
|
@@ -984,4 +985,4 @@ async function scanRouteFilesWithExports(routesDir, defaultMode = "ssr") {
|
|
|
984
985
|
|
|
985
986
|
//#endregion
|
|
986
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 };
|
|
987
|
-
//# sourceMappingURL=fs-router-
|
|
988
|
+
//# sourceMappingURL=fs-router-BVY4lTH_.js.map
|
package/lib/image-plugin.js
CHANGED
|
@@ -20,6 +20,13 @@ const cdnProviders = {
|
|
|
20
20
|
/** Bunny CDN: `https://{pullZone}.b-cdn.net/...?width=...&quality=...` */
|
|
21
21
|
bunny: (pullZone) => (src, { width, quality }) => `https://${pullZone}.b-cdn.net/${src}?width=${width}&quality=${quality}`
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Normalize the public {@link PlaceholderStrategy} to an internal kind.
|
|
25
|
+
* @internal Exported for testing.
|
|
26
|
+
*/
|
|
27
|
+
function normalizePlaceholder(s) {
|
|
28
|
+
return s === "dominant-color" ? "color" : s;
|
|
29
|
+
}
|
|
23
30
|
const IMAGE_EXT_RE = /\.(jpe?g|png|webp|avif)$/i;
|
|
24
31
|
/**
|
|
25
32
|
* Zero image processing Vite plugin.
|
|
@@ -52,9 +59,9 @@ function imagePlugin(config = {}) {
|
|
|
52
59
|
1920
|
|
53
60
|
];
|
|
54
61
|
const defaultFormats = config.formats ?? ["webp"];
|
|
55
|
-
const
|
|
62
|
+
const qualityFor = resolveQuality(config.quality);
|
|
56
63
|
const placeholderSize = config.placeholderSize ?? 16;
|
|
57
|
-
const placeholderStrategy = config.placeholder ?? "blur";
|
|
64
|
+
const placeholderStrategy = normalizePlaceholder(config.placeholder ?? "blur");
|
|
58
65
|
const outSubDir = config.outDir ?? "assets/img";
|
|
59
66
|
const include = config.include ?? IMAGE_EXT_RE;
|
|
60
67
|
const cdn = config.cdn;
|
|
@@ -110,7 +117,7 @@ export default function SvgComponent(props) {
|
|
|
110
117
|
const sources = defaultWidths.map((w) => ({
|
|
111
118
|
src: cdn(rawPath, {
|
|
112
119
|
width: w,
|
|
113
|
-
quality,
|
|
120
|
+
quality: qualityFor(defaultFormats[0]),
|
|
114
121
|
format: defaultFormats[0]
|
|
115
122
|
}) ?? rawPath,
|
|
116
123
|
width: w,
|
|
@@ -122,12 +129,12 @@ export default function SvgComponent(props) {
|
|
|
122
129
|
srcset,
|
|
123
130
|
width: metadata.width,
|
|
124
131
|
height: metadata.height,
|
|
125
|
-
placeholder:
|
|
132
|
+
placeholder: await generatePlaceholder(absPath, placeholderStrategy, placeholderSize),
|
|
126
133
|
formats: defaultFormats.map((fmt) => ({
|
|
127
134
|
type: `image/${fmt}`,
|
|
128
135
|
srcset: defaultWidths.map((w) => `${cdn(rawPath, {
|
|
129
136
|
width: w,
|
|
130
|
-
quality,
|
|
137
|
+
quality: qualityFor(fmt),
|
|
131
138
|
format: fmt
|
|
132
139
|
}) ?? rawPath} ${w}w`).join(", ")
|
|
133
140
|
})),
|
|
@@ -136,13 +143,14 @@ export default function SvgComponent(props) {
|
|
|
136
143
|
return `export default ${JSON.stringify(result)}`;
|
|
137
144
|
}
|
|
138
145
|
if (!isBuild) {
|
|
139
|
-
const result = await loadDevImage(absPath, rawPath, placeholderSize);
|
|
146
|
+
const result = await loadDevImage(absPath, rawPath, placeholderStrategy, placeholderSize);
|
|
140
147
|
return `export default ${JSON.stringify(result)}`;
|
|
141
148
|
}
|
|
142
149
|
const processed = await processImage(absPath, {
|
|
143
150
|
widths: defaultWidths,
|
|
144
151
|
formats: defaultFormats,
|
|
145
|
-
|
|
152
|
+
qualityFor,
|
|
153
|
+
placeholderStrategy,
|
|
146
154
|
placeholderSize,
|
|
147
155
|
outSubDir,
|
|
148
156
|
outDir: join(root, outDir)
|
|
@@ -153,7 +161,7 @@ export default function SvgComponent(props) {
|
|
|
153
161
|
}
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
|
-
async function loadDevImage(absPath, rawPath, placeholderSize) {
|
|
164
|
+
async function loadDevImage(absPath, rawPath, strategy, placeholderSize) {
|
|
157
165
|
const metadata = await getImageMetadata(absPath);
|
|
158
166
|
const publicPath = rawPath.startsWith("/") ? rawPath : `/@fs/${absPath}`;
|
|
159
167
|
return {
|
|
@@ -161,7 +169,7 @@ async function loadDevImage(absPath, rawPath, placeholderSize) {
|
|
|
161
169
|
srcset: "",
|
|
162
170
|
width: metadata.width,
|
|
163
171
|
height: metadata.height,
|
|
164
|
-
placeholder: await
|
|
172
|
+
placeholder: await generatePlaceholder(absPath, strategy, placeholderSize),
|
|
165
173
|
formats: [],
|
|
166
174
|
sources: [{
|
|
167
175
|
src: publicPath,
|
|
@@ -208,7 +216,7 @@ async function processImage(absPath, opts) {
|
|
|
208
216
|
for (const format of opts.formats) for (const targetWidth of opts.widths) {
|
|
209
217
|
const width = Math.min(targetWidth, metadata.width);
|
|
210
218
|
const outPath = join(processedDir, `${name}-${width}.${format}`);
|
|
211
|
-
await resizeImage(absPath, outPath, width, format, opts.
|
|
219
|
+
await resizeImage(absPath, outPath, width, format, opts.qualityFor(format));
|
|
212
220
|
sources.push({
|
|
213
221
|
src: outPath,
|
|
214
222
|
width,
|
|
@@ -233,7 +241,7 @@ async function processImage(absPath, opts) {
|
|
|
233
241
|
}));
|
|
234
242
|
const fallbackFormat = formats[formats.length - 1];
|
|
235
243
|
const fallbackSources = formatGroups.get([...formatGroups.keys()].pop());
|
|
236
|
-
const placeholder = await
|
|
244
|
+
const placeholder = await generatePlaceholder(absPath, opts.placeholderStrategy, opts.placeholderSize);
|
|
237
245
|
return {
|
|
238
246
|
src: fallbackSources[fallbackSources.length - 1]?.src ?? absPath,
|
|
239
247
|
srcset: fallbackFormat?.srcset ?? "",
|
|
@@ -351,10 +359,66 @@ async function generateBlurPlaceholder(input, size) {
|
|
|
351
359
|
try {
|
|
352
360
|
return `data:image/webp;base64,${(await (await import("sharp").then((m) => m.default ?? m))(input).resize(size, size, { fit: "inside" }).blur(2).webp({ quality: 20 }).toBuffer()).toString("base64")}`;
|
|
353
361
|
} catch {
|
|
354
|
-
return
|
|
362
|
+
return TRANSPARENT_PLACEHOLDER;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/** 1×1 transparent SVG — the no-sharp fallback for every strategy. */
|
|
366
|
+
const TRANSPARENT_PLACEHOLDER = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='1'%3E%3C/svg%3E";
|
|
367
|
+
const DEFAULT_QUALITY = 80;
|
|
368
|
+
/**
|
|
369
|
+
* Resolve the public {@link ImageQuality} config into a per-format lookup.
|
|
370
|
+
*
|
|
371
|
+
* - `undefined` → every format gets {@link DEFAULT_QUALITY}.
|
|
372
|
+
* - `number` → that number for every format (backward-compatible).
|
|
373
|
+
* - `Partial<Record<ImageFormat, number>>` → per-format; formats omitted
|
|
374
|
+
* from the map fall back to {@link DEFAULT_QUALITY}.
|
|
375
|
+
*
|
|
376
|
+
* @internal Exported for testing.
|
|
377
|
+
*/
|
|
378
|
+
function resolveQuality(q) {
|
|
379
|
+
if (q === void 0) return () => DEFAULT_QUALITY;
|
|
380
|
+
if (typeof q === "number") return () => q;
|
|
381
|
+
return (format) => q[format] ?? DEFAULT_QUALITY;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Dispatch placeholder generation by strategy. Single source of truth used
|
|
385
|
+
* by every code path (CDN / dev / build) — pre-fix each path open-coded
|
|
386
|
+
* `generateBlurPlaceholder`, so `'none'` was honoured only in the CDN path
|
|
387
|
+
* and `'dominant-color'` (typed since the plugin's inception) was never
|
|
388
|
+
* implemented anywhere — the exact typed-but-unimplemented bug class the
|
|
389
|
+
* `audit-types` gate exists to catch.
|
|
390
|
+
*
|
|
391
|
+
* @internal Exported for testing.
|
|
392
|
+
*/
|
|
393
|
+
async function generatePlaceholder(input, strategy, size) {
|
|
394
|
+
if (strategy === "none") return "";
|
|
395
|
+
if (strategy === "color") return generateColorPlaceholder(input);
|
|
396
|
+
return generateBlurPlaceholder(input, size);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Generate a dominant-colour placeholder: a ~200-byte flat-fill SVG data URI.
|
|
400
|
+
*
|
|
401
|
+
* Uses sharp's `.stats()` `dominant` swatch — a histogram-binned colour,
|
|
402
|
+
* not a naive average (averaging a photo trends muddy grey). Note the
|
|
403
|
+
* swatch is approximate by design: a pure-red source resolves to ~#f80808,
|
|
404
|
+
* not #ff0000. The SVG is a constant ~200 bytes regardless of source
|
|
405
|
+
* complexity and needs zero image decode, at the cost of showing a solid
|
|
406
|
+
* colour instead of a blurry preview of the content.
|
|
407
|
+
*/
|
|
408
|
+
async function generateColorPlaceholder(input) {
|
|
409
|
+
try {
|
|
410
|
+
const { dominant } = await (await import("sharp").then((m) => m.default ?? m))(input).stats();
|
|
411
|
+
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1' preserveAspectRatio='none'><rect width='1' height='1' fill='${"#" + [
|
|
412
|
+
dominant.r,
|
|
413
|
+
dominant.g,
|
|
414
|
+
dominant.b
|
|
415
|
+
].map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, "0")).join("")}'/></svg>`;
|
|
416
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
417
|
+
} catch {
|
|
418
|
+
return TRANSPARENT_PLACEHOLDER;
|
|
355
419
|
}
|
|
356
420
|
}
|
|
357
421
|
|
|
358
422
|
//#endregion
|
|
359
|
-
export { cdnProviders, imagePlugin, parseJpegDimensions, parseWebPDimensions };
|
|
423
|
+
export { cdnProviders, generatePlaceholder, imagePlugin, normalizePlaceholder, parseJpegDimensions, parseWebPDimensions, resolveQuality };
|
|
360
424
|
//# sourceMappingURL=image-plugin.js.map
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,102 @@
|
|
|
1
|
-
import { createContext, createRef, onMount, onUnmount } from "@pyreon/core";
|
|
2
|
-
import { effect, signal } from "@pyreon/reactivity";
|
|
1
|
+
import { createContext, createRef, onMount, onUnmount, splitProps } from "@pyreon/core";
|
|
3
2
|
import { jsx, jsxs } from "@pyreon/core/jsx-runtime";
|
|
3
|
+
import { effect, signal } from "@pyreon/reactivity";
|
|
4
4
|
import { useRouter } from "@pyreon/router";
|
|
5
5
|
import { useHead } from "@pyreon/head";
|
|
6
6
|
|
|
7
|
+
//#region src/icon.tsx
|
|
8
|
+
const FILL_STYLE = "display:block;width:100%;height:100%";
|
|
9
|
+
/**
|
|
10
|
+
* Render a loaded SVG — container-filling, theme-aware, props-transparent.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import Check from './check.svg?component'
|
|
14
|
+
* <span style="width:2rem"><Icon as={Check} /></span>
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import check from './check.svg?raw'
|
|
18
|
+
* <span style="width:2rem"><Icon svg={check} /></span>
|
|
19
|
+
*/
|
|
20
|
+
function Icon(props) {
|
|
21
|
+
const [own, rest] = splitProps(props, ["as", "svg"]);
|
|
22
|
+
if (own.as) {
|
|
23
|
+
const As = own.as;
|
|
24
|
+
return /* @__PURE__ */ jsx(As, {
|
|
25
|
+
fill: "currentColor",
|
|
26
|
+
style: FILL_STYLE,
|
|
27
|
+
...rest
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (own.svg) return /* @__PURE__ */ jsx("span", {
|
|
31
|
+
style: FILL_STYLE,
|
|
32
|
+
...rest,
|
|
33
|
+
dangerouslySetInnerHTML: { __html: own.svg }
|
|
34
|
+
});
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a reusable icon component from a loaded svg — a markup string OR an
|
|
39
|
+
* imported SVG component. The result is still just `<Icon>`, so it's
|
|
40
|
+
* container-sizable + theme-aware with every prop passed through.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* import check from './check.svg?raw'
|
|
44
|
+
* export const Check = createIcon(check)
|
|
45
|
+
*
|
|
46
|
+
* import StarSvg from './star.svg?component'
|
|
47
|
+
* export const Star = createIcon(StarSvg)
|
|
48
|
+
*
|
|
49
|
+
* // …sized + themed entirely by the consumer:
|
|
50
|
+
* <span style="width:48px"><Check class="text-green-600" /></span>
|
|
51
|
+
*/
|
|
52
|
+
function createIcon(source) {
|
|
53
|
+
return (props) => typeof source === "string" ? /* @__PURE__ */ jsx(Icon, {
|
|
54
|
+
svg: source,
|
|
55
|
+
...props
|
|
56
|
+
}) : /* @__PURE__ */ jsx(Icon, {
|
|
57
|
+
as: source,
|
|
58
|
+
...props
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build a strictly-typed `<Icon name="…" />` from a name→source registry.
|
|
63
|
+
*
|
|
64
|
+
* - `mode: 'inline'` (default) — `source` is raw `<svg>` markup; rendered via
|
|
65
|
+
* {@link Icon} so it's `currentColor`-themeable (system icons you recolor).
|
|
66
|
+
* - `mode: 'image'` — `source` is an asset URL; rendered as `<img>` with NO
|
|
67
|
+
* svg mutation, original colors preserved (colorful / brand icons).
|
|
68
|
+
*
|
|
69
|
+
* Either way it stays container-filling + props-transparent. Not called by
|
|
70
|
+
* hand normally — `iconsPlugin` emits the generated file that calls it.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // icons.gen.tsx (auto-generated):
|
|
74
|
+
* export const Icon = createNamedIcon({ 'check-circle': '<svg…' })
|
|
75
|
+
* // app:
|
|
76
|
+
* <span style="width:2rem"><Icon name="check-circle" /></span>
|
|
77
|
+
*/
|
|
78
|
+
function createNamedIcon(registry, options = {}) {
|
|
79
|
+
const mode = options.mode ?? "inline";
|
|
80
|
+
return (props) => {
|
|
81
|
+
const [own, rest] = splitProps(props, ["name", "alt"]);
|
|
82
|
+
const source = registry[own.name];
|
|
83
|
+
if (mode === "image") {
|
|
84
|
+
const hostRest = rest;
|
|
85
|
+
return /* @__PURE__ */ jsx("img", {
|
|
86
|
+
src: source,
|
|
87
|
+
alt: own.alt ?? "",
|
|
88
|
+
style: FILL_STYLE,
|
|
89
|
+
...hostRest
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
93
|
+
svg: source,
|
|
94
|
+
...rest
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
7
100
|
//#region src/utils/use-intersection-observer.ts
|
|
8
101
|
/**
|
|
9
102
|
* Observes an element and calls `onIntersect` once it enters the viewport.
|
|
@@ -1118,5 +1211,5 @@ function aiPlugin(..._) {
|
|
|
1118
1211
|
}
|
|
1119
1212
|
|
|
1120
1213
|
//#endregion
|
|
1121
|
-
export { Image, Link, Meta, Script, ThemeToggle, aiPlugin, buildLocalePath, buildMetaTags, createImage, createLink, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
|
|
1214
|
+
export { Icon, Image, Link, Meta, Script, ThemeToggle, aiPlugin, buildLocalePath, buildMetaTags, createIcon, createImage, createLink, createNamedIcon, createScript, createServer, defineConfig, extractLocaleFromPath, faviconPlugin, initTheme, ogImagePlugin, prefetchRoute, resolvedTheme, seoPlugin, setLocale, setSSRThemeDefault, setTheme, theme, themeScript, toggleTheme, useImage, useLink, useLocale, useScript, validateEnv };
|
|
1122
1215
|
//# sourceMappingURL=index.js.map
|
package/lib/rate-limit.js
CHANGED
|
@@ -26,6 +26,11 @@ function rateLimitMiddleware(config = {}) {
|
|
|
26
26
|
if (store.size < MAX_STORE_SIZE / 2 && now - lastCleanup < windowMs) return;
|
|
27
27
|
lastCleanup = now;
|
|
28
28
|
for (const [key, entry] of store) if (entry.resetAt <= now) store.delete(key);
|
|
29
|
+
while (store.size > MAX_STORE_SIZE) {
|
|
30
|
+
const oldest = store.keys().next().value;
|
|
31
|
+
if (oldest === void 0) break;
|
|
32
|
+
store.delete(oldest);
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
return (ctx) => {
|
|
31
36
|
if (include && !include.some((p) => matchSimpleGlob(p, ctx.path))) return;
|
package/lib/seo.js
CHANGED
|
@@ -14,7 +14,7 @@ import { join, resolve } from "node:path";
|
|
|
14
14
|
*/
|
|
15
15
|
function generateSitemap(routeFiles, config, i18n) {
|
|
16
16
|
const { origin, exclude = [], changefreq = "weekly", priority = .7 } = config;
|
|
17
|
-
const
|
|
17
|
+
const paths = routeFiles.filter((f) => {
|
|
18
18
|
const name = f.split("/").pop()?.replace(/\.\w+$/, "");
|
|
19
19
|
return name !== "_layout" && name !== "_error" && name !== "_loading";
|
|
20
20
|
}).map((f) => {
|
|
@@ -23,11 +23,16 @@ function generateSitemap(routeFiles, config, i18n) {
|
|
|
23
23
|
path = path.replace(/\([\w-]+\)\//g, "");
|
|
24
24
|
if (!path.startsWith("/")) path = `/${path}`;
|
|
25
25
|
return path;
|
|
26
|
-
}).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e)))
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
}).filter((p) => p !== null).filter((p) => !exclude.some((e) => p.startsWith(e)));
|
|
27
|
+
const clusters = clusterPathsByLocale((() => {
|
|
28
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const e of [...paths.map((p) => ({
|
|
30
|
+
path: p,
|
|
31
|
+
changefreq,
|
|
32
|
+
priority
|
|
33
|
+
})), ...config.additionalPaths ?? []]) if (!byPath.has(e.path)) byPath.set(e.path, e);
|
|
34
|
+
return [...byPath.values()];
|
|
35
|
+
})(), i18n);
|
|
31
36
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
32
37
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${i18n != null && i18n.locales.length > 0 ? " xmlns:xhtml=\"http://www.w3.org/1999/xhtml\"" : ""}>
|
|
33
38
|
${clusters.map((cluster) => renderClusterEntry(cluster, origin, changefreq, priority, i18n)).join("\n")}
|