@pyreon/zero 0.1.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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/lib/cache.js +80 -0
  4. package/lib/cache.js.map +1 -0
  5. package/lib/client.js +58 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/config.js +35 -0
  8. package/lib/config.js.map +1 -0
  9. package/lib/font.js +251 -0
  10. package/lib/font.js.map +1 -0
  11. package/lib/fs-router-BkbIWqek.js +30 -0
  12. package/lib/fs-router-BkbIWqek.js.map +1 -0
  13. package/lib/fs-router-jfd1QGLB.js +261 -0
  14. package/lib/fs-router-jfd1QGLB.js.map +1 -0
  15. package/lib/image-plugin.js +289 -0
  16. package/lib/image-plugin.js.map +1 -0
  17. package/lib/image.js +113 -0
  18. package/lib/image.js.map +1 -0
  19. package/lib/index.js +1665 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/link.js +186 -0
  22. package/lib/link.js.map +1 -0
  23. package/lib/script.js +102 -0
  24. package/lib/script.js.map +1 -0
  25. package/lib/seo.js +136 -0
  26. package/lib/seo.js.map +1 -0
  27. package/lib/theme.js +165 -0
  28. package/lib/theme.js.map +1 -0
  29. package/lib/types/adapters/bun.d.ts +6 -0
  30. package/lib/types/adapters/bun.d.ts.map +1 -0
  31. package/lib/types/adapters/index.d.ts +10 -0
  32. package/lib/types/adapters/index.d.ts.map +1 -0
  33. package/lib/types/adapters/node.d.ts +6 -0
  34. package/lib/types/adapters/node.d.ts.map +1 -0
  35. package/lib/types/adapters/static.d.ts +7 -0
  36. package/lib/types/adapters/static.d.ts.map +1 -0
  37. package/lib/types/app.d.ts +24 -0
  38. package/lib/types/app.d.ts.map +1 -0
  39. package/lib/types/cache.d.ts +54 -0
  40. package/lib/types/cache.d.ts.map +1 -0
  41. package/lib/types/client.d.ts +19 -0
  42. package/lib/types/client.d.ts.map +1 -0
  43. package/lib/types/config.d.ts +18 -0
  44. package/lib/types/config.d.ts.map +1 -0
  45. package/lib/types/entry-server.d.ts +26 -0
  46. package/lib/types/entry-server.d.ts.map +1 -0
  47. package/lib/types/font.d.ts +119 -0
  48. package/lib/types/font.d.ts.map +1 -0
  49. package/lib/types/fs-router.d.ts +33 -0
  50. package/lib/types/fs-router.d.ts.map +1 -0
  51. package/lib/types/image-plugin.d.ts +79 -0
  52. package/lib/types/image-plugin.d.ts.map +1 -0
  53. package/lib/types/image.d.ts +50 -0
  54. package/lib/types/image.d.ts.map +1 -0
  55. package/lib/types/index.d.ts +27 -0
  56. package/lib/types/index.d.ts.map +1 -0
  57. package/lib/types/isr.d.ts +9 -0
  58. package/lib/types/isr.d.ts.map +1 -0
  59. package/lib/types/link.d.ts +116 -0
  60. package/lib/types/link.d.ts.map +1 -0
  61. package/lib/types/script.d.ts +34 -0
  62. package/lib/types/script.d.ts.map +1 -0
  63. package/lib/types/seo.d.ts +88 -0
  64. package/lib/types/seo.d.ts.map +1 -0
  65. package/lib/types/theme.d.ts +38 -0
  66. package/lib/types/theme.d.ts.map +1 -0
  67. package/lib/types/types.d.ts +104 -0
  68. package/lib/types/types.d.ts.map +1 -0
  69. package/lib/types/utils/use-intersection-observer.d.ts +10 -0
  70. package/lib/types/utils/use-intersection-observer.d.ts.map +1 -0
  71. package/lib/types/utils/with-headers.d.ts +6 -0
  72. package/lib/types/utils/with-headers.d.ts.map +1 -0
  73. package/lib/types/vite-plugin.d.ts +17 -0
  74. package/lib/types/vite-plugin.d.ts.map +1 -0
  75. package/package.json +100 -0
  76. package/src/adapters/bun.ts +65 -0
  77. package/src/adapters/index.ts +29 -0
  78. package/src/adapters/node.ts +113 -0
  79. package/src/adapters/static.ts +17 -0
  80. package/src/app.ts +62 -0
  81. package/src/cache.ts +149 -0
  82. package/src/client.ts +43 -0
  83. package/src/config.ts +36 -0
  84. package/src/entry-server.ts +51 -0
  85. package/src/font.ts +461 -0
  86. package/src/fs-router.ts +380 -0
  87. package/src/image-plugin.ts +452 -0
  88. package/src/image.tsx +167 -0
  89. package/src/index.ts +119 -0
  90. package/src/isr.ts +95 -0
  91. package/src/link.tsx +266 -0
  92. package/src/script.tsx +133 -0
  93. package/src/seo.ts +281 -0
  94. package/src/sharp.d.ts +20 -0
  95. package/src/theme.tsx +162 -0
  96. package/src/types.ts +130 -0
  97. package/src/utils/use-intersection-observer.ts +36 -0
  98. package/src/utils/with-headers.ts +16 -0
  99. package/src/vite-plugin.ts +92 -0
@@ -0,0 +1,380 @@
1
+ import type { FileRoute, RenderMode } from './types'
2
+
3
+ // ─── File-system route conventions ──────────────────────────────────────────
4
+ //
5
+ // src/routes/
6
+ // _layout.tsx → layout for all routes
7
+ // index.tsx → /
8
+ // about.tsx → /about
9
+ // users/
10
+ // _layout.tsx → layout for /users/*
11
+ // _loading.tsx → loading fallback for /users/*
12
+ // _error.tsx → error boundary for /users/*
13
+ // index.tsx → /users
14
+ // [id].tsx → /users/:id
15
+ // [id]/
16
+ // settings.tsx → /users/:id/settings
17
+ // blog/
18
+ // [...slug].tsx → /blog/* (catch-all)
19
+ //
20
+ // Conventions:
21
+ // [param] → dynamic segment → :param
22
+ // [...param] → catch-all → :param*
23
+ // _layout → layout wrapper (not a route itself)
24
+ // _error → error component
25
+ // _loading → loading component
26
+ // (group) → route group (directory ignored in URL)
27
+
28
+ const ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']
29
+
30
+ /**
31
+ * Parse a set of file paths (relative to routes dir) into FileRoute objects.
32
+ *
33
+ * @param files Array of file paths like ["index.tsx", "users/[id].tsx"]
34
+ * @param defaultMode Default rendering mode from config
35
+ */
36
+ export function parseFileRoutes(
37
+ files: string[],
38
+ defaultMode: RenderMode = 'ssr',
39
+ ): FileRoute[] {
40
+ return files
41
+ .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))
42
+ .map((filePath) => parseFilePath(filePath, defaultMode))
43
+ .sort(sortRoutes)
44
+ }
45
+
46
+ function parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {
47
+ // Remove extension
48
+ let route = filePath
49
+ for (const ext of ROUTE_EXTENSIONS) {
50
+ if (route.endsWith(ext)) {
51
+ route = route.slice(0, -ext.length)
52
+ break
53
+ }
54
+ }
55
+
56
+ const fileName = getFileName(route)
57
+ const isLayout = fileName === '_layout'
58
+ const isError = fileName === '_error'
59
+ const isLoading = fileName === '_loading'
60
+ const isCatchAll = route.includes('[...')
61
+
62
+ // Get directory path (strip groups for consistent grouping)
63
+ const parts = route.split('/')
64
+ parts.pop() // remove filename
65
+ const dirPath = parts
66
+ .filter((s) => !(s.startsWith('(') && s.endsWith(')')))
67
+ .join('/')
68
+
69
+ // Convert file path to URL pattern
70
+ const urlPath = filePathToUrlPath(route)
71
+ const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length
72
+
73
+ return {
74
+ filePath,
75
+ urlPath,
76
+ dirPath,
77
+ depth,
78
+ isLayout,
79
+ isError,
80
+ isLoading,
81
+ isCatchAll,
82
+ renderMode: defaultMode,
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Convert a file path (without extension) to a URL path pattern.
88
+ *
89
+ * Examples:
90
+ * "index" → "/"
91
+ * "about" → "/about"
92
+ * "users/index" → "/users"
93
+ * "users/[id]" → "/users/:id"
94
+ * "blog/[...slug]" → "/blog/:slug*"
95
+ * "(auth)/login" → "/login" (group stripped)
96
+ * "_layout" → "/" (layout marker)
97
+ */
98
+ export function filePathToUrlPath(filePath: string): string {
99
+ const segments = filePath.split('/')
100
+ const urlSegments: string[] = []
101
+
102
+ for (const seg of segments) {
103
+ // Skip route groups "(name)"
104
+ if (seg.startsWith('(') && seg.endsWith(')')) continue
105
+
106
+ // Skip special files
107
+ if (seg === '_layout' || seg === '_error' || seg === '_loading') continue
108
+
109
+ // "index" maps to the parent path
110
+ if (seg === 'index') continue
111
+
112
+ // Catch-all: [...param] → :param*
113
+ const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/)
114
+ if (catchAll) {
115
+ urlSegments.push(`:${catchAll[1]}*`)
116
+ continue
117
+ }
118
+
119
+ // Dynamic: [param] → :param
120
+ const dynamic = seg.match(/^\[(\w+)\]$/)
121
+ if (dynamic) {
122
+ urlSegments.push(`:${dynamic[1]}`)
123
+ continue
124
+ }
125
+
126
+ urlSegments.push(seg)
127
+ }
128
+
129
+ const path = `/${urlSegments.join('/')}`
130
+ return path || '/'
131
+ }
132
+
133
+ /** Sort routes: static before dynamic, catch-all last. */
134
+ function sortRoutes(a: FileRoute, b: FileRoute): number {
135
+ // Catch-all routes go last
136
+ if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1
137
+ // Layouts go first within same depth
138
+ if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1
139
+ // Static segments before dynamic
140
+ const aDynamic = a.urlPath.includes(':')
141
+ const bDynamic = b.urlPath.includes(':')
142
+ if (aDynamic !== bDynamic) return aDynamic ? 1 : -1
143
+ // Alphabetical
144
+ return a.urlPath.localeCompare(b.urlPath)
145
+ }
146
+
147
+ function getFileName(filePath: string): string {
148
+ const parts = filePath.split('/')
149
+ return parts[parts.length - 1] ?? ''
150
+ }
151
+
152
+ // ─── Route generation (for Vite plugin) ─────────────────────────────────────
153
+
154
+ /** Internal tree node for building nested route structures. */
155
+ interface RouteNode {
156
+ /** Page routes at this directory level. */
157
+ pages: FileRoute[]
158
+ /** Layout file for this directory (if any). */
159
+ layout?: FileRoute
160
+ /** Error boundary file (if any). */
161
+ error?: FileRoute
162
+ /** Loading fallback file (if any). */
163
+ loading?: FileRoute
164
+ /** Child directories. */
165
+ children: Map<string, RouteNode>
166
+ }
167
+
168
+ /**
169
+ * Group flat file routes into a directory tree.
170
+ */
171
+ function getOrCreateChild(node: RouteNode, segment: string): RouteNode {
172
+ let child = node.children.get(segment)
173
+ if (!child) {
174
+ child = { pages: [], children: new Map() }
175
+ node.children.set(segment, child)
176
+ }
177
+ return child
178
+ }
179
+
180
+ function resolveNode(root: RouteNode, dirPath: string): RouteNode {
181
+ let node = root
182
+ if (dirPath) {
183
+ for (const segment of dirPath.split('/')) {
184
+ node = getOrCreateChild(node, segment)
185
+ }
186
+ }
187
+ return node
188
+ }
189
+
190
+ function placeRoute(node: RouteNode, route: FileRoute) {
191
+ if (route.isLayout) node.layout = route
192
+ else if (route.isError) node.error = route
193
+ else if (route.isLoading) node.loading = route
194
+ else node.pages.push(route)
195
+ }
196
+
197
+ function buildRouteTree(routes: FileRoute[]): RouteNode {
198
+ const root: RouteNode = { pages: [], children: new Map() }
199
+ for (const route of routes) {
200
+ placeRoute(resolveNode(root, route.dirPath), route)
201
+ }
202
+ return root
203
+ }
204
+
205
+ /**
206
+ * Generate a virtual module that exports a nested route tree.
207
+ * Wires up layouts as parent routes with children, loaders, guards,
208
+ * error/loading components, middleware, and meta from route module exports.
209
+ */
210
+ export function generateRouteModule(
211
+ files: string[],
212
+ routesDir: string,
213
+ ): string {
214
+ const routes = parseFileRoutes(files)
215
+ const tree = buildRouteTree(routes)
216
+ const imports: string[] = []
217
+ let importCounter = 0
218
+
219
+ function nextImport(filePath: string, exportName = 'default'): string {
220
+ const name = `_${importCounter++}`
221
+ const fullPath = `${routesDir}/${filePath}`
222
+ if (exportName === 'default') {
223
+ imports.push(`import ${name} from "${fullPath}"`)
224
+ } else {
225
+ imports.push(`import { ${exportName} as ${name} } from "${fullPath}"`)
226
+ }
227
+ return name
228
+ }
229
+
230
+ function nextLazy(
231
+ filePath: string,
232
+ loadingName?: string,
233
+ errorName?: string,
234
+ ): string {
235
+ const name = `_${importCounter++}`
236
+ const fullPath = `${routesDir}/${filePath}`
237
+ const opts: string[] = []
238
+ if (loadingName) opts.push(`loading: ${loadingName}`)
239
+ if (errorName) opts.push(`error: ${errorName}`)
240
+ const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''
241
+ imports.push(`const ${name} = lazy(() => import("${fullPath}")${optsStr})`)
242
+ return name
243
+ }
244
+
245
+ function nextModuleImport(filePath: string): string {
246
+ const name = `_m${importCounter++}`
247
+ const fullPath = `${routesDir}/${filePath}`
248
+ imports.push(`import * as ${name} from "${fullPath}"`)
249
+ return name
250
+ }
251
+
252
+ function generatePageRoute(
253
+ page: FileRoute,
254
+ indent: string,
255
+ loadingName: string | undefined,
256
+ errorName: string | undefined,
257
+ ): string {
258
+ const mod = nextModuleImport(page.filePath)
259
+ const comp = nextLazy(page.filePath, loadingName, errorName)
260
+
261
+ const props: string[] = [
262
+ `${indent} path: ${JSON.stringify(page.urlPath)}`,
263
+ `${indent} component: ${comp}`,
264
+ `${indent} loader: ${mod}.loader`,
265
+ `${indent} beforeEnter: ${mod}.guard`,
266
+ `${indent} meta: ${mod}.meta`,
267
+ ]
268
+
269
+ if (errorName) {
270
+ props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`)
271
+ } else {
272
+ props.push(`${indent} errorComponent: ${mod}.error`)
273
+ }
274
+
275
+ return `${indent}{\n${props.join(',\n')}\n${indent}}`
276
+ }
277
+
278
+ function wrapWithLayout(
279
+ node: RouteNode,
280
+ children: string[],
281
+ indent: string,
282
+ errorName: string | undefined,
283
+ ): string {
284
+ const layout = node.layout as FileRoute
285
+ const layoutMod = nextModuleImport(layout.filePath)
286
+ const layoutComp = nextImport(layout.filePath, 'layout')
287
+
288
+ const props: string[] = [
289
+ `${indent}path: ${JSON.stringify(layout.urlPath)}`,
290
+ `${indent}component: ${layoutComp}`,
291
+ `${indent}loader: ${layoutMod}.loader`,
292
+ `${indent}beforeEnter: ${layoutMod}.guard`,
293
+ `${indent}meta: ${layoutMod}.meta`,
294
+ ]
295
+ if (errorName) {
296
+ props.push(`${indent}errorComponent: ${errorName}`)
297
+ }
298
+ if (children.length > 0) {
299
+ props.push(`${indent}children: [\n${children.join(',\n')}\n${indent}]`)
300
+ }
301
+
302
+ return `${indent}{\n${props.map((p) => ` ${p}`).join(',\n')}\n${indent}}`
303
+ }
304
+
305
+ /**
306
+ * Generate route definitions for a tree node.
307
+ */
308
+ function generateNode(node: RouteNode, depth: number): string[] {
309
+ const indent = ' '.repeat(depth + 1)
310
+
311
+ const errorName = node.error ? nextImport(node.error.filePath) : undefined
312
+ const loadingName = node.loading
313
+ ? nextImport(node.loading.filePath)
314
+ : undefined
315
+
316
+ const childRouteDefs: string[] = []
317
+ for (const [, childNode] of node.children) {
318
+ childRouteDefs.push(...generateNode(childNode, depth + 1))
319
+ }
320
+
321
+ const pageRouteDefs = node.pages.map((page) =>
322
+ generatePageRoute(page, indent, loadingName, errorName),
323
+ )
324
+
325
+ const allChildren = [...pageRouteDefs, ...childRouteDefs]
326
+
327
+ if (node.layout) {
328
+ return [wrapWithLayout(node, allChildren, indent, errorName)]
329
+ }
330
+ return allChildren
331
+ }
332
+
333
+ const routeDefs = generateNode(tree, 0)
334
+
335
+ return [
336
+ `import { lazy } from "@pyreon/router"`,
337
+ '',
338
+ ...imports,
339
+ '',
340
+ // Filter out undefined properties at runtime
341
+ `function clean(routes) {`,
342
+ ` return routes.map(r => {`,
343
+ ` const c = {}`,
344
+ ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,
345
+ ` if (c.children) c.children = clean(c.children)`,
346
+ ` return c`,
347
+ ` })`,
348
+ `}`,
349
+ '',
350
+ `export const routes = clean([`,
351
+ routeDefs.join(',\n'),
352
+ `])`,
353
+ ].join('\n')
354
+ }
355
+
356
+ /**
357
+ * Scan a directory for route files.
358
+ * Returns paths relative to the routes directory.
359
+ */
360
+ export async function scanRouteFiles(routesDir: string): Promise<string[]> {
361
+ const { readdir } = await import('node:fs/promises')
362
+ const { join, relative } = await import('node:path')
363
+
364
+ const files: string[] = []
365
+
366
+ async function walk(dir: string) {
367
+ const entries = await readdir(dir, { withFileTypes: true })
368
+ for (const entry of entries) {
369
+ const fullPath = join(dir, entry.name)
370
+ if (entry.isDirectory()) {
371
+ await walk(fullPath)
372
+ } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
373
+ files.push(relative(routesDir, fullPath))
374
+ }
375
+ }
376
+ }
377
+
378
+ await walk(routesDir)
379
+ return files
380
+ }