@pyreon/zero 0.5.0 → 0.11.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/cache.js.map +1 -1
- package/lib/client.js.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/font.js.map +1 -1
- package/lib/fs-router-BkbIWqek.js.map +1 -1
- package/lib/fs-router-n4VA4lxu.js.map +1 -1
- package/lib/image-plugin.js.map +1 -1
- package/lib/image.js +1 -1
- package/lib/image.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/link.js +1 -1
- package/lib/link.js.map +1 -1
- package/lib/script.js +1 -1
- package/lib/script.js.map +1 -1
- package/lib/seo.js.map +1 -1
- package/lib/theme.js +1 -1
- package/lib/theme.js.map +1 -1
- package/package.json +14 -13
- package/src/actions.ts +20 -28
- package/src/adapters/bun.ts +7 -7
- package/src/adapters/index.ts +12 -14
- package/src/adapters/node.ts +8 -11
- package/src/adapters/static.ts +3 -3
- package/src/api-routes.ts +23 -50
- package/src/app.ts +9 -13
- package/src/cache.ts +16 -29
- package/src/client.ts +8 -8
- package/src/compression.ts +21 -28
- package/src/config.ts +6 -7
- package/src/cors.ts +20 -28
- package/src/entry-server.ts +15 -19
- package/src/error-overlay.ts +10 -13
- package/src/font.ts +44 -55
- package/src/fs-router.ts +44 -63
- package/src/image-plugin.ts +53 -79
- package/src/image.tsx +39 -41
- package/src/index.ts +36 -36
- package/src/isr.ts +8 -8
- package/src/link.tsx +27 -30
- package/src/rate-limit.ts +15 -15
- package/src/script.tsx +21 -22
- package/src/seo.ts +47 -57
- package/src/sharp.d.ts +2 -6
- package/src/testing.ts +8 -12
- package/src/theme.tsx +18 -20
- package/src/types.ts +6 -6
- package/src/utils/use-intersection-observer.ts +2 -2
- package/src/utils/with-headers.ts +1 -4
- package/src/vite-plugin.ts +21 -28
- package/lib/types/actions.d.ts +0 -57
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -10
- package/lib/types/adapters/index.d.ts.map +0 -1
- package/lib/types/adapters/node.d.ts +0 -6
- package/lib/types/adapters/node.d.ts.map +0 -1
- package/lib/types/adapters/static.d.ts +0 -7
- package/lib/types/adapters/static.d.ts.map +0 -1
- package/lib/types/api-routes.d.ts +0 -66
- package/lib/types/api-routes.d.ts.map +0 -1
- package/lib/types/app.d.ts +0 -24
- package/lib/types/app.d.ts.map +0 -1
- package/lib/types/cache.d.ts +0 -54
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts +0 -19
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts +0 -33
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts +0 -18
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts +0 -32
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -34
- package/lib/types/entry-server.d.ts.map +0 -1
- package/lib/types/error-overlay.d.ts +0 -6
- package/lib/types/error-overlay.d.ts.map +0 -1
- package/lib/types/font.d.ts +0 -119
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -38
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts +0 -79
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts +0 -51
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -37
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/isr.d.ts +0 -9
- package/lib/types/isr.d.ts.map +0 -1
- package/lib/types/link.d.ts +0 -116
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts +0 -34
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts +0 -35
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts +0 -88
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/testing.d.ts +0 -85
- package/lib/types/testing.d.ts.map +0 -1
- package/lib/types/theme.d.ts +0 -39
- package/lib/types/theme.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -109
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils/use-intersection-observer.d.ts +0 -10
- package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
- package/lib/types/utils/with-headers.d.ts +0 -6
- package/lib/types/utils/with-headers.d.ts.map +0 -1
- package/lib/types/vite-plugin.d.ts +0 -17
- package/lib/types/vite-plugin.d.ts.map +0 -1
package/src/error-overlay.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Renders a styled HTML page with the error stack trace.
|
|
4
4
|
*/
|
|
5
5
|
export function renderErrorOverlay(error: Error): string {
|
|
6
|
-
const title = escapeHtml(error.message ||
|
|
7
|
-
const stack = escapeHtml(error.stack ||
|
|
6
|
+
const title = escapeHtml(error.message || "Unknown error")
|
|
7
|
+
const stack = escapeHtml(error.stack || "")
|
|
8
8
|
|
|
9
9
|
return `<!DOCTYPE html>
|
|
10
10
|
<html lang="en">
|
|
@@ -96,26 +96,23 @@ export function renderErrorOverlay(error: Error): string {
|
|
|
96
96
|
|
|
97
97
|
function escapeHtml(str: string): string {
|
|
98
98
|
return str
|
|
99
|
-
.replace(/&/g,
|
|
100
|
-
.replace(/</g,
|
|
101
|
-
.replace(/>/g,
|
|
102
|
-
.replace(/"/g,
|
|
99
|
+
.replace(/&/g, "&")
|
|
100
|
+
.replace(/</g, "<")
|
|
101
|
+
.replace(/>/g, ">")
|
|
102
|
+
.replace(/"/g, """)
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function formatStack(stack: string): string {
|
|
106
106
|
return stack
|
|
107
|
-
.split(
|
|
107
|
+
.split("\n")
|
|
108
108
|
.map((line) => {
|
|
109
|
-
if (line.includes(
|
|
109
|
+
if (line.includes("at ")) {
|
|
110
110
|
const fileMatch = line.match(/\(([^)]+)\)/)
|
|
111
111
|
if (fileMatch) {
|
|
112
|
-
return line.replace(
|
|
113
|
-
fileMatch[0],
|
|
114
|
-
`(<span class="file">${fileMatch[1]}</span>)`,
|
|
115
|
-
)
|
|
112
|
+
return line.replace(fileMatch[0], `(<span class="file">${fileMatch[1]}</span>)`)
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
return line
|
|
119
116
|
})
|
|
120
|
-
.join(
|
|
117
|
+
.join("\n")
|
|
121
118
|
}
|
package/src/font.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Plugin } from
|
|
1
|
+
import type { Plugin } from "vite"
|
|
2
2
|
|
|
3
3
|
// ─── Font optimization ──────────────────────────────────────────────────────
|
|
4
4
|
//
|
|
@@ -57,11 +57,11 @@ export interface LocalFont {
|
|
|
57
57
|
src: string
|
|
58
58
|
/** Single weight (400) or variable range ("100 900"). */
|
|
59
59
|
weight?: number | `${number} ${number}`
|
|
60
|
-
style?:
|
|
60
|
+
style?: "normal" | "italic"
|
|
61
61
|
display?: FontDisplay
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export type FontDisplay =
|
|
64
|
+
export type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional"
|
|
65
65
|
|
|
66
66
|
/** Metrics for generating size-adjusted fallback fonts to reduce CLS. */
|
|
67
67
|
export interface FallbackMetrics {
|
|
@@ -98,7 +98,7 @@ type ResolvedFont = StaticFont | VariableFont
|
|
|
98
98
|
* Normalize a GoogleFontInput (string or object) into a ResolvedFont.
|
|
99
99
|
*/
|
|
100
100
|
export function resolveGoogleFont(input: GoogleFontInput): ResolvedFont {
|
|
101
|
-
if (typeof input ===
|
|
101
|
+
if (typeof input === "string") {
|
|
102
102
|
return parseGoogleFamily(input)
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -127,13 +127,13 @@ export function resolveGoogleFont(input: GoogleFontInput): ResolvedFont {
|
|
|
127
127
|
* Variable with italic: "Inter:ital,wght@100..900"
|
|
128
128
|
*/
|
|
129
129
|
export function parseGoogleFamily(input: string): ResolvedFont {
|
|
130
|
-
const parts = input.split(
|
|
131
|
-
const family = (parts[0] ??
|
|
130
|
+
const parts = input.split(":")
|
|
131
|
+
const family = (parts[0] ?? "").trim()
|
|
132
132
|
const spec = parts[1]
|
|
133
133
|
let italic = false
|
|
134
134
|
|
|
135
135
|
if (spec) {
|
|
136
|
-
italic = spec.includes(
|
|
136
|
+
italic = spec.includes("ital")
|
|
137
137
|
|
|
138
138
|
// Variable font range syntax: wght@100..900
|
|
139
139
|
const rangeMatch = spec.match(/wght@(\d+)\.\.(\d+)/)
|
|
@@ -153,7 +153,7 @@ export function parseGoogleFamily(input: string): ResolvedFont {
|
|
|
153
153
|
family,
|
|
154
154
|
italic,
|
|
155
155
|
variable: false,
|
|
156
|
-
weights: weightMatch[1].split(
|
|
156
|
+
weights: weightMatch[1].split(";").map(Number),
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -164,14 +164,11 @@ export function parseGoogleFamily(input: string): ResolvedFont {
|
|
|
164
164
|
/**
|
|
165
165
|
* Generate a Google Fonts CSS URL.
|
|
166
166
|
*/
|
|
167
|
-
export function googleFontsUrl(
|
|
168
|
-
families: ResolvedFont[],
|
|
169
|
-
display: FontDisplay = 'swap',
|
|
170
|
-
): string {
|
|
167
|
+
export function googleFontsUrl(families: ResolvedFont[], display: FontDisplay = "swap"): string {
|
|
171
168
|
const params = families
|
|
172
169
|
.map((f) => {
|
|
173
|
-
const axes = f.italic ?
|
|
174
|
-
const name = f.family.replace(/ /g,
|
|
170
|
+
const axes = f.italic ? "ital,wght" : "wght"
|
|
171
|
+
const name = f.family.replace(/ /g, "+")
|
|
175
172
|
|
|
176
173
|
if (f.variable) {
|
|
177
174
|
const range = `${f.weightRange[0]}..${f.weightRange[1]}`
|
|
@@ -179,12 +176,10 @@ export function googleFontsUrl(
|
|
|
179
176
|
return `family=${name}:${axes}@${value}`
|
|
180
177
|
}
|
|
181
178
|
|
|
182
|
-
const values = f.weights
|
|
183
|
-
.map((w) => (f.italic ? `0,${w};1,${w}` : String(w)))
|
|
184
|
-
.join(';')
|
|
179
|
+
const values = f.weights.map((w) => (f.italic ? `0,${w};1,${w}` : String(w))).join(";")
|
|
185
180
|
return `family=${name}:${axes}@${values}`
|
|
186
181
|
})
|
|
187
|
-
.join(
|
|
182
|
+
.join("&")
|
|
188
183
|
|
|
189
184
|
return `https://fonts.googleapis.com/css2?${params}&display=${display}`
|
|
190
185
|
}
|
|
@@ -198,12 +193,12 @@ function localFontFaces(fonts: LocalFont[], display: FontDisplay): string {
|
|
|
198
193
|
(f) => `@font-face {
|
|
199
194
|
font-family: "${f.family}";
|
|
200
195
|
src: url("${f.src}");
|
|
201
|
-
font-weight: ${f.weight ??
|
|
202
|
-
font-style: ${f.style ??
|
|
196
|
+
font-weight: ${f.weight ?? "400"};
|
|
197
|
+
font-style: ${f.style ?? "normal"};
|
|
203
198
|
font-display: ${f.display ?? display};
|
|
204
199
|
}`,
|
|
205
200
|
)
|
|
206
|
-
.join(
|
|
201
|
+
.join("\n\n")
|
|
207
202
|
}
|
|
208
203
|
|
|
209
204
|
/**
|
|
@@ -213,8 +208,7 @@ function fallbackFontFaces(fallbacks: Record<string, FallbackMetrics>): string {
|
|
|
213
208
|
return Object.entries(fallbacks)
|
|
214
209
|
.map(([family, metrics]) => {
|
|
215
210
|
const overrides: string[] = []
|
|
216
|
-
if (metrics.sizeAdjust != null)
|
|
217
|
-
overrides.push(` size-adjust: ${metrics.sizeAdjust * 100}%;`)
|
|
211
|
+
if (metrics.sizeAdjust != null) overrides.push(` size-adjust: ${metrics.sizeAdjust * 100}%;`)
|
|
218
212
|
if (metrics.ascentOverride != null)
|
|
219
213
|
overrides.push(` ascent-override: ${metrics.ascentOverride}%;`)
|
|
220
214
|
if (metrics.descentOverride != null)
|
|
@@ -225,10 +219,10 @@ function fallbackFontFaces(fallbacks: Record<string, FallbackMetrics>): string {
|
|
|
225
219
|
return `@font-face {
|
|
226
220
|
font-family: "${family} Fallback";
|
|
227
221
|
src: local("${metrics.fallback}");
|
|
228
|
-
${overrides.join(
|
|
222
|
+
${overrides.join("\n")}
|
|
229
223
|
}`
|
|
230
224
|
})
|
|
231
|
-
.join(
|
|
225
|
+
.join("\n\n")
|
|
232
226
|
}
|
|
233
227
|
|
|
234
228
|
/**
|
|
@@ -237,18 +231,18 @@ ${overrides.join('\n')}
|
|
|
237
231
|
function preloadTags(fonts: LocalFont[]): string {
|
|
238
232
|
return fonts
|
|
239
233
|
.map((f) => {
|
|
240
|
-
const ext = f.src.split(
|
|
234
|
+
const ext = f.src.split(".").pop()
|
|
241
235
|
const type =
|
|
242
|
-
ext ===
|
|
243
|
-
?
|
|
244
|
-
: ext ===
|
|
245
|
-
?
|
|
246
|
-
: ext ===
|
|
247
|
-
?
|
|
248
|
-
:
|
|
236
|
+
ext === "woff2"
|
|
237
|
+
? "font/woff2"
|
|
238
|
+
: ext === "woff"
|
|
239
|
+
? "font/woff"
|
|
240
|
+
: ext === "ttf"
|
|
241
|
+
? "font/ttf"
|
|
242
|
+
: "font/otf"
|
|
249
243
|
return `<link rel="preload" href="${f.src}" as="font" type="${type}" crossorigin>`
|
|
250
244
|
})
|
|
251
|
-
.join(
|
|
245
|
+
.join("\n")
|
|
252
246
|
}
|
|
253
247
|
|
|
254
248
|
/**
|
|
@@ -257,8 +251,8 @@ function preloadTags(fonts: LocalFont[]): string {
|
|
|
257
251
|
async function downloadGoogleFontsCSS(url: string): Promise<string> {
|
|
258
252
|
const response = await fetch(url, {
|
|
259
253
|
headers: {
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
"User-Agent":
|
|
255
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
262
256
|
},
|
|
263
257
|
})
|
|
264
258
|
if (!response.ok) {
|
|
@@ -306,8 +300,8 @@ async function selfHostFonts(
|
|
|
306
300
|
let rewrittenCss = css
|
|
307
301
|
|
|
308
302
|
for (const url of fontUrls) {
|
|
309
|
-
const urlParts = url.split(
|
|
310
|
-
const fileName = urlParts.at(-1)?.split(
|
|
303
|
+
const urlParts = url.split("/")
|
|
304
|
+
const fileName = urlParts.at(-1)?.split("?")[0] ?? "font"
|
|
311
305
|
const content = await downloadFontFile(url)
|
|
312
306
|
|
|
313
307
|
fontFiles.push({ name: fileName, content })
|
|
@@ -340,27 +334,27 @@ async function selfHostFonts(
|
|
|
340
334
|
* }
|
|
341
335
|
*/
|
|
342
336
|
export function fontPlugin(config: FontConfig = {}): Plugin {
|
|
343
|
-
const display = config.display ??
|
|
337
|
+
const display = config.display ?? "swap"
|
|
344
338
|
const shouldPreload = config.preload !== false
|
|
345
339
|
const shouldSelfHost = config.selfHost !== false
|
|
346
340
|
const googleFamilies = (config.google ?? []).map(resolveGoogleFont)
|
|
347
341
|
|
|
348
342
|
let isBuild = false
|
|
349
|
-
let selfHostedCSS =
|
|
343
|
+
let selfHostedCSS = ""
|
|
350
344
|
let selfHostedFontFiles: Array<{ name: string; content: Buffer }> = []
|
|
351
345
|
|
|
352
346
|
return {
|
|
353
|
-
name:
|
|
347
|
+
name: "pyreon-zero-fonts",
|
|
354
348
|
|
|
355
349
|
configResolved(resolvedConfig) {
|
|
356
|
-
isBuild = resolvedConfig.command ===
|
|
350
|
+
isBuild = resolvedConfig.command === "build"
|
|
357
351
|
},
|
|
358
352
|
|
|
359
353
|
async buildStart() {
|
|
360
354
|
if (isBuild && shouldSelfHost && googleFamilies.length > 0) {
|
|
361
355
|
const cssUrl = googleFontsUrl(googleFamilies, display)
|
|
362
356
|
try {
|
|
363
|
-
const result = await selfHostFonts(cssUrl,
|
|
357
|
+
const result = await selfHostFonts(cssUrl, "assets/fonts")
|
|
364
358
|
selfHostedCSS = result.css
|
|
365
359
|
selfHostedFontFiles = result.fontFiles
|
|
366
360
|
} catch {
|
|
@@ -373,7 +367,7 @@ export function fontPlugin(config: FontConfig = {}): Plugin {
|
|
|
373
367
|
// Emit self-hosted font files as assets
|
|
374
368
|
for (const file of selfHostedFontFiles) {
|
|
375
369
|
this.emitFile({
|
|
376
|
-
type:
|
|
370
|
+
type: "asset",
|
|
377
371
|
fileName: `assets/fonts/${file.name}`,
|
|
378
372
|
source: file.content,
|
|
379
373
|
})
|
|
@@ -394,7 +388,7 @@ export function fontPlugin(config: FontConfig = {}): Plugin {
|
|
|
394
388
|
collectLocalFontTags(tags, config, shouldPreload, display)
|
|
395
389
|
|
|
396
390
|
if (tags.length === 0) return html
|
|
397
|
-
return html.replace(
|
|
391
|
+
return html.replace("</head>", `${tags.join("\n")}\n</head>`)
|
|
398
392
|
},
|
|
399
393
|
}
|
|
400
394
|
}
|
|
@@ -413,12 +407,9 @@ function collectGoogleFontTags(
|
|
|
413
407
|
if (opts.isBuild && opts.selfHostedCSS) {
|
|
414
408
|
tags.push(`<style>${opts.selfHostedCSS}</style>`)
|
|
415
409
|
if (opts.shouldPreload) {
|
|
416
|
-
for (const file of opts.selfHostedFontFiles.slice(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
)) {
|
|
420
|
-
const ext = file.name.split('.').pop()
|
|
421
|
-
const type = ext === 'woff2' ? 'font/woff2' : 'font/woff'
|
|
410
|
+
for (const file of opts.selfHostedFontFiles.slice(0, opts.googleFamilies.length)) {
|
|
411
|
+
const ext = file.name.split(".").pop()
|
|
412
|
+
const type = ext === "woff2" ? "font/woff2" : "font/woff"
|
|
422
413
|
tags.push(
|
|
423
414
|
`<link rel="preload" href="/assets/fonts/${file.name}" as="font" type="${type}" crossorigin>`,
|
|
424
415
|
)
|
|
@@ -427,9 +418,7 @@ function collectGoogleFontTags(
|
|
|
427
418
|
} else if (opts.googleFamilies.length > 0) {
|
|
428
419
|
const cssUrl = googleFontsUrl(opts.googleFamilies, opts.display)
|
|
429
420
|
tags.push(`<link rel="preconnect" href="https://fonts.googleapis.com">`)
|
|
430
|
-
tags.push(
|
|
431
|
-
`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`,
|
|
432
|
-
)
|
|
421
|
+
tags.push(`<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`)
|
|
433
422
|
tags.push(`<link rel="stylesheet" href="${cssUrl}">`)
|
|
434
423
|
}
|
|
435
424
|
}
|
|
@@ -457,6 +446,6 @@ function collectLocalFontTags(
|
|
|
457
446
|
export function fontVariables(families: Record<string, string>): string {
|
|
458
447
|
const vars = Object.entries(families)
|
|
459
448
|
.map(([key, value]) => ` --font-${key}: ${value};`)
|
|
460
|
-
.join(
|
|
449
|
+
.join("\n")
|
|
461
450
|
return `:root {\n${vars}\n}`
|
|
462
451
|
}
|
package/src/fs-router.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileRoute, RenderMode } from
|
|
1
|
+
import type { FileRoute, RenderMode } from "./types"
|
|
2
2
|
|
|
3
3
|
// ─── File-system route conventions ──────────────────────────────────────────
|
|
4
4
|
//
|
|
@@ -25,7 +25,7 @@ import type { FileRoute, RenderMode } from './types'
|
|
|
25
25
|
// _loading → loading component
|
|
26
26
|
// (group) → route group (directory ignored in URL)
|
|
27
27
|
|
|
28
|
-
const ROUTE_EXTENSIONS = [
|
|
28
|
+
const ROUTE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"]
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Parse a set of file paths (relative to routes dir) into FileRoute objects.
|
|
@@ -33,10 +33,7 @@ const ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']
|
|
|
33
33
|
* @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
|
|
34
34
|
* @param defaultMode Default rendering mode from config
|
|
35
35
|
*/
|
|
36
|
-
export function parseFileRoutes(
|
|
37
|
-
files: string[],
|
|
38
|
-
defaultMode: RenderMode = 'ssr',
|
|
39
|
-
): FileRoute[] {
|
|
36
|
+
export function parseFileRoutes(files: string[], defaultMode: RenderMode = "ssr"): FileRoute[] {
|
|
40
37
|
return files
|
|
41
38
|
.filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))
|
|
42
39
|
.map((filePath) => parseFilePath(filePath, defaultMode))
|
|
@@ -54,21 +51,19 @@ function parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
const fileName = getFileName(route)
|
|
57
|
-
const isLayout = fileName ===
|
|
58
|
-
const isError = fileName ===
|
|
59
|
-
const isLoading = fileName ===
|
|
60
|
-
const isCatchAll = route.includes(
|
|
54
|
+
const isLayout = fileName === "_layout"
|
|
55
|
+
const isError = fileName === "_error"
|
|
56
|
+
const isLoading = fileName === "_loading"
|
|
57
|
+
const isCatchAll = route.includes("[...")
|
|
61
58
|
|
|
62
59
|
// Get directory path (strip groups for consistent grouping)
|
|
63
|
-
const parts = route.split(
|
|
60
|
+
const parts = route.split("/")
|
|
64
61
|
parts.pop() // remove filename
|
|
65
|
-
const dirPath = parts
|
|
66
|
-
.filter((s) => !(s.startsWith('(') && s.endsWith(')')))
|
|
67
|
-
.join('/')
|
|
62
|
+
const dirPath = parts.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/")
|
|
68
63
|
|
|
69
64
|
// Convert file path to URL pattern
|
|
70
65
|
const urlPath = filePathToUrlPath(route)
|
|
71
|
-
const depth = urlPath ===
|
|
66
|
+
const depth = urlPath === "/" ? 0 : urlPath.split("/").filter(Boolean).length
|
|
72
67
|
|
|
73
68
|
return {
|
|
74
69
|
filePath,
|
|
@@ -96,18 +91,18 @@ function parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {
|
|
|
96
91
|
* "_layout" → "/" (layout marker)
|
|
97
92
|
*/
|
|
98
93
|
export function filePathToUrlPath(filePath: string): string {
|
|
99
|
-
const segments = filePath.split(
|
|
94
|
+
const segments = filePath.split("/")
|
|
100
95
|
const urlSegments: string[] = []
|
|
101
96
|
|
|
102
97
|
for (const seg of segments) {
|
|
103
98
|
// Skip route groups "(name)"
|
|
104
|
-
if (seg.startsWith(
|
|
99
|
+
if (seg.startsWith("(") && seg.endsWith(")")) continue
|
|
105
100
|
|
|
106
101
|
// Skip special files
|
|
107
|
-
if (seg ===
|
|
102
|
+
if (seg === "_layout" || seg === "_error" || seg === "_loading") continue
|
|
108
103
|
|
|
109
104
|
// "index" maps to the parent path
|
|
110
|
-
if (seg ===
|
|
105
|
+
if (seg === "index") continue
|
|
111
106
|
|
|
112
107
|
// Catch-all: [...param] → :param*
|
|
113
108
|
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/)
|
|
@@ -126,8 +121,8 @@ export function filePathToUrlPath(filePath: string): string {
|
|
|
126
121
|
urlSegments.push(seg)
|
|
127
122
|
}
|
|
128
123
|
|
|
129
|
-
const path = `/${urlSegments.join(
|
|
130
|
-
return path ||
|
|
124
|
+
const path = `/${urlSegments.join("/")}`
|
|
125
|
+
return path || "/"
|
|
131
126
|
}
|
|
132
127
|
|
|
133
128
|
/** Sort routes: static before dynamic, catch-all last. */
|
|
@@ -137,16 +132,16 @@ function sortRoutes(a: FileRoute, b: FileRoute): number {
|
|
|
137
132
|
// Layouts go first within same depth
|
|
138
133
|
if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1
|
|
139
134
|
// Static segments before dynamic
|
|
140
|
-
const aDynamic = a.urlPath.includes(
|
|
141
|
-
const bDynamic = b.urlPath.includes(
|
|
135
|
+
const aDynamic = a.urlPath.includes(":")
|
|
136
|
+
const bDynamic = b.urlPath.includes(":")
|
|
142
137
|
if (aDynamic !== bDynamic) return aDynamic ? 1 : -1
|
|
143
138
|
// Alphabetical
|
|
144
139
|
return a.urlPath.localeCompare(b.urlPath)
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
function getFileName(filePath: string): string {
|
|
148
|
-
const parts = filePath.split(
|
|
149
|
-
return parts[parts.length - 1] ??
|
|
143
|
+
const parts = filePath.split("/")
|
|
144
|
+
return parts[parts.length - 1] ?? ""
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
// ─── Route generation (for Vite plugin) ─────────────────────────────────────
|
|
@@ -180,7 +175,7 @@ function getOrCreateChild(node: RouteNode, segment: string): RouteNode {
|
|
|
180
175
|
function resolveNode(root: RouteNode, dirPath: string): RouteNode {
|
|
181
176
|
let node = root
|
|
182
177
|
if (dirPath) {
|
|
183
|
-
for (const segment of dirPath.split(
|
|
178
|
+
for (const segment of dirPath.split("/")) {
|
|
184
179
|
node = getOrCreateChild(node, segment)
|
|
185
180
|
}
|
|
186
181
|
}
|
|
@@ -207,19 +202,16 @@ function buildRouteTree(routes: FileRoute[]): RouteNode {
|
|
|
207
202
|
* Wires up layouts as parent routes with children, loaders, guards,
|
|
208
203
|
* error/loading components, middleware, and meta from route module exports.
|
|
209
204
|
*/
|
|
210
|
-
export function generateRouteModule(
|
|
211
|
-
files: string[],
|
|
212
|
-
routesDir: string,
|
|
213
|
-
): string {
|
|
205
|
+
export function generateRouteModule(files: string[], routesDir: string): string {
|
|
214
206
|
const routes = parseFileRoutes(files)
|
|
215
207
|
const tree = buildRouteTree(routes)
|
|
216
208
|
const imports: string[] = []
|
|
217
209
|
let importCounter = 0
|
|
218
210
|
|
|
219
|
-
function nextImport(filePath: string, exportName =
|
|
211
|
+
function nextImport(filePath: string, exportName = "default"): string {
|
|
220
212
|
const name = `_${importCounter++}`
|
|
221
213
|
const fullPath = `${routesDir}/${filePath}`
|
|
222
|
-
if (exportName ===
|
|
214
|
+
if (exportName === "default") {
|
|
223
215
|
imports.push(`import ${name} from "${fullPath}"`)
|
|
224
216
|
} else {
|
|
225
217
|
imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`)
|
|
@@ -227,17 +219,13 @@ export function generateRouteModule(
|
|
|
227
219
|
return name
|
|
228
220
|
}
|
|
229
221
|
|
|
230
|
-
function nextLazy(
|
|
231
|
-
filePath: string,
|
|
232
|
-
loadingName?: string,
|
|
233
|
-
errorName?: string,
|
|
234
|
-
): string {
|
|
222
|
+
function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {
|
|
235
223
|
const name = `_${importCounter++}`
|
|
236
224
|
const fullPath = `${routesDir}/${filePath}`
|
|
237
225
|
const opts: string[] = []
|
|
238
226
|
if (loadingName) opts.push(`loading: ${loadingName}`)
|
|
239
227
|
if (errorName) opts.push(`error: ${errorName}`)
|
|
240
|
-
const optsStr = opts.length > 0 ? `, { ${opts.join(
|
|
228
|
+
const optsStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : ""
|
|
241
229
|
imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`)
|
|
242
230
|
return name
|
|
243
231
|
}
|
|
@@ -272,7 +260,7 @@ export function generateRouteModule(
|
|
|
272
260
|
props.push(`${indent} errorComponent: ${mod}.error`)
|
|
273
261
|
}
|
|
274
262
|
|
|
275
|
-
return `${indent}{\n${props.join(
|
|
263
|
+
return `${indent}{\n${props.join(",\n")}\n${indent}}`
|
|
276
264
|
}
|
|
277
265
|
|
|
278
266
|
function wrapWithLayout(
|
|
@@ -283,7 +271,7 @@ export function generateRouteModule(
|
|
|
283
271
|
): string {
|
|
284
272
|
const layout = node.layout as FileRoute
|
|
285
273
|
const layoutMod = nextModuleImport(layout.filePath)
|
|
286
|
-
const layoutComp = nextImport(layout.filePath,
|
|
274
|
+
const layoutComp = nextImport(layout.filePath, "layout")
|
|
287
275
|
|
|
288
276
|
const props: string[] = [
|
|
289
277
|
`${indent}path: ${JSON.stringify(layout.urlPath)}`,
|
|
@@ -296,22 +284,20 @@ export function generateRouteModule(
|
|
|
296
284
|
props.push(`${indent}errorComponent: ${errorName}`)
|
|
297
285
|
}
|
|
298
286
|
if (children.length > 0) {
|
|
299
|
-
props.push(`${indent}children: [\n${children.join(
|
|
287
|
+
props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`)
|
|
300
288
|
}
|
|
301
289
|
|
|
302
|
-
return `${indent}{\n${props.map((p) => ` ${p}`).join(
|
|
290
|
+
return `${indent}{\n${props.map((p) => ` ${p}`).join(",\n")}\n${indent}}`
|
|
303
291
|
}
|
|
304
292
|
|
|
305
293
|
/**
|
|
306
294
|
* Generate route definitions for a tree node.
|
|
307
295
|
*/
|
|
308
296
|
function generateNode(node: RouteNode, depth: number): string[] {
|
|
309
|
-
const indent =
|
|
297
|
+
const indent = " ".repeat(depth + 1)
|
|
310
298
|
|
|
311
299
|
const errorName = node.error ? nextImport(node.error.filePath) : undefined
|
|
312
|
-
const loadingName = node.loading
|
|
313
|
-
? nextImport(node.loading.filePath)
|
|
314
|
-
: undefined
|
|
300
|
+
const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined
|
|
315
301
|
|
|
316
302
|
const childRouteDefs: string[] = []
|
|
317
303
|
for (const [, childNode] of node.children) {
|
|
@@ -334,9 +320,9 @@ export function generateRouteModule(
|
|
|
334
320
|
|
|
335
321
|
return [
|
|
336
322
|
`import { lazy } from "@pyreon/router"`,
|
|
337
|
-
|
|
323
|
+
"",
|
|
338
324
|
...imports,
|
|
339
|
-
|
|
325
|
+
"",
|
|
340
326
|
// Filter out undefined properties at runtime
|
|
341
327
|
`function clean(routes) {`,
|
|
342
328
|
` return routes.map(r => {`,
|
|
@@ -346,21 +332,18 @@ export function generateRouteModule(
|
|
|
346
332
|
` return c`,
|
|
347
333
|
` })`,
|
|
348
334
|
`}`,
|
|
349
|
-
|
|
335
|
+
"",
|
|
350
336
|
`export const routes = clean([`,
|
|
351
|
-
routeDefs.join(
|
|
337
|
+
routeDefs.join(",\n"),
|
|
352
338
|
`])`,
|
|
353
|
-
].join(
|
|
339
|
+
].join("\n")
|
|
354
340
|
}
|
|
355
341
|
|
|
356
342
|
/**
|
|
357
343
|
* Generate a virtual module that maps URL patterns to their middleware exports.
|
|
358
344
|
* Used by the server entry to dispatch per-route middleware.
|
|
359
345
|
*/
|
|
360
|
-
export function generateMiddlewareModule(
|
|
361
|
-
files: string[],
|
|
362
|
-
routesDir: string,
|
|
363
|
-
): string {
|
|
346
|
+
export function generateMiddlewareModule(files: string[], routesDir: string): string {
|
|
364
347
|
const routes = parseFileRoutes(files)
|
|
365
348
|
const imports: string[] = []
|
|
366
349
|
const entries: string[] = []
|
|
@@ -371,18 +354,16 @@ export function generateMiddlewareModule(
|
|
|
371
354
|
const name = `_mw${counter++}`
|
|
372
355
|
const fullPath = `${routesDir}/${route.filePath}`
|
|
373
356
|
imports.push(`import { middleware as ${name} } from "${fullPath}"`)
|
|
374
|
-
entries.push(
|
|
375
|
-
` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`,
|
|
376
|
-
)
|
|
357
|
+
entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)
|
|
377
358
|
}
|
|
378
359
|
|
|
379
360
|
return [
|
|
380
361
|
...imports,
|
|
381
|
-
|
|
362
|
+
"",
|
|
382
363
|
`export const routeMiddleware = [`,
|
|
383
|
-
entries.join(
|
|
364
|
+
entries.join(",\n"),
|
|
384
365
|
`].filter(e => e.middleware)`,
|
|
385
|
-
].join(
|
|
366
|
+
].join("\n")
|
|
386
367
|
}
|
|
387
368
|
|
|
388
369
|
/**
|
|
@@ -390,8 +371,8 @@ export function generateMiddlewareModule(
|
|
|
390
371
|
* Returns paths relative to the routes directory.
|
|
391
372
|
*/
|
|
392
373
|
export async function scanRouteFiles(routesDir: string): Promise<string[]> {
|
|
393
|
-
const { readdir } = await import(
|
|
394
|
-
const { join, relative } = await import(
|
|
374
|
+
const { readdir } = await import("node:fs/promises")
|
|
375
|
+
const { join, relative } = await import("node:path")
|
|
395
376
|
|
|
396
377
|
const files: string[] = []
|
|
397
378
|
|