@pyreon/zero 0.11.8 → 0.11.9
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/fs-router-BkbIWqek.js.map +1 -1
- package/lib/{fs-router-n4VA4lxu.js → fs-router-Dil4IKZR.js} +23 -19
- package/lib/fs-router-Dil4IKZR.js.map +1 -0
- package/lib/index.js +872 -17
- package/lib/index.js.map +1 -1
- package/lib/link.js +12 -1
- package/lib/link.js.map +1 -1
- package/package.json +10 -10
- package/src/entry-server.ts +124 -76
- package/src/favicon.ts +380 -0
- package/src/fs-router.ts +54 -13
- package/src/i18n-routing.ts +299 -0
- package/src/index.ts +125 -76
- package/src/link.tsx +12 -0
- package/src/meta.tsx +210 -0
- package/src/middleware.ts +65 -0
- package/src/not-found.ts +44 -0
- package/src/types.ts +2 -0
- package/src/vite-plugin.ts +258 -127
- package/lib/fs-router-n4VA4lxu.js.map +0 -1
package/src/vite-plugin.ts
CHANGED
|
@@ -1,18 +1,43 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
1
3
|
import type { Plugin } from 'vite'
|
|
2
4
|
import { generateApiRouteModule } from './api-routes'
|
|
3
5
|
import { resolveConfig } from './config'
|
|
4
|
-
import { renderErrorOverlay } from './error-overlay'
|
|
5
|
-
import { generateMiddlewareModule, generateRouteModule, scanRouteFiles } from './fs-router'
|
|
6
|
-
import type { ZeroConfig } from './types'
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Scan node_modules/@pyreon/ to discover all installed Pyreon packages.
|
|
9
|
+
* Returns package names to exclude from Vite's dep optimizer.
|
|
10
|
+
*/
|
|
11
|
+
function scanPyreonPackages(root: string): string[] {
|
|
12
|
+
const pyreonDir = join(root, 'node_modules', '@pyreon')
|
|
13
|
+
if (!existsSync(pyreonDir)) return []
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return readdirSync(pyreonDir)
|
|
17
|
+
.filter((name) => !name.startsWith('.'))
|
|
18
|
+
.map((name) => `@pyreon/${name}`)
|
|
19
|
+
} catch {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
import { matchPattern } from "./entry-server";
|
|
24
|
+
import { renderErrorOverlay } from "./error-overlay";
|
|
25
|
+
import {
|
|
26
|
+
generateMiddlewareModule,
|
|
27
|
+
generateRouteModule,
|
|
28
|
+
scanRouteFiles,
|
|
29
|
+
} from "./fs-router";
|
|
30
|
+
import { render404Page } from "./not-found";
|
|
31
|
+
import type { ZeroConfig } from "./types";
|
|
10
32
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
33
|
+
const VIRTUAL_ROUTES_ID = "virtual:zero/routes";
|
|
34
|
+
const RESOLVED_VIRTUAL_ROUTES_ID = `\0${VIRTUAL_ROUTES_ID}`;
|
|
13
35
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
36
|
+
const VIRTUAL_MIDDLEWARE_ID = "virtual:zero/route-middleware";
|
|
37
|
+
const RESOLVED_VIRTUAL_MIDDLEWARE_ID = `\0${VIRTUAL_MIDDLEWARE_ID}`;
|
|
38
|
+
|
|
39
|
+
const VIRTUAL_API_ROUTES_ID = "virtual:zero/api-routes";
|
|
40
|
+
const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`;
|
|
16
41
|
|
|
17
42
|
/**
|
|
18
43
|
* Zero Vite plugin — adds file-based routing and zero-config conventions
|
|
@@ -28,123 +53,229 @@ const RESOLVED_VIRTUAL_API_ROUTES_ID = `\0${VIRTUAL_API_ROUTES_ID}`
|
|
|
28
53
|
* }
|
|
29
54
|
*/
|
|
30
55
|
export function zeroPlugin(userConfig: ZeroConfig = {}): Plugin {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
56
|
+
const config = resolveConfig(userConfig);
|
|
57
|
+
let routesDir: string;
|
|
58
|
+
let root: string;
|
|
59
|
+
|
|
60
|
+
const plugin: Plugin & { _zeroConfig: ZeroConfig } = {
|
|
61
|
+
name: "pyreon-zero",
|
|
62
|
+
enforce: "pre",
|
|
63
|
+
_zeroConfig: userConfig,
|
|
64
|
+
|
|
65
|
+
configResolved(resolvedConfig) {
|
|
66
|
+
root = resolvedConfig.root;
|
|
67
|
+
routesDir = `${root}/src/routes`;
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
resolveId(id) {
|
|
71
|
+
if (id === VIRTUAL_ROUTES_ID) return RESOLVED_VIRTUAL_ROUTES_ID;
|
|
72
|
+
if (id === VIRTUAL_MIDDLEWARE_ID) return RESOLVED_VIRTUAL_MIDDLEWARE_ID;
|
|
73
|
+
if (id === VIRTUAL_API_ROUTES_ID) return RESOLVED_VIRTUAL_API_ROUTES_ID;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async load(id) {
|
|
77
|
+
if (id === RESOLVED_VIRTUAL_ROUTES_ID) {
|
|
78
|
+
try {
|
|
79
|
+
const files = await scanRouteFiles(routesDir);
|
|
80
|
+
return generateRouteModule(files, routesDir, {
|
|
81
|
+
staticImports: config.mode === 'ssg',
|
|
82
|
+
});
|
|
83
|
+
} catch (_err) {
|
|
84
|
+
return `export const routes = []`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (id === RESOLVED_VIRTUAL_MIDDLEWARE_ID) {
|
|
89
|
+
try {
|
|
90
|
+
const files = await scanRouteFiles(routesDir);
|
|
91
|
+
return generateMiddlewareModule(files, routesDir);
|
|
92
|
+
} catch (_err) {
|
|
93
|
+
return `export const routeMiddleware = []`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (id === RESOLVED_VIRTUAL_API_ROUTES_ID) {
|
|
98
|
+
try {
|
|
99
|
+
const files = await scanRouteFiles(routesDir);
|
|
100
|
+
return generateApiRouteModule(files, routesDir);
|
|
101
|
+
} catch (_err) {
|
|
102
|
+
return `export const apiRoutes = []`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
configureServer(server) {
|
|
108
|
+
// 404 handler — check if the requested path matches any route.
|
|
109
|
+
// If not, render the nearest _404.tsx component with a 404 status.
|
|
110
|
+
// Uses a sync wrapper that calls the async handler, since Connect
|
|
111
|
+
// middleware does not natively support async functions.
|
|
112
|
+
server.middlewares.use((req, res, next) => {
|
|
113
|
+
const accept = req.headers.accept ?? "";
|
|
114
|
+
// Accept HTML requests and wildcard requests (fetch without explicit Accept header)
|
|
115
|
+
if (!accept.includes("text/html") && !accept.includes("*/*"))
|
|
116
|
+
return next();
|
|
117
|
+
|
|
118
|
+
const pathname = req.url?.split("?")[0] ?? "/";
|
|
119
|
+
|
|
120
|
+
// Skip static assets, Vite internal requests, and file-like paths (with extensions)
|
|
121
|
+
if (pathname.startsWith("/@") || pathname.startsWith("/__"))
|
|
122
|
+
return next();
|
|
123
|
+
if (/\.\w+$/.test(pathname)) return next();
|
|
124
|
+
|
|
125
|
+
handle404(server, routesDir, pathname, res).then(
|
|
126
|
+
(handled) => {
|
|
127
|
+
if (!handled) next();
|
|
128
|
+
},
|
|
129
|
+
() => next(), // On error, fall through to Vite's default handling
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// SSR error overlay — intercept HTML requests and catch SSR errors
|
|
134
|
+
// This runs as a late middleware (return function) so it wraps
|
|
135
|
+
// Vite's own SSR handling and catches rendering failures.
|
|
136
|
+
server.middlewares.use((req, res, next) => {
|
|
137
|
+
const accept = req.headers.accept ?? "";
|
|
138
|
+
if (!accept.includes("text/html")) return next();
|
|
139
|
+
|
|
140
|
+
const originalEnd = res.end.bind(res);
|
|
141
|
+
let errored = false;
|
|
142
|
+
|
|
143
|
+
const handleError = (err: unknown) => {
|
|
144
|
+
if (errored) return;
|
|
145
|
+
errored = true;
|
|
146
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
147
|
+
server.ssrFixStacktrace(error);
|
|
148
|
+
const html = renderErrorOverlay(error);
|
|
149
|
+
res.statusCode = 500;
|
|
150
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
151
|
+
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
152
|
+
originalEnd(html);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
res.on("error", handleError);
|
|
156
|
+
|
|
157
|
+
// Wrap next() in try/catch to handle both sync and async errors.
|
|
158
|
+
// Express-style middleware may throw synchronously or pass errors
|
|
159
|
+
// through next(err), and Vite's SSR pipeline may reject promises.
|
|
160
|
+
try {
|
|
161
|
+
const result = next() as unknown;
|
|
162
|
+
// Handle async errors from Vite's SSR pipeline
|
|
163
|
+
if (
|
|
164
|
+
result &&
|
|
165
|
+
typeof (result as Promise<unknown>).catch === "function"
|
|
166
|
+
) {
|
|
167
|
+
(result as Promise<unknown>).catch(handleError);
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
handleError(err);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Watch routes directory for changes
|
|
175
|
+
server.watcher.add(`${routesDir}/**/*.{tsx,jsx,ts,js}`);
|
|
176
|
+
|
|
177
|
+
// Invalidate virtual modules when route files change
|
|
178
|
+
server.watcher.on("all", (event, path) => {
|
|
179
|
+
if (
|
|
180
|
+
path.startsWith(routesDir) &&
|
|
181
|
+
(event === "add" || event === "unlink")
|
|
182
|
+
) {
|
|
183
|
+
for (const resolvedId of [
|
|
184
|
+
RESOLVED_VIRTUAL_ROUTES_ID,
|
|
185
|
+
RESOLVED_VIRTUAL_MIDDLEWARE_ID,
|
|
186
|
+
RESOLVED_VIRTUAL_API_ROUTES_ID,
|
|
187
|
+
]) {
|
|
188
|
+
const mod = server.moduleGraph.getModuleById(resolvedId);
|
|
189
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
190
|
+
}
|
|
191
|
+
server.ws.send({ type: "full-reload" });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
config(userConfig) {
|
|
197
|
+
// Discover all @pyreon/* packages installed in node_modules.
|
|
198
|
+
// The "bun" export condition points to TS source — esbuild's
|
|
199
|
+
// dep optimizer would compile them with the wrong JSX runtime.
|
|
200
|
+
const root = userConfig.root ?? process.cwd()
|
|
201
|
+
const pyreonExclude = scanPyreonPackages(root)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
resolve: {
|
|
205
|
+
conditions: ['bun'],
|
|
206
|
+
},
|
|
207
|
+
optimizeDeps: {
|
|
208
|
+
exclude: pyreonExclude,
|
|
209
|
+
},
|
|
210
|
+
server: {
|
|
211
|
+
port: config.port,
|
|
212
|
+
},
|
|
213
|
+
define: {
|
|
214
|
+
__ZERO_MODE__: JSON.stringify(config.mode),
|
|
215
|
+
__ZERO_BASE__: JSON.stringify(config.base),
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return plugin;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if the requested path matches any route. If not, render a 404 page.
|
|
226
|
+
* Returns true if the 404 was handled (response sent), false otherwise.
|
|
227
|
+
*
|
|
228
|
+
* In dev mode, the _404.tsx component cannot be SSR-rendered because
|
|
229
|
+
* the compiler emits _tpl() calls that require `document`. Instead,
|
|
230
|
+
* we return a static 404 page. The actual component rendering happens
|
|
231
|
+
* on the client side when the SPA loads.
|
|
232
|
+
*/
|
|
233
|
+
async function handle404(
|
|
234
|
+
server: import("vite").ViteDevServer,
|
|
235
|
+
_routesDir: string,
|
|
236
|
+
pathname: string,
|
|
237
|
+
res: import("http").ServerResponse,
|
|
238
|
+
): Promise<boolean> {
|
|
239
|
+
const mod = await server.ssrLoadModule(VIRTUAL_ROUTES_ID);
|
|
240
|
+
const routes = mod.routes as Array<{ path?: string; children?: unknown[] }>;
|
|
241
|
+
const patterns = flattenRoutePatterns(routes);
|
|
242
|
+
|
|
243
|
+
if (patterns.some((pattern) => matchPattern(pattern, pathname))) {
|
|
244
|
+
return false; // Route matches — not a 404
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// No route matched — return a 404.
|
|
248
|
+
// In dev, we return a static page since the compiler emits _tpl() calls
|
|
249
|
+
// that require document (unavailable in SSR). The _404.tsx component
|
|
250
|
+
// renders on the client side after hydration.
|
|
251
|
+
const html = await render404Page(undefined);
|
|
252
|
+
|
|
253
|
+
res.statusCode = 404;
|
|
254
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
255
|
+
res.setHeader("Content-Length", Buffer.byteLength(html));
|
|
256
|
+
res.end(html);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
148
259
|
|
|
149
|
-
|
|
260
|
+
/** Extract all URL patterns from a nested route tree. */
|
|
261
|
+
function flattenRoutePatterns(
|
|
262
|
+
routes: Array<{ path?: string; children?: unknown[] }>,
|
|
263
|
+
prefix = "",
|
|
264
|
+
): string[] {
|
|
265
|
+
const patterns: string[] = [];
|
|
266
|
+
for (const route of routes) {
|
|
267
|
+
if (!route.path) continue;
|
|
268
|
+
const fullPath =
|
|
269
|
+
route.path === "/" && prefix ? prefix : `${prefix}${route.path}`;
|
|
270
|
+
patterns.push(fullPath);
|
|
271
|
+
if (route.children) {
|
|
272
|
+
patterns.push(
|
|
273
|
+
...flattenRoutePatterns(
|
|
274
|
+
route.children as Array<{ path?: string; children?: unknown[] }>,
|
|
275
|
+
fullPath,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return patterns;
|
|
150
281
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fs-router-n4VA4lxu.js","names":[],"sources":["../src/fs-router.ts"],"sourcesContent":["import type { FileRoute, RenderMode } from './types'\n\n// ─── File-system route conventions ──────────────────────────────────────────\n//\n// src/routes/\n// _layout.tsx → layout for all routes\n// index.tsx → /\n// about.tsx → /about\n// users/\n// _layout.tsx → layout for /users/*\n// _loading.tsx → loading fallback for /users/*\n// _error.tsx → error boundary for /users/*\n// index.tsx → /users\n// [id].tsx → /users/:id\n// [id]/\n// settings.tsx → /users/:id/settings\n// blog/\n// [...slug].tsx → /blog/* (catch-all)\n//\n// Conventions:\n// [param] → dynamic segment → :param\n// [...param] → catch-all → :param*\n// _layout → layout wrapper (not a route itself)\n// _error → error component\n// _loading → loading component\n// (group) → route group (directory ignored in URL)\n\nconst ROUTE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js']\n\n/**\n * Parse a set of file paths (relative to routes dir) into FileRoute objects.\n *\n * @param files Array of file paths like [\"index.tsx\", \"users/[id].tsx\"]\n * @param defaultMode Default rendering mode from config\n */\nexport function parseFileRoutes(files: string[], defaultMode: RenderMode = 'ssr'): FileRoute[] {\n return files\n .filter((f) => ROUTE_EXTENSIONS.some((ext) => f.endsWith(ext)))\n .map((filePath) => parseFilePath(filePath, defaultMode))\n .sort(sortRoutes)\n}\n\nfunction parseFilePath(filePath: string, defaultMode: RenderMode): FileRoute {\n // Remove extension\n let route = filePath\n for (const ext of ROUTE_EXTENSIONS) {\n if (route.endsWith(ext)) {\n route = route.slice(0, -ext.length)\n break\n }\n }\n\n const fileName = getFileName(route)\n const isLayout = fileName === '_layout'\n const isError = fileName === '_error'\n const isLoading = fileName === '_loading'\n const isCatchAll = route.includes('[...')\n\n // Get directory path (strip groups for consistent grouping)\n const parts = route.split('/')\n parts.pop() // remove filename\n const dirPath = parts.filter((s) => !(s.startsWith('(') && s.endsWith(')'))).join('/')\n\n // Convert file path to URL pattern\n const urlPath = filePathToUrlPath(route)\n const depth = urlPath === '/' ? 0 : urlPath.split('/').filter(Boolean).length\n\n return {\n filePath,\n urlPath,\n dirPath,\n depth,\n isLayout,\n isError,\n isLoading,\n isCatchAll,\n renderMode: defaultMode,\n }\n}\n\n/**\n * Convert a file path (without extension) to a URL path pattern.\n *\n * Examples:\n * \"index\" → \"/\"\n * \"about\" → \"/about\"\n * \"users/index\" → \"/users\"\n * \"users/[id]\" → \"/users/:id\"\n * \"blog/[...slug]\" → \"/blog/:slug*\"\n * \"(auth)/login\" → \"/login\" (group stripped)\n * \"_layout\" → \"/\" (layout marker)\n */\nexport function filePathToUrlPath(filePath: string): string {\n const segments = filePath.split('/')\n const urlSegments: string[] = []\n\n for (const seg of segments) {\n // Skip route groups \"(name)\"\n if (seg.startsWith('(') && seg.endsWith(')')) continue\n\n // Skip special files\n if (seg === '_layout' || seg === '_error' || seg === '_loading') continue\n\n // \"index\" maps to the parent path\n if (seg === 'index') continue\n\n // Catch-all: [...param] → :param*\n const catchAll = seg.match(/^\\[\\.\\.\\.(\\w+)\\]$/)\n if (catchAll) {\n urlSegments.push(`:${catchAll[1]}*`)\n continue\n }\n\n // Dynamic: [param] → :param\n const dynamic = seg.match(/^\\[(\\w+)\\]$/)\n if (dynamic) {\n urlSegments.push(`:${dynamic[1]}`)\n continue\n }\n\n urlSegments.push(seg)\n }\n\n const path = `/${urlSegments.join('/')}`\n return path || '/'\n}\n\n/** Sort routes: static before dynamic, catch-all last. */\nfunction sortRoutes(a: FileRoute, b: FileRoute): number {\n // Catch-all routes go last\n if (a.isCatchAll !== b.isCatchAll) return a.isCatchAll ? 1 : -1\n // Layouts go first within same depth\n if (a.isLayout !== b.isLayout) return a.isLayout ? -1 : 1\n // Static segments before dynamic\n const aDynamic = a.urlPath.includes(':')\n const bDynamic = b.urlPath.includes(':')\n if (aDynamic !== bDynamic) return aDynamic ? 1 : -1\n // Alphabetical\n return a.urlPath.localeCompare(b.urlPath)\n}\n\nfunction getFileName(filePath: string): string {\n const parts = filePath.split('/')\n return parts[parts.length - 1] ?? ''\n}\n\n// ─── Route generation (for Vite plugin) ─────────────────────────────────────\n\n/** Internal tree node for building nested route structures. */\ninterface RouteNode {\n /** Page routes at this directory level. */\n pages: FileRoute[]\n /** Layout file for this directory (if any). */\n layout?: FileRoute\n /** Error boundary file (if any). */\n error?: FileRoute\n /** Loading fallback file (if any). */\n loading?: FileRoute\n /** Child directories. */\n children: Map<string, RouteNode>\n}\n\n/**\n * Group flat file routes into a directory tree.\n */\nfunction getOrCreateChild(node: RouteNode, segment: string): RouteNode {\n let child = node.children.get(segment)\n if (!child) {\n child = { pages: [], children: new Map() }\n node.children.set(segment, child)\n }\n return child\n}\n\nfunction resolveNode(root: RouteNode, dirPath: string): RouteNode {\n let node = root\n if (dirPath) {\n for (const segment of dirPath.split('/')) {\n node = getOrCreateChild(node, segment)\n }\n }\n return node\n}\n\nfunction placeRoute(node: RouteNode, route: FileRoute) {\n if (route.isLayout) node.layout = route\n else if (route.isError) node.error = route\n else if (route.isLoading) node.loading = route\n else node.pages.push(route)\n}\n\nfunction buildRouteTree(routes: FileRoute[]): RouteNode {\n const root: RouteNode = { pages: [], children: new Map() }\n for (const route of routes) {\n placeRoute(resolveNode(root, route.dirPath), route)\n }\n return root\n}\n\n/**\n * Generate a virtual module that exports a nested route tree.\n * Wires up layouts as parent routes with children, loaders, guards,\n * error/loading components, middleware, and meta from route module exports.\n */\nexport function generateRouteModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const tree = buildRouteTree(routes)\n const imports: string[] = []\n let importCounter = 0\n\n function nextImport(filePath: string, exportName = 'default'): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n if (exportName === 'default') {\n imports.push(`import ${name} from \"${fullPath}\"`)\n } else {\n imports.push(`import { ${exportName} as ${name} } from \"${fullPath}\"`)\n }\n return name\n }\n\n function nextLazy(filePath: string, loadingName?: string, errorName?: string): string {\n const name = `_${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n const opts: string[] = []\n if (loadingName) opts.push(`loading: ${loadingName}`)\n if (errorName) opts.push(`error: ${errorName}`)\n const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : ''\n imports.push(`const ${name} = lazy(() => import(\"${fullPath}\")${optsStr})`)\n return name\n }\n\n function nextModuleImport(filePath: string): string {\n const name = `_m${importCounter++}`\n const fullPath = `${routesDir}/${filePath}`\n imports.push(`import * as ${name} from \"${fullPath}\"`)\n return name\n }\n\n function generatePageRoute(\n page: FileRoute,\n indent: string,\n loadingName: string | undefined,\n errorName: string | undefined,\n ): string {\n const mod = nextModuleImport(page.filePath)\n const comp = nextLazy(page.filePath, loadingName, errorName)\n\n const props: string[] = [\n `${indent} path: ${JSON.stringify(page.urlPath)}`,\n `${indent} component: ${comp}`,\n `${indent} loader: ${mod}.loader`,\n `${indent} beforeEnter: ${mod}.guard`,\n `${indent} meta: { ...${mod}.meta, renderMode: ${mod}.renderMode }`,\n ]\n\n if (errorName) {\n props.push(`${indent} errorComponent: ${mod}.error || ${errorName}`)\n } else {\n props.push(`${indent} errorComponent: ${mod}.error`)\n }\n\n return `${indent}{\\n${props.join(',\\n')}\\n${indent}}`\n }\n\n function wrapWithLayout(\n node: RouteNode,\n children: string[],\n indent: string,\n errorName: string | undefined,\n ): string {\n const layout = node.layout as FileRoute\n const layoutMod = nextModuleImport(layout.filePath)\n const layoutComp = nextImport(layout.filePath, 'layout')\n\n const props: string[] = [\n `${indent}path: ${JSON.stringify(layout.urlPath)}`,\n `${indent}component: ${layoutComp}`,\n `${indent}loader: ${layoutMod}.loader`,\n `${indent}beforeEnter: ${layoutMod}.guard`,\n `${indent}meta: { ...${layoutMod}.meta, renderMode: ${layoutMod}.renderMode }`,\n ]\n if (errorName) {\n props.push(`${indent}errorComponent: ${errorName}`)\n }\n if (children.length > 0) {\n props.push(`${indent}children: [\\n${children.join(',\\n')}\\n${indent}]`)\n }\n\n return `${indent}{\\n${props.map((p) => ` ${p}`).join(',\\n')}\\n${indent}}`\n }\n\n /**\n * Generate route definitions for a tree node.\n */\n function generateNode(node: RouteNode, depth: number): string[] {\n const indent = ' '.repeat(depth + 1)\n\n const errorName = node.error ? nextImport(node.error.filePath) : undefined\n const loadingName = node.loading ? nextImport(node.loading.filePath) : undefined\n\n const childRouteDefs: string[] = []\n for (const [, childNode] of node.children) {\n childRouteDefs.push(...generateNode(childNode, depth + 1))\n }\n\n const pageRouteDefs = node.pages.map((page) =>\n generatePageRoute(page, indent, loadingName, errorName),\n )\n\n const allChildren = [...pageRouteDefs, ...childRouteDefs]\n\n if (node.layout) {\n return [wrapWithLayout(node, allChildren, indent, errorName)]\n }\n return allChildren\n }\n\n const routeDefs = generateNode(tree, 0)\n\n return [\n `import { lazy } from \"@pyreon/router\"`,\n '',\n ...imports,\n '',\n // Filter out undefined properties at runtime\n `function clean(routes) {`,\n ` return routes.map(r => {`,\n ` const c = {}`,\n ` for (const k in r) if (r[k] !== undefined) c[k] = r[k]`,\n ` if (c.children) c.children = clean(c.children)`,\n ` return c`,\n ` })`,\n `}`,\n '',\n `export const routes = clean([`,\n routeDefs.join(',\\n'),\n `])`,\n ].join('\\n')\n}\n\n/**\n * Generate a virtual module that maps URL patterns to their middleware exports.\n * Used by the server entry to dispatch per-route middleware.\n */\nexport function generateMiddlewareModule(files: string[], routesDir: string): string {\n const routes = parseFileRoutes(files)\n const imports: string[] = []\n const entries: string[] = []\n let counter = 0\n\n for (const route of routes) {\n if (route.isLayout || route.isError || route.isLoading) continue\n const name = `_mw${counter++}`\n const fullPath = `${routesDir}/${route.filePath}`\n imports.push(`import { middleware as ${name} } from \"${fullPath}\"`)\n entries.push(` { pattern: ${JSON.stringify(route.urlPath)}, middleware: ${name} }`)\n }\n\n return [\n ...imports,\n '',\n `export const routeMiddleware = [`,\n entries.join(',\\n'),\n `].filter(e => e.middleware)`,\n ].join('\\n')\n}\n\n/**\n * Scan a directory for route files.\n * Returns paths relative to the routes directory.\n */\nexport async function scanRouteFiles(routesDir: string): Promise<string[]> {\n const { readdir } = await import('node:fs/promises')\n const { join, relative } = await import('node:path')\n\n const files: string[] = []\n\n async function walk(dir: string) {\n const entries = await readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = join(dir, entry.name)\n if (entry.isDirectory()) {\n await walk(fullPath)\n } else if (ROUTE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {\n files.push(relative(routesDir, fullPath))\n }\n }\n }\n\n await walk(routesDir)\n return files\n}\n"],"mappings":"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"}
|