@pyreon/zero 0.18.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-Ci0kVmM4.js → api-routes-CMsLztoj.js} +5 -3
- package/lib/api-routes.js +4 -2
- package/lib/favicon.js +57 -3
- package/lib/{fs-router-MewHc5SB.js → fs-router-Bacdhsq-.js} +4 -3
- package/lib/image-plugin.js +90 -19
- package/lib/index.js +96 -3
- package/lib/rate-limit.js +5 -0
- package/lib/seo.js +11 -6
- package/lib/server.js +2888 -147
- package/lib/testing.js +4 -2
- package/lib/types/config.d.ts +9 -0
- package/lib/types/favicon.d.ts +17 -1
- package/lib/types/i18n-routing.d.ts +2 -4
- package/lib/types/image-plugin.d.ts +65 -7
- package/lib/types/index.d.ts +91 -7
- package/lib/types/link.d.ts +2 -4
- package/lib/types/server.d.ts +89 -5
- package/lib/types/theme.d.ts +1 -2
- package/package.json +10 -10
- package/src/api-routes.ts +12 -2
- package/src/favicon.ts +84 -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 +200 -32
- 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
- package/lib/vite-plugin-y0NmCLJA.js +0 -2476
package/src/ssg-plugin.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { existsSync } from 'node:fs'
|
|
25
25
|
import { mkdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises'
|
|
26
|
-
import { dirname, join, resolve } from 'node:path'
|
|
26
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
27
27
|
import { pathToFileURL } from 'node:url'
|
|
28
28
|
import type { Plugin } from 'vite'
|
|
29
29
|
import { resolveAdapter } from './adapters'
|
|
@@ -373,6 +373,25 @@ export function expandUrlPattern(pattern: string, params: Record<string, string>
|
|
|
373
373
|
`[zero:ssg] getStaticPaths for "${pattern}" returned params without "${name}"`,
|
|
374
374
|
)
|
|
375
375
|
}
|
|
376
|
+
// Path-escape guard. The value is substituted verbatim into the
|
|
377
|
+
// URL that becomes a `dist/<path>/index.html` write target. A
|
|
378
|
+
// single (non-catch-all) `:slug` is ONE segment — a value
|
|
379
|
+
// containing `/` or being `.`/`..` (e.g. an unsanitized CMS slug
|
|
380
|
+
// `../../secret`) would escape the intended structure and write
|
|
381
|
+
// outside it. Catch-all `:slug*` legitimately spans segments
|
|
382
|
+
// (`a/b/c`), so it's exempt from the `/` check but still rejects
|
|
383
|
+
// `.`/`..` traversal segments.
|
|
384
|
+
const segs = isCatchAll ? value.split('/') : [value]
|
|
385
|
+
if (
|
|
386
|
+
(!isCatchAll && value.includes('/')) ||
|
|
387
|
+
segs.some((s) => s === '.' || s === '..')
|
|
388
|
+
) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`[zero:ssg] getStaticPaths for "${pattern}" produced an unsafe "${name}" value ` +
|
|
391
|
+
`(${JSON.stringify(value)}): a ${isCatchAll ? 'catch-all' : 'dynamic'} segment ` +
|
|
392
|
+
`must not contain path-traversal ("." / "..")${isCatchAll ? '' : ' or "/"'}.`,
|
|
393
|
+
)
|
|
394
|
+
}
|
|
376
395
|
return value
|
|
377
396
|
})
|
|
378
397
|
.join('/')
|
|
@@ -443,10 +462,17 @@ async function autoDetectStaticPaths(
|
|
|
443
462
|
}
|
|
444
463
|
}
|
|
445
464
|
|
|
465
|
+
// Dedup (order-preserving). The same concrete path can be produced
|
|
466
|
+
// more than once — a `getStaticPaths` returning a duplicate slug, or
|
|
467
|
+
// i18n route fan-out colliding — which otherwise renders the same
|
|
468
|
+
// `dist/<path>/index.html` twice (wasted work + last-write race) and
|
|
469
|
+
// feeds a duplicate `<url>` into the SSG→sitemap merge.
|
|
470
|
+
const deduped = [...new Set(out)]
|
|
471
|
+
|
|
446
472
|
// Always include "/" as a fallback if no static routes were found —
|
|
447
473
|
// a project with only dynamic routes still needs an index.html for the
|
|
448
474
|
// host to know where to send unmatched URLs.
|
|
449
|
-
return
|
|
475
|
+
return deduped.length > 0 ? deduped : ['/']
|
|
450
476
|
}
|
|
451
477
|
|
|
452
478
|
async function resolvePaths(
|
|
@@ -598,6 +624,21 @@ function resolveOutputPath(distDir: string, path: string): string {
|
|
|
598
624
|
return join(distDir, path, 'index.html')
|
|
599
625
|
}
|
|
600
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Path-containment check that is SEPARATOR-TERMINATED. A bare
|
|
629
|
+
* `resolve(filePath).startsWith(resolve(distDir))` is a string-prefix
|
|
630
|
+
* test, not a path test: with distDir `/app/dist`, a traversed filePath
|
|
631
|
+
* resolving to the SIBLING `/app/dist-evil/x` passes
|
|
632
|
+
* `'/app/dist-evil/x'.startsWith('/app/dist')` → true and the build
|
|
633
|
+
* writes outside the intended output root. `path` derives from caller
|
|
634
|
+
* route params (CMS slugs via `getStaticPaths`), so this is reachable.
|
|
635
|
+
*/
|
|
636
|
+
function isInsideDist(distDir: string, filePath: string): boolean {
|
|
637
|
+
const root = resolve(distDir)
|
|
638
|
+
const target = resolve(filePath)
|
|
639
|
+
return target === root || target.startsWith(root + sep)
|
|
640
|
+
}
|
|
641
|
+
|
|
601
642
|
// ─── Redirect emission (PR B) ──────────────────────────────────────────────
|
|
602
643
|
//
|
|
603
644
|
// The shape returned by the SSG entry's renderPath when a loader throws
|
|
@@ -1128,8 +1169,7 @@ export function ssgPlugin(userConfig: ZeroConfig = {}): Plugin {
|
|
|
1128
1169
|
|
|
1129
1170
|
if (config.ssg?.redirectsAsHtml === 'meta-refresh') {
|
|
1130
1171
|
const filePath = resolveOutputPath(distDir, p)
|
|
1131
|
-
|
|
1132
|
-
if (!resolve(filePath).startsWith(resolvedOut)) {
|
|
1172
|
+
if (!isInsideDist(distDir, filePath)) {
|
|
1133
1173
|
errors.push({ path: p, error: new Error(`Path traversal detected: "${p}"`) })
|
|
1134
1174
|
return
|
|
1135
1175
|
}
|
|
@@ -1144,8 +1184,7 @@ export function ssgPlugin(userConfig: ZeroConfig = {}): Plugin {
|
|
|
1144
1184
|
const filePath = resolveOutputPath(distDir, p)
|
|
1145
1185
|
|
|
1146
1186
|
// Path-traversal guard — same as @pyreon/server's prerender.
|
|
1147
|
-
|
|
1148
|
-
if (!resolve(filePath).startsWith(resolvedOut)) {
|
|
1187
|
+
if (!isInsideDist(distDir, filePath)) {
|
|
1149
1188
|
errors.push({ path: p, error: new Error(`Path traversal detected: "${p}"`) })
|
|
1150
1189
|
return
|
|
1151
1190
|
}
|
|
@@ -1174,8 +1213,7 @@ export function ssgPlugin(userConfig: ZeroConfig = {}): Plugin {
|
|
|
1174
1213
|
const fallbackHtml = await config.ssg.onPathError(p, error)
|
|
1175
1214
|
if (typeof fallbackHtml === 'string') {
|
|
1176
1215
|
const filePath = resolveOutputPath(distDir, p)
|
|
1177
|
-
|
|
1178
|
-
if (!resolve(filePath).startsWith(resolvedOut)) {
|
|
1216
|
+
if (!isInsideDist(distDir, filePath)) {
|
|
1179
1217
|
errors.push({ path: p, error: new Error(`Path traversal detected: "${p}"`) })
|
|
1180
1218
|
return
|
|
1181
1219
|
}
|
|
@@ -1505,6 +1543,7 @@ export const _internal = {
|
|
|
1505
1543
|
resolvePaths,
|
|
1506
1544
|
autoDetectStaticPaths,
|
|
1507
1545
|
resolveOutputPath,
|
|
1546
|
+
isInsideDist,
|
|
1508
1547
|
expandUrlPattern,
|
|
1509
1548
|
injectIntoTemplate,
|
|
1510
1549
|
renderNetlifyRedirects,
|
package/src/types.ts
CHANGED
|
@@ -58,6 +58,15 @@ export interface ISRConfig {
|
|
|
58
58
|
* space (e.g. `/user/:id` where `:id` is free-form).
|
|
59
59
|
*/
|
|
60
60
|
maxEntries?: number
|
|
61
|
+
/**
|
|
62
|
+
* Max wall-time (ms) for a single background revalidation before it is
|
|
63
|
+
* abandoned. Without a bound, a handler that hangs leaves its key
|
|
64
|
+
* pinned in the in-flight set forever — every later request for that
|
|
65
|
+
* key short-circuits the de-dupe guard and the entry can never
|
|
66
|
+
* recover from stale. Default: `30000` (matches the Suspense
|
|
67
|
+
* streaming timeout).
|
|
68
|
+
*/
|
|
69
|
+
revalidateTimeoutMs?: number
|
|
61
70
|
/**
|
|
62
71
|
* Cache-key derivation function. The default keys cache entries by
|
|
63
72
|
* `url.pathname` ONLY — query strings, cookies, and headers are
|