@pyreon/zero 0.4.1 → 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.
Files changed (110) hide show
  1. package/lib/cache.js.map +1 -1
  2. package/lib/client.js.map +1 -1
  3. package/lib/config.js.map +1 -1
  4. package/lib/font.js.map +1 -1
  5. package/lib/fs-router-BkbIWqek.js.map +1 -1
  6. package/lib/fs-router-n4VA4lxu.js.map +1 -1
  7. package/lib/image-plugin.js.map +1 -1
  8. package/lib/image.js +5 -5
  9. package/lib/image.js.map +1 -1
  10. package/lib/index.js +14 -14
  11. package/lib/index.js.map +1 -1
  12. package/lib/link.js +9 -9
  13. package/lib/link.js.map +1 -1
  14. package/lib/script.js +1 -1
  15. package/lib/script.js.map +1 -1
  16. package/lib/seo.js.map +1 -1
  17. package/lib/theme.js +2 -2
  18. package/lib/theme.js.map +1 -1
  19. package/package.json +14 -13
  20. package/src/actions.ts +20 -28
  21. package/src/adapters/bun.ts +7 -7
  22. package/src/adapters/index.ts +12 -14
  23. package/src/adapters/node.ts +8 -11
  24. package/src/adapters/static.ts +3 -3
  25. package/src/api-routes.ts +23 -50
  26. package/src/app.ts +9 -13
  27. package/src/cache.ts +16 -29
  28. package/src/client.ts +8 -8
  29. package/src/compression.ts +21 -28
  30. package/src/config.ts +6 -7
  31. package/src/cors.ts +20 -28
  32. package/src/entry-server.ts +15 -19
  33. package/src/error-overlay.ts +10 -13
  34. package/src/font.ts +44 -55
  35. package/src/fs-router.ts +44 -63
  36. package/src/image-plugin.ts +53 -79
  37. package/src/image.tsx +41 -43
  38. package/src/index.ts +36 -36
  39. package/src/isr.ts +8 -8
  40. package/src/link.tsx +35 -38
  41. package/src/rate-limit.ts +15 -15
  42. package/src/script.tsx +21 -22
  43. package/src/seo.ts +47 -57
  44. package/src/sharp.d.ts +2 -6
  45. package/src/testing.ts +8 -12
  46. package/src/theme.tsx +19 -21
  47. package/src/types.ts +6 -6
  48. package/src/utils/use-intersection-observer.ts +2 -2
  49. package/src/utils/with-headers.ts +1 -4
  50. package/src/vite-plugin.ts +21 -28
  51. package/lib/types/actions.d.ts +0 -57
  52. package/lib/types/actions.d.ts.map +0 -1
  53. package/lib/types/adapters/bun.d.ts +0 -6
  54. package/lib/types/adapters/bun.d.ts.map +0 -1
  55. package/lib/types/adapters/index.d.ts +0 -10
  56. package/lib/types/adapters/index.d.ts.map +0 -1
  57. package/lib/types/adapters/node.d.ts +0 -6
  58. package/lib/types/adapters/node.d.ts.map +0 -1
  59. package/lib/types/adapters/static.d.ts +0 -7
  60. package/lib/types/adapters/static.d.ts.map +0 -1
  61. package/lib/types/api-routes.d.ts +0 -66
  62. package/lib/types/api-routes.d.ts.map +0 -1
  63. package/lib/types/app.d.ts +0 -24
  64. package/lib/types/app.d.ts.map +0 -1
  65. package/lib/types/cache.d.ts +0 -54
  66. package/lib/types/cache.d.ts.map +0 -1
  67. package/lib/types/client.d.ts +0 -19
  68. package/lib/types/client.d.ts.map +0 -1
  69. package/lib/types/compression.d.ts +0 -33
  70. package/lib/types/compression.d.ts.map +0 -1
  71. package/lib/types/config.d.ts +0 -18
  72. package/lib/types/config.d.ts.map +0 -1
  73. package/lib/types/cors.d.ts +0 -32
  74. package/lib/types/cors.d.ts.map +0 -1
  75. package/lib/types/entry-server.d.ts +0 -34
  76. package/lib/types/entry-server.d.ts.map +0 -1
  77. package/lib/types/error-overlay.d.ts +0 -6
  78. package/lib/types/error-overlay.d.ts.map +0 -1
  79. package/lib/types/font.d.ts +0 -119
  80. package/lib/types/font.d.ts.map +0 -1
  81. package/lib/types/fs-router.d.ts +0 -38
  82. package/lib/types/fs-router.d.ts.map +0 -1
  83. package/lib/types/image-plugin.d.ts +0 -79
  84. package/lib/types/image-plugin.d.ts.map +0 -1
  85. package/lib/types/image.d.ts +0 -51
  86. package/lib/types/image.d.ts.map +0 -1
  87. package/lib/types/index.d.ts +0 -37
  88. package/lib/types/index.d.ts.map +0 -1
  89. package/lib/types/isr.d.ts +0 -9
  90. package/lib/types/isr.d.ts.map +0 -1
  91. package/lib/types/link.d.ts +0 -116
  92. package/lib/types/link.d.ts.map +0 -1
  93. package/lib/types/rate-limit.d.ts +0 -34
  94. package/lib/types/rate-limit.d.ts.map +0 -1
  95. package/lib/types/script.d.ts +0 -35
  96. package/lib/types/script.d.ts.map +0 -1
  97. package/lib/types/seo.d.ts +0 -88
  98. package/lib/types/seo.d.ts.map +0 -1
  99. package/lib/types/testing.d.ts +0 -85
  100. package/lib/types/testing.d.ts.map +0 -1
  101. package/lib/types/theme.d.ts +0 -39
  102. package/lib/types/theme.d.ts.map +0 -1
  103. package/lib/types/types.d.ts +0 -109
  104. package/lib/types/types.d.ts.map +0 -1
  105. package/lib/types/utils/use-intersection-observer.d.ts +0 -10
  106. package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
  107. package/lib/types/utils/with-headers.d.ts +0 -6
  108. package/lib/types/utils/with-headers.d.ts.map +0 -1
  109. package/lib/types/vite-plugin.d.ts +0 -17
  110. package/lib/types/vite-plugin.d.ts.map +0 -1
@@ -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 || 'Unknown error')
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, '&amp;')
100
- .replace(/</g, '&lt;')
101
- .replace(/>/g, '&gt;')
102
- .replace(/"/g, '&quot;')
99
+ .replace(/&/g, "&amp;")
100
+ .replace(/</g, "&lt;")
101
+ .replace(/>/g, "&gt;")
102
+ .replace(/"/g, "&quot;")
103
103
  }
104
104
 
105
105
  function formatStack(stack: string): string {
106
106
  return stack
107
- .split('\n')
107
+ .split("\n")
108
108
  .map((line) => {
109
- if (line.includes('at ')) {
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('\n')
117
+ .join("\n")
121
118
  }
package/src/font.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Plugin } from 'vite'
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?: 'normal' | 'italic'
60
+ style?: "normal" | "italic"
61
61
  display?: FontDisplay
62
62
  }
63
63
 
64
- export type FontDisplay = 'auto' | 'block' | 'swap' | 'fallback' | 'optional'
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 === 'string') {
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] ?? '').trim()
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('ital')
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(';').map(Number),
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 ? 'ital,wght' : 'wght'
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 ?? '400'};
202
- font-style: ${f.style ?? 'normal'};
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('\n\n')
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('\n')}
222
+ ${overrides.join("\n")}
229
223
  }`
230
224
  })
231
- .join('\n\n')
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('.').pop()
234
+ const ext = f.src.split(".").pop()
241
235
  const type =
242
- ext === 'woff2'
243
- ? 'font/woff2'
244
- : ext === 'woff'
245
- ? 'font/woff'
246
- : ext === 'ttf'
247
- ? 'font/ttf'
248
- : 'font/otf'
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('\n')
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
- 'User-Agent':
261
- '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',
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('?')[0] ?? 'font'
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 ?? 'swap'
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: 'pyreon-zero-fonts',
347
+ name: "pyreon-zero-fonts",
354
348
 
355
349
  configResolved(resolvedConfig) {
356
- isBuild = resolvedConfig.command === 'build'
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, 'assets/fonts')
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: 'asset',
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('</head>', `${tags.join('\n')}\n</head>`)
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
- 0,
418
- opts.googleFamilies.length,
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('\n')
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 './types'
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 = ['.tsx', '.jsx', '.ts', '.js']
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 === '_layout'
58
- const isError = fileName === '_error'
59
- const isLoading = fileName === '_loading'
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 === '/' ? 0 : urlPath.split('/').filter(Boolean).length
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('(') && seg.endsWith(')')) continue
99
+ if (seg.startsWith("(") && seg.endsWith(")")) continue
105
100
 
106
101
  // Skip special files
107
- if (seg === '_layout' || seg === '_error' || seg === '_loading') continue
102
+ if (seg === "_layout" || seg === "_error" || seg === "_loading") continue
108
103
 
109
104
  // "index" maps to the parent path
110
- if (seg === 'index') continue
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 = 'default'): string {
211
+ function nextImport(filePath: string, exportName = "default"): string {
220
212
  const name = `_${importCounter++}`
221
213
  const fullPath = `${routesDir}/${filePath}`
222
- if (exportName === 'default') {
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(',\n')}\n${indent}}`
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, 'layout')
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(',\n')}\n${indent}]`)
287
+ props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`)
300
288
  }
301
289
 
302
- return `${indent}{\n${props.map((p) => ` ${p}`).join(',\n')}\n${indent}}`
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 = ' '.repeat(depth + 1)
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(',\n'),
337
+ routeDefs.join(",\n"),
352
338
  `])`,
353
- ].join('\n')
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(',\n'),
364
+ entries.join(",\n"),
384
365
  `].filter(e => e.middleware)`,
385
- ].join('\n')
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('node:fs/promises')
394
- const { join, relative } = await import('node:path')
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