@pyreon/zero 0.11.4 → 0.11.6

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/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,11 +164,11 @@ export function parseGoogleFamily(input: string): ResolvedFont {
164
164
  /**
165
165
  * Generate a Google Fonts CSS URL.
166
166
  */
167
- export function googleFontsUrl(families: ResolvedFont[], display: FontDisplay = "swap"): string {
167
+ export function googleFontsUrl(families: ResolvedFont[], display: FontDisplay = 'swap'): string {
168
168
  const params = families
169
169
  .map((f) => {
170
- const axes = f.italic ? "ital,wght" : "wght"
171
- const name = f.family.replace(/ /g, "+")
170
+ const axes = f.italic ? 'ital,wght' : 'wght'
171
+ const name = f.family.replace(/ /g, '+')
172
172
 
173
173
  if (f.variable) {
174
174
  const range = `${f.weightRange[0]}..${f.weightRange[1]}`
@@ -176,10 +176,10 @@ export function googleFontsUrl(families: ResolvedFont[], display: FontDisplay =
176
176
  return `family=${name}:${axes}@${value}`
177
177
  }
178
178
 
179
- const values = f.weights.map((w) => (f.italic ? `0,${w};1,${w}` : String(w))).join(";")
179
+ const values = f.weights.map((w) => (f.italic ? `0,${w};1,${w}` : String(w))).join(';')
180
180
  return `family=${name}:${axes}@${values}`
181
181
  })
182
- .join("&")
182
+ .join('&')
183
183
 
184
184
  return `https://fonts.googleapis.com/css2?${params}&display=${display}`
185
185
  }
@@ -193,12 +193,12 @@ function localFontFaces(fonts: LocalFont[], display: FontDisplay): string {
193
193
  (f) => `@font-face {
194
194
  font-family: "${f.family}";
195
195
  src: url("${f.src}");
196
- font-weight: ${f.weight ?? "400"};
197
- font-style: ${f.style ?? "normal"};
196
+ font-weight: ${f.weight ?? '400'};
197
+ font-style: ${f.style ?? 'normal'};
198
198
  font-display: ${f.display ?? display};
199
199
  }`,
200
200
  )
201
- .join("\n\n")
201
+ .join('\n\n')
202
202
  }
203
203
 
204
204
  /**
@@ -219,10 +219,10 @@ function fallbackFontFaces(fallbacks: Record<string, FallbackMetrics>): string {
219
219
  return `@font-face {
220
220
  font-family: "${family} Fallback";
221
221
  src: local("${metrics.fallback}");
222
- ${overrides.join("\n")}
222
+ ${overrides.join('\n')}
223
223
  }`
224
224
  })
225
- .join("\n\n")
225
+ .join('\n\n')
226
226
  }
227
227
 
228
228
  /**
@@ -231,18 +231,18 @@ ${overrides.join("\n")}
231
231
  function preloadTags(fonts: LocalFont[]): string {
232
232
  return fonts
233
233
  .map((f) => {
234
- const ext = f.src.split(".").pop()
234
+ const ext = f.src.split('.').pop()
235
235
  const type =
236
- ext === "woff2"
237
- ? "font/woff2"
238
- : ext === "woff"
239
- ? "font/woff"
240
- : ext === "ttf"
241
- ? "font/ttf"
242
- : "font/otf"
236
+ ext === 'woff2'
237
+ ? 'font/woff2'
238
+ : ext === 'woff'
239
+ ? 'font/woff'
240
+ : ext === 'ttf'
241
+ ? 'font/ttf'
242
+ : 'font/otf'
243
243
  return `<link rel="preload" href="${f.src}" as="font" type="${type}" crossorigin>`
244
244
  })
245
- .join("\n")
245
+ .join('\n')
246
246
  }
247
247
 
248
248
  /**
@@ -251,8 +251,8 @@ function preloadTags(fonts: LocalFont[]): string {
251
251
  async function downloadGoogleFontsCSS(url: string): Promise<string> {
252
252
  const response = await fetch(url, {
253
253
  headers: {
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",
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',
256
256
  },
257
257
  })
258
258
  if (!response.ok) {
@@ -300,8 +300,8 @@ async function selfHostFonts(
300
300
  let rewrittenCss = css
301
301
 
302
302
  for (const url of fontUrls) {
303
- const urlParts = url.split("/")
304
- const fileName = urlParts.at(-1)?.split("?")[0] ?? "font"
303
+ const urlParts = url.split('/')
304
+ const fileName = urlParts.at(-1)?.split('?')[0] ?? 'font'
305
305
  const content = await downloadFontFile(url)
306
306
 
307
307
  fontFiles.push({ name: fileName, content })
@@ -334,27 +334,27 @@ async function selfHostFonts(
334
334
  * }
335
335
  */
336
336
  export function fontPlugin(config: FontConfig = {}): Plugin {
337
- const display = config.display ?? "swap"
337
+ const display = config.display ?? 'swap'
338
338
  const shouldPreload = config.preload !== false
339
339
  const shouldSelfHost = config.selfHost !== false
340
340
  const googleFamilies = (config.google ?? []).map(resolveGoogleFont)
341
341
 
342
342
  let isBuild = false
343
- let selfHostedCSS = ""
343
+ let selfHostedCSS = ''
344
344
  let selfHostedFontFiles: Array<{ name: string; content: Buffer }> = []
345
345
 
346
346
  return {
347
- name: "pyreon-zero-fonts",
347
+ name: 'pyreon-zero-fonts',
348
348
 
349
349
  configResolved(resolvedConfig) {
350
- isBuild = resolvedConfig.command === "build"
350
+ isBuild = resolvedConfig.command === 'build'
351
351
  },
352
352
 
353
353
  async buildStart() {
354
354
  if (isBuild && shouldSelfHost && googleFamilies.length > 0) {
355
355
  const cssUrl = googleFontsUrl(googleFamilies, display)
356
356
  try {
357
- const result = await selfHostFonts(cssUrl, "assets/fonts")
357
+ const result = await selfHostFonts(cssUrl, 'assets/fonts')
358
358
  selfHostedCSS = result.css
359
359
  selfHostedFontFiles = result.fontFiles
360
360
  } catch {
@@ -367,7 +367,7 @@ export function fontPlugin(config: FontConfig = {}): Plugin {
367
367
  // Emit self-hosted font files as assets
368
368
  for (const file of selfHostedFontFiles) {
369
369
  this.emitFile({
370
- type: "asset",
370
+ type: 'asset',
371
371
  fileName: `assets/fonts/${file.name}`,
372
372
  source: file.content,
373
373
  })
@@ -388,7 +388,7 @@ export function fontPlugin(config: FontConfig = {}): Plugin {
388
388
  collectLocalFontTags(tags, config, shouldPreload, display)
389
389
 
390
390
  if (tags.length === 0) return html
391
- return html.replace("</head>", `${tags.join("\n")}\n</head>`)
391
+ return html.replace('</head>', `${tags.join('\n')}\n</head>`)
392
392
  },
393
393
  }
394
394
  }
@@ -408,8 +408,8 @@ function collectGoogleFontTags(
408
408
  tags.push(`<style>${opts.selfHostedCSS}</style>`)
409
409
  if (opts.shouldPreload) {
410
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"
411
+ const ext = file.name.split('.').pop()
412
+ const type = ext === 'woff2' ? 'font/woff2' : 'font/woff'
413
413
  tags.push(
414
414
  `<link rel="preload" href="/assets/fonts/${file.name}" as="font" type="${type}" crossorigin>`,
415
415
  )
@@ -446,6 +446,6 @@ function collectLocalFontTags(
446
446
  export function fontVariables(families: Record<string, string>): string {
447
447
  const vars = Object.entries(families)
448
448
  .map(([key, value]) => ` --font-${key}: ${value};`)
449
- .join("\n")
449
+ .join('\n')
450
450
  return `:root {\n${vars}\n}`
451
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,7 +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(files: string[], defaultMode: RenderMode = "ssr"): FileRoute[] {
36
+ export function parseFileRoutes(files: string[], defaultMode: RenderMode = 'ssr'): FileRoute[] {
37
37
  return files
38
38
  .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))
39
39
  .map((filePath) => parseFilePath(filePath, defaultMode))
@@ -51,19 +51,19 @@ function parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {
51
51
  }
52
52
 
53
53
  const fileName = getFileName(route)
54
- const isLayout = fileName === "_layout"
55
- const isError = fileName === "_error"
56
- const isLoading = fileName === "_loading"
57
- const isCatchAll = route.includes("[...")
54
+ const isLayout = fileName === '_layout'
55
+ const isError = fileName === '_error'
56
+ const isLoading = fileName === '_loading'
57
+ const isCatchAll = route.includes('[...')
58
58
 
59
59
  // Get directory path (strip groups for consistent grouping)
60
- const parts = route.split("/")
60
+ const parts = route.split('/')
61
61
  parts.pop() // remove filename
62
- const dirPath = parts.filter((s) => !(s.startsWith("(") && s.endsWith(")"))).join("/")
62
+ const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')
63
63
 
64
64
  // Convert file path to URL pattern
65
65
  const urlPath = filePathToUrlPath(route)
66
- const depth = urlPath === "/" ? 0 : urlPath.split("/").filter(Boolean).length
66
+ const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length
67
67
 
68
68
  return {
69
69
  filePath,
@@ -91,18 +91,18 @@ function parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {
91
91
  * "_layout" → "/" (layout marker)
92
92
  */
93
93
  export function filePathToUrlPath(filePath: string): string {
94
- const segments = filePath.split("/")
94
+ const segments = filePath.split('/')
95
95
  const urlSegments: string[] = []
96
96
 
97
97
  for (const seg of segments) {
98
98
  // Skip route groups "(name)"
99
- if (seg.startsWith("(") && seg.endsWith(")")) continue
99
+ if (seg.startsWith('(') && seg.endsWith(')')) continue
100
100
 
101
101
  // Skip special files
102
- if (seg === "_layout" || seg === "_error" || seg === "_loading") continue
102
+ if (seg === '_layout' || seg === '_error' || seg === '_loading') continue
103
103
 
104
104
  // "index" maps to the parent path
105
- if (seg === "index") continue
105
+ if (seg === 'index') continue
106
106
 
107
107
  // Catch-all: [...param] → :param*
108
108
  const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/)
@@ -121,8 +121,8 @@ export function filePathToUrlPath(filePath: string): string {
121
121
  urlSegments.push(seg)
122
122
  }
123
123
 
124
- const path = `/${urlSegments.join("/")}`
125
- return path || "/"
124
+ const path = `/${urlSegments.join('/')}`
125
+ return path || '/'
126
126
  }
127
127
 
128
128
  /** Sort routes: static before dynamic, catch-all last. */
@@ -132,16 +132,16 @@ function sortRoutes(a: FileRoute, b: FileRoute): number {
132
132
  // Layouts go first within same depth
133
133
  if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1
134
134
  // Static segments before dynamic
135
- const aDynamic = a.urlPath.includes(":")
136
- const bDynamic = b.urlPath.includes(":")
135
+ const aDynamic = a.urlPath.includes(':')
136
+ const bDynamic = b.urlPath.includes(':')
137
137
  if (aDynamic !== bDynamic) return aDynamic ? 1 : -1
138
138
  // Alphabetical
139
139
  return a.urlPath.localeCompare(b.urlPath)
140
140
  }
141
141
 
142
142
  function getFileName(filePath: string): string {
143
- const parts = filePath.split("/")
144
- return parts[parts.length - 1] ?? ""
143
+ const parts = filePath.split('/')
144
+ return parts[parts.length - 1] ?? ''
145
145
  }
146
146
 
147
147
  // ─── Route generation (for Vite plugin) ─────────────────────────────────────
@@ -175,7 +175,7 @@ function getOrCreateChild(node: RouteNode, segment: string): RouteNode {
175
175
  function resolveNode(root: RouteNode, dirPath: string): RouteNode {
176
176
  let node = root
177
177
  if (dirPath) {
178
- for (const segment of dirPath.split("/")) {
178
+ for (const segment of dirPath.split('/')) {
179
179
  node = getOrCreateChild(node, segment)
180
180
  }
181
181
  }
@@ -208,10 +208,10 @@ export function generateRouteModule(files: string[], routesDir: string): string
208
208
  const imports: string[] = []
209
209
  let importCounter = 0
210
210
 
211
- function nextImport(filePath: string, exportName = "default"): string {
211
+ function nextImport(filePath: string, exportName = 'default'): string {
212
212
  const name = `_${importCounter++}`
213
213
  const fullPath = `${routesDir}/${filePath}`
214
- if (exportName === "default") {
214
+ if (exportName === 'default') {
215
215
  imports.push(`import ${name} from "${fullPath}"`)
216
216
  } else {
217
217
  imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`)
@@ -225,7 +225,7 @@ export function generateRouteModule(files: string[], routesDir: string): string
225
225
  const opts: string[] = []
226
226
  if (loadingName) opts.push(`loading: ${loadingName}`)
227
227
  if (errorName) opts.push(`error: ${errorName}`)
228
- const optsStr = opts.length > 0 ? `, { ${opts.join(", ")} }` : ""
228
+ const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''
229
229
  imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`)
230
230
  return name
231
231
  }
@@ -260,7 +260,7 @@ export function generateRouteModule(files: string[], routesDir: string): string
260
260
  props.push(`${indent} errorComponent: ${mod}.error`)
261
261
  }
262
262
 
263
- return `${indent}{\n${props.join(",\n")}\n${indent}}`
263
+ return `${indent}{\n${props.join(',\n')}\n${indent}}`
264
264
  }
265
265
 
266
266
  function wrapWithLayout(
@@ -271,7 +271,7 @@ export function generateRouteModule(files: string[], routesDir: string): string
271
271
  ): string {
272
272
  const layout = node.layout as FileRoute
273
273
  const layoutMod = nextModuleImport(layout.filePath)
274
- const layoutComp = nextImport(layout.filePath, "layout")
274
+ const layoutComp = nextImport(layout.filePath, 'layout')
275
275
 
276
276
  const props: string[] = [
277
277
  `${indent}path: ${JSON.stringify(layout.urlPath)}`,
@@ -284,17 +284,17 @@ export function generateRouteModule(files: string[], routesDir: string): string
284
284
  props.push(`${indent}errorComponent: ${errorName}`)
285
285
  }
286
286
  if (children.length > 0) {
287
- props.push(`${indent}children: [\n${children.join(",\n")}\n${indent}]`)
287
+ props.push(`${indent}children: [\n${children.join(',\n')}\n${indent}]`)
288
288
  }
289
289
 
290
- return `${indent}{\n${props.map((p) => ` ${p}`).join(",\n")}\n${indent}}`
290
+ return `${indent}{\n${props.map((p) => ` ${p}`).join(',\n')}\n${indent}}`
291
291
  }
292
292
 
293
293
  /**
294
294
  * Generate route definitions for a tree node.
295
295
  */
296
296
  function generateNode(node: RouteNode, depth: number): string[] {
297
- const indent = " ".repeat(depth + 1)
297
+ const indent = ' '.repeat(depth + 1)
298
298
 
299
299
  const errorName = node.error ? nextImport(node.error.filePath) : undefined
300
300
  const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined
@@ -320,9 +320,9 @@ export function generateRouteModule(files: string[], routesDir: string): string
320
320
 
321
321
  return [
322
322
  `import { lazy } from "@pyreon/router"`,
323
- "",
323
+ '',
324
324
  ...imports,
325
- "",
325
+ '',
326
326
  // Filter out undefined properties at runtime
327
327
  `function clean(routes) {`,
328
328
  ` return routes.map(r => {`,
@@ -332,11 +332,11 @@ export function generateRouteModule(files: string[], routesDir: string): string
332
332
  ` return c`,
333
333
  ` })`,
334
334
  `}`,
335
- "",
335
+ '',
336
336
  `export const routes = clean([`,
337
- routeDefs.join(",\n"),
337
+ routeDefs.join(',\n'),
338
338
  `])`,
339
- ].join("\n")
339
+ ].join('\n')
340
340
  }
341
341
 
342
342
  /**
@@ -359,11 +359,11 @@ export function generateMiddlewareModule(files: string[], routesDir: string): st
359
359
 
360
360
  return [
361
361
  ...imports,
362
- "",
362
+ '',
363
363
  `export const routeMiddleware = [`,
364
- entries.join(",\n"),
364
+ entries.join(',\n'),
365
365
  `].filter(e => e.middleware)`,
366
- ].join("\n")
366
+ ].join('\n')
367
367
  }
368
368
 
369
369
  /**
@@ -371,8 +371,8 @@ export function generateMiddlewareModule(files: string[], routesDir: string): st
371
371
  * Returns paths relative to the routes directory.
372
372
  */
373
373
  export async function scanRouteFiles(routesDir: string): Promise<string[]> {
374
- const { readdir } = await import("node:fs/promises")
375
- const { join, relative } = await import("node:path")
374
+ const { readdir } = await import('node:fs/promises')
375
+ const { join, relative } = await import('node:path')
376
376
 
377
377
  const files: string[] = []
378
378