@emkodev/emroute 1.6.6-beta.1 → 1.6.6-beta.2

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 (78) hide show
  1. package/dist/runtime/abstract.runtime.d.ts +6 -4
  2. package/dist/runtime/abstract.runtime.js +42 -121
  3. package/dist/runtime/abstract.runtime.js.map +1 -1
  4. package/dist/runtime/bun/fs/bun-fs.runtime.js +0 -1
  5. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
  6. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +0 -1
  7. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
  8. package/dist/runtime/universal/fs/universal-fs.runtime.js +0 -1
  9. package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
  10. package/dist/server/codegen.util.d.ts +1 -1
  11. package/dist/server/codegen.util.js +6 -4
  12. package/dist/server/codegen.util.js.map +1 -1
  13. package/dist/server/emroute.server.js +54 -47
  14. package/dist/server/emroute.server.js.map +1 -1
  15. package/dist/server/esbuild-manifest.plugin.d.ts +0 -2
  16. package/dist/server/esbuild-manifest.plugin.js +67 -87
  17. package/dist/server/esbuild-manifest.plugin.js.map +1 -1
  18. package/dist/server/server-api.type.d.ts +5 -5
  19. package/dist/src/index.d.ts +5 -2
  20. package/dist/src/index.js +2 -1
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/renderer/spa/base.renderer.js +4 -4
  23. package/dist/src/renderer/spa/base.renderer.js.map +1 -1
  24. package/dist/src/renderer/spa/hash.renderer.d.ts +3 -2
  25. package/dist/src/renderer/spa/hash.renderer.js +36 -17
  26. package/dist/src/renderer/spa/hash.renderer.js.map +1 -1
  27. package/dist/src/renderer/spa/html.renderer.d.ts +11 -3
  28. package/dist/src/renderer/spa/html.renderer.js +24 -11
  29. package/dist/src/renderer/spa/html.renderer.js.map +1 -1
  30. package/dist/src/renderer/spa/mod.d.ts +4 -1
  31. package/dist/src/renderer/spa/mod.js +1 -0
  32. package/dist/src/renderer/spa/mod.js.map +1 -1
  33. package/dist/src/renderer/ssr/html.renderer.d.ts +4 -3
  34. package/dist/src/renderer/ssr/html.renderer.js +4 -4
  35. package/dist/src/renderer/ssr/html.renderer.js.map +1 -1
  36. package/dist/src/renderer/ssr/md.renderer.d.ts +4 -3
  37. package/dist/src/renderer/ssr/md.renderer.js +4 -4
  38. package/dist/src/renderer/ssr/md.renderer.js.map +1 -1
  39. package/dist/src/renderer/ssr/ssr.renderer.d.ts +3 -2
  40. package/dist/src/renderer/ssr/ssr.renderer.js +10 -15
  41. package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -1
  42. package/dist/src/route/route-tree.util.d.ts +15 -0
  43. package/dist/src/route/route-tree.util.js +32 -0
  44. package/dist/src/route/route-tree.util.js.map +1 -0
  45. package/dist/src/route/route.core.d.ts +31 -25
  46. package/dist/src/route/route.core.js +82 -60
  47. package/dist/src/route/route.core.js.map +1 -1
  48. package/dist/src/route/route.resolver.d.ts +28 -0
  49. package/dist/src/route/route.resolver.js +11 -0
  50. package/dist/src/route/route.resolver.js.map +1 -0
  51. package/dist/src/route/route.trie.d.ts +38 -0
  52. package/dist/src/route/route.trie.js +206 -0
  53. package/dist/src/route/route.trie.js.map +1 -0
  54. package/dist/src/type/route-tree.type.d.ts +42 -0
  55. package/dist/src/type/route-tree.type.js +12 -0
  56. package/dist/src/type/route-tree.type.js.map +1 -0
  57. package/package.json +1 -1
  58. package/runtime/abstract.runtime.ts +46 -146
  59. package/runtime/bun/fs/bun-fs.runtime.ts +0 -1
  60. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +0 -1
  61. package/runtime/universal/fs/universal-fs.runtime.ts +0 -1
  62. package/server/codegen.util.ts +8 -4
  63. package/server/emroute.server.ts +50 -48
  64. package/server/esbuild-manifest.plugin.ts +68 -104
  65. package/server/server-api.type.ts +5 -5
  66. package/src/index.ts +5 -2
  67. package/src/renderer/spa/base.renderer.ts +4 -4
  68. package/src/renderer/spa/hash.renderer.ts +43 -20
  69. package/src/renderer/spa/html.renderer.ts +29 -12
  70. package/src/renderer/spa/mod.ts +3 -1
  71. package/src/renderer/ssr/html.renderer.ts +6 -5
  72. package/src/renderer/ssr/md.renderer.ts +6 -5
  73. package/src/renderer/ssr/ssr.renderer.ts +11 -17
  74. package/src/route/route-tree.util.ts +35 -0
  75. package/src/route/route.core.ts +89 -64
  76. package/src/route/route.resolver.ts +33 -0
  77. package/src/route/route.trie.ts +265 -0
  78. package/src/type/route-tree.type.ts +49 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Route Trie
3
+ *
4
+ * Segment-based trie implementing RouteResolver for O(depth) route matching.
5
+ *
6
+ * Each URL segment maps to a trie node. Nodes are tried in order:
7
+ * static → dynamic (:param) → wildcard (:rest*). Backtracking handles
8
+ * cases where a dynamic path leads to a dead end but a wildcard at an
9
+ * ancestor would match.
10
+ *
11
+ * Static segment matching is case-sensitive, per RFC 3986.
12
+ *
13
+ * Accepts a RouteNode tree (the JSON-serializable manifest from Runtime)
14
+ * and converts it to an internal trie with Map-based static children for
15
+ * O(1) segment lookup.
16
+ */
17
+ function createNode() {
18
+ return { static: new Map() };
19
+ }
20
+ /** Try decodeURIComponent, return the original segment on malformed input. */
21
+ function safeDecode(segment) {
22
+ try {
23
+ return decodeURIComponent(segment);
24
+ }
25
+ catch {
26
+ return segment;
27
+ }
28
+ }
29
+ /**
30
+ * Split a normalized pathname into segments.
31
+ * Assumes leading slash, no trailing slash. Must NOT be called with '/'.
32
+ */
33
+ function splitSegments(pathname) {
34
+ return pathname.substring(1).split('/');
35
+ }
36
+ /**
37
+ * Convert a RouteNode tree into a TrieNode tree.
38
+ * Recursively walks the RouteNode, converting Record children to Map
39
+ * and reconstructing URL patterns at each node.
40
+ */
41
+ function convertNode(source, pattern) {
42
+ const node = createNode();
43
+ // Terminal route (has files or redirect)
44
+ if (source.files || source.redirect) {
45
+ node.route = source;
46
+ node.pattern = pattern;
47
+ }
48
+ // Error boundary
49
+ if (source.errorBoundary) {
50
+ node.errorBoundary = source.errorBoundary;
51
+ }
52
+ // Static children
53
+ if (source.children) {
54
+ for (const [segment, child] of Object.entries(source.children)) {
55
+ const childPattern = pattern === '/' ? `/${segment}` : `${pattern}/${segment}`;
56
+ node.static.set(segment, convertNode(child, childPattern));
57
+ }
58
+ }
59
+ // Dynamic child
60
+ if (source.dynamic) {
61
+ const { param, child } = source.dynamic;
62
+ const childPattern = pattern === '/' ? `/:${param}` : `${pattern}/:${param}`;
63
+ node.dynamic = { param, node: convertNode(child, childPattern) };
64
+ }
65
+ // Wildcard child
66
+ if (source.wildcard) {
67
+ const { param, child } = source.wildcard;
68
+ const childPattern = pattern === '/' ? `/:${param}*` : `${pattern}/:${param}*`;
69
+ node.wildcard = { param, node: convertNode(child, childPattern) };
70
+ }
71
+ return node;
72
+ }
73
+ /**
74
+ * Trie-based route resolver.
75
+ *
76
+ * Implements RouteResolver with O(depth) matching by walking the trie.
77
+ * Constructed from a RouteNode tree (produced by Runtime.scanRoutes).
78
+ */
79
+ export class RouteTrie {
80
+ root;
81
+ constructor(tree) {
82
+ this.root = convertNode(tree, '/');
83
+ }
84
+ match(pathname) {
85
+ // Normalize: strip trailing slash (except bare '/')
86
+ if (pathname.length > 1 && pathname.endsWith('/')) {
87
+ pathname = pathname.slice(0, -1);
88
+ }
89
+ if (!pathname.startsWith('/')) {
90
+ pathname = '/' + pathname;
91
+ }
92
+ if (pathname === '/') {
93
+ if (this.root.route) {
94
+ return { node: this.root.route, pattern: '/', params: {} };
95
+ }
96
+ return undefined;
97
+ }
98
+ const segments = splitSegments(pathname);
99
+ return this.walk(this.root, segments, 0, {});
100
+ }
101
+ findErrorBoundary(pathname) {
102
+ if (pathname.length > 1 && pathname.endsWith('/')) {
103
+ pathname = pathname.slice(0, -1);
104
+ }
105
+ if (!pathname.startsWith('/')) {
106
+ pathname = '/' + pathname;
107
+ }
108
+ if (pathname === '/')
109
+ return this.root.errorBoundary;
110
+ const segments = splitSegments(pathname);
111
+ return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
112
+ }
113
+ findRoute(pattern) {
114
+ if (pattern === '/') {
115
+ return this.root.route;
116
+ }
117
+ const segments = splitSegments(pattern);
118
+ let node = this.root;
119
+ for (const segment of segments) {
120
+ let child;
121
+ if (segment.startsWith(':') && segment.endsWith('*')) {
122
+ child = node.wildcard?.node;
123
+ }
124
+ else if (segment.startsWith(':')) {
125
+ child = node.dynamic?.node;
126
+ }
127
+ else {
128
+ child = node.static.get(segment);
129
+ }
130
+ if (!child)
131
+ return undefined;
132
+ node = child;
133
+ }
134
+ return node.route;
135
+ }
136
+ // ── Private matching ──────────────────────────────────────────────────
137
+ walk(node, segments, index, params) {
138
+ if (index === segments.length) {
139
+ if (node.route) {
140
+ return { node: node.route, pattern: node.pattern, params: { ...params } };
141
+ }
142
+ if (node.wildcard?.node.route) {
143
+ return {
144
+ node: node.wildcard.node.route,
145
+ pattern: node.wildcard.node.pattern,
146
+ params: { ...params, [node.wildcard.param]: '' },
147
+ };
148
+ }
149
+ return undefined;
150
+ }
151
+ const segment = segments[index];
152
+ // 1. Try static child
153
+ const staticChild = node.static.get(segment);
154
+ if (staticChild) {
155
+ const result = this.walk(staticChild, segments, index + 1, params);
156
+ if (result)
157
+ return result;
158
+ }
159
+ // 2. Try dynamic child (single segment)
160
+ if (node.dynamic) {
161
+ const { param, node: dynamicNode } = node.dynamic;
162
+ params[param] = safeDecode(segment);
163
+ const result = this.walk(dynamicNode, segments, index + 1, params);
164
+ if (result)
165
+ return result;
166
+ delete params[param];
167
+ }
168
+ // 3. Try wildcard (consumes all remaining segments)
169
+ if (node.wildcard?.node.route) {
170
+ const { param, node: wildcardNode } = node.wildcard;
171
+ let rest = safeDecode(segments[index]);
172
+ for (let i = index + 1; i < segments.length; i++) {
173
+ rest += '/' + safeDecode(segments[i]);
174
+ }
175
+ return {
176
+ node: wildcardNode.route,
177
+ pattern: wildcardNode.pattern,
178
+ params: { ...params, [param]: rest },
179
+ };
180
+ }
181
+ return undefined;
182
+ }
183
+ /**
184
+ * Walk for error boundary. Follows the same priority as match
185
+ * (static → dynamic → wildcard) without backtracking across branches.
186
+ * Returns the deepest error boundary module path found along the path.
187
+ */
188
+ walkForBoundary(node, segments, index, deepest) {
189
+ if (index === segments.length) {
190
+ return node.errorBoundary ?? deepest;
191
+ }
192
+ const segment = segments[index];
193
+ const staticChild = node.static.get(segment);
194
+ if (staticChild) {
195
+ return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
196
+ }
197
+ if (node.dynamic) {
198
+ return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
199
+ }
200
+ if (node.wildcard) {
201
+ return node.wildcard.node.errorBoundary ?? deepest;
202
+ }
203
+ return deepest;
204
+ }
205
+ }
206
+ //# sourceMappingURL=route.trie.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.trie.js","sourceRoot":"","sources":["../../../src/route/route.trie.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Route Tree
3
+ *
4
+ * Serializable tree structure that mirrors the filesystem layout.
5
+ * Replaces the flat array manifest as the canonical route format.
6
+ *
7
+ * Each node corresponds to a URL segment. The tree is JSON-serializable
8
+ * (no Maps, no classes) so it can be written to disk, sent over the wire,
9
+ * or used directly as the in-memory trie for O(depth) route matching.
10
+ */
11
+ /** Files associated with a route (companion files discovered alongside the page). */
12
+ export interface RouteFiles {
13
+ /** TypeScript module (e.g. "routes/about.page.ts") */
14
+ ts?: string;
15
+ /** HTML template (e.g. "routes/about.page.html") */
16
+ html?: string;
17
+ /** Markdown content (e.g. "routes/about.page.md") */
18
+ md?: string;
19
+ /** Scoped stylesheet (e.g. "routes/about.page.css") */
20
+ css?: string;
21
+ }
22
+ /** A single node in the route tree. */
23
+ export interface RouteNode {
24
+ /** Route files when this node is a terminal route. */
25
+ files?: RouteFiles;
26
+ /** Error boundary module path scoped to this prefix (from .error.ts). */
27
+ errorBoundary?: string;
28
+ /** Redirect module path (from .redirect.ts). Mutually exclusive with files. */
29
+ redirect?: string;
30
+ /** Static children keyed by URL segment (e.g. "about", "projects"). */
31
+ children?: Record<string, RouteNode>;
32
+ /** Single-segment dynamic param (from [param] directories/files). */
33
+ dynamic?: {
34
+ param: string;
35
+ child: RouteNode;
36
+ };
37
+ /** Catch-all wildcard (from directory index.page.* files). */
38
+ wildcard?: {
39
+ param: string;
40
+ child: RouteNode;
41
+ };
42
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Route Tree
3
+ *
4
+ * Serializable tree structure that mirrors the filesystem layout.
5
+ * Replaces the flat array manifest as the canonical route format.
6
+ *
7
+ * Each node corresponds to a URL segment. The tree is JSON-serializable
8
+ * (no Maps, no classes) so it can be written to disk, sent over the wire,
9
+ * or used directly as the in-memory trie for O(depth) route matching.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=route-tree.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-tree.type.js","sourceRoot":"","sources":["../../../src/type/route-tree.type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emkodev/emroute",
3
- "version": "1.6.6-beta.1",
3
+ "version": "1.6.6-beta.2",
4
4
  "description": "File-based (but storage-agnostic) router with triple rendering (SPA, SSR HTML, SSR Markdown). Zero dependencies.",
5
5
  "license": "BSD-3-Clause",
6
6
  "author": "emko.dev",
@@ -1,15 +1,5 @@
1
- import {
2
- filePathToPattern,
3
- getPageFileType,
4
- getRouteType,
5
- sortRoutesBySpecificity,
6
- } from '../src/route/route.matcher.ts';
7
- import type {
8
- ErrorBoundary,
9
- RouteConfig,
10
- RouteFiles,
11
- RoutesManifest,
12
- } from '../src/type/route.type.ts';
1
+ import type { RouteNode, RouteFiles } from '../src/type/route-tree.type.ts';
2
+ import { resolveTargetNode } from '../src/route/route-tree.util.ts';
13
3
  import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
14
4
 
15
5
  export const CONTENT_TYPES: Map<string, string> = new Map<string, string>([
@@ -198,17 +188,9 @@ export abstract class Runtime {
198
188
  return new Response('Not Found', { status: 404 });
199
189
  }
200
190
 
201
- const { warnings, ...manifest } = await this.scanRoutes(routesDir);
202
- for (const w of warnings) console.warn(w);
191
+ const tree = await this.scanRoutes(routesDir);
203
192
 
204
- const json = {
205
- routes: manifest.routes,
206
- errorBoundaries: manifest.errorBoundaries,
207
- statusPages: [...manifest.statusPages.entries()],
208
- errorHandler: manifest.errorHandler,
209
- };
210
-
211
- this.routesManifestCache = Response.json(json);
193
+ this.routesManifestCache = Response.json(tree);
212
194
  return this.routesManifestCache.clone();
213
195
  }
214
196
 
@@ -248,16 +230,12 @@ export abstract class Runtime {
248
230
  }
249
231
  }
250
232
 
251
- protected async scanRoutes(routesDir: string): Promise<RoutesManifest & { warnings: string[] }> {
252
- const pageFiles: Array<{
253
- path: string;
254
- pattern: string;
255
- fileType: 'ts' | 'html' | 'md' | 'css';
256
- }> = [];
257
- const redirects: RouteConfig[] = [];
258
- const errorBoundaries: ErrorBoundary[] = [];
259
- const statusPages = new Map<number, RouteConfig>();
260
- let errorHandler: RouteConfig | undefined;
233
+ /**
234
+ * Scan a routes directory and build a RouteNode tree.
235
+ * The filesystem structure maps directly to the tree — no intermediate array.
236
+ */
237
+ protected async scanRoutes(routesDir: string): Promise<RouteNode> {
238
+ const root: RouteNode = {};
261
239
 
262
240
  const allFiles: string[] = [];
263
241
  for await (const file of this.walkDirectory(routesDir)) {
@@ -266,130 +244,52 @@ export abstract class Runtime {
266
244
 
267
245
  for (const filePath of allFiles) {
268
246
  const relativePath = filePath.replace(`${routesDir}/`, '');
269
- const filename = relativePath.split('/').pop() ?? '';
270
-
271
- if (filename === 'index.error.ts' && relativePath === 'index.error.ts') {
272
- errorHandler = {
273
- pattern: '/',
274
- type: 'error',
275
- modulePath: filePath,
276
- };
277
- continue;
278
- }
279
-
280
- const cssFileType = getPageFileType(filename);
281
- if (cssFileType === 'css') {
282
- const pattern = filePathToPattern(relativePath);
283
- pageFiles.push({ path: filePath, pattern, fileType: 'css' });
284
- continue;
285
- }
286
-
287
- const routeType = getRouteType(filename);
288
- if (!routeType) continue;
289
-
290
- const statusMatch = filename.match(/^(\d{3})\.page\.(ts|html|md)$/);
291
- if (statusMatch) {
292
- const statusCode = parseInt(statusMatch[1], 10);
293
- const fileType = getPageFileType(filename);
294
- if (fileType) {
295
- const existing = statusPages.get(statusCode);
296
- if (existing) {
297
- existing.files ??= {};
298
- existing.files[fileType] = filePath;
299
- existing.modulePath = existing.files.ts ?? existing.files.html ?? existing.files.md ??
300
- '';
301
- } else {
302
- const files: RouteFiles = { [fileType]: filePath };
303
- statusPages.set(statusCode, {
304
- pattern: `/${statusCode}`,
305
- type: 'page',
306
- modulePath: filePath,
307
- statusCode,
308
- files,
309
- });
310
- }
247
+ const parts = relativePath.split('/');
248
+ const filename = parts[parts.length - 1];
249
+ const dirSegments = parts.slice(0, -1);
250
+
251
+ // Parse filename: name.kind.ext (e.g. "about.page.ts", "[id].page.html", "index.error.ts")
252
+ const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|html|md|css)$/);
253
+ if (!match) continue;
254
+
255
+ const [, name, kind, ext] = match;
256
+
257
+ // Walk directory segments to reach the parent node
258
+ let node = root;
259
+ for (const dir of dirSegments) {
260
+ if (dir.startsWith('[') && dir.endsWith(']')) {
261
+ const param = dir.slice(1, -1);
262
+ node.dynamic ??= { param, child: {} };
263
+ node = node.dynamic.child;
264
+ } else {
265
+ node.children ??= {};
266
+ node.children[dir] ??= {};
267
+ node = node.children[dir];
311
268
  }
312
- continue;
313
269
  }
314
270
 
315
- const pattern = filePathToPattern(relativePath);
316
-
317
- if (routeType === 'error') {
318
- const boundaryPattern = pattern.replace(/\/[^/]+$/, '') || '/';
319
- errorBoundaries.push({ pattern: boundaryPattern, modulePath: filePath });
271
+ // Place the file on the correct node
272
+ if (kind === 'error') {
273
+ // Error boundary scopes to the directory it's in.
274
+ // Root index.error.ts → root.errorBoundary (global handler).
275
+ // projects/projects.error.ts projects node errorBoundary.
276
+ node.errorBoundary = filePath;
320
277
  continue;
321
278
  }
322
279
 
323
- if (routeType === 'redirect') {
324
- redirects.push({ pattern, type: 'redirect', modulePath: filePath });
325
- continue;
326
- }
280
+ // For page and redirect files, the name determines the final node
281
+ const target = resolveTargetNode(node, name, dirSegments.length === 0);
327
282
 
328
- const fileType = getPageFileType(filename);
329
- if (fileType) {
330
- pageFiles.push({ path: filePath, pattern, fileType });
331
- }
332
- }
333
-
334
- // Group files by pattern
335
- const groups = new Map<string, { pattern: string; files: RouteFiles; parent?: string }>();
336
- for (const { path, pattern, fileType } of pageFiles) {
337
- let group = groups.get(pattern);
338
- if (!group) {
339
- group = { pattern, files: {} };
340
- const segments = pattern.split('/').filter(Boolean);
341
- if (segments.length > 1) {
342
- group.parent = '/' + segments.slice(0, -1).join('/');
343
- }
344
- groups.set(pattern, group);
345
- }
346
- const existing = group.files[fileType];
347
- if (existing?.includes('/index.page.') && !path.includes('/index.page.')) {
348
- continue;
349
- }
350
- group.files[fileType] = path;
351
- }
352
-
353
- // Detect collisions
354
- const warnings: string[] = [];
355
- for (const [pattern, group] of groups) {
356
- const filePaths = Object.values(group.files).filter(Boolean);
357
- const hasIndex = filePaths.some((p) => p?.includes('/index.page.'));
358
- const hasFlat = filePaths.some((p) => p && !p.includes('/index.page.'));
359
- if (hasIndex && hasFlat) {
360
- warnings.push(
361
- `Warning: Mixed file structure for ${pattern}:\n` +
362
- filePaths.map((p) => ` ${p}`).join('\n') +
363
- `\n Both folder/index and flat files detected`,
364
- );
283
+ if (kind === 'redirect') {
284
+ target.redirect = filePath;
285
+ } else {
286
+ // kind === 'page'
287
+ target.files ??= {};
288
+ target.files[ext as keyof RouteFiles] = filePath;
365
289
  }
366
290
  }
367
291
 
368
- // Convert groups to RouteConfig array
369
- const routes: RouteConfig[] = [];
370
- for (const [_, group] of groups) {
371
- const modulePath = group.files.ts ?? group.files.html ?? group.files.md ?? '';
372
- if (!modulePath) continue;
373
- const route: RouteConfig = {
374
- pattern: group.pattern,
375
- type: 'page',
376
- modulePath,
377
- files: group.files,
378
- };
379
- if (group.parent) route.parent = group.parent;
380
- routes.push(route);
381
- }
382
-
383
- routes.push(...redirects);
384
- const sortedRoutes = sortRoutesBySpecificity(routes);
385
-
386
- return {
387
- routes: sortedRoutes,
388
- errorBoundaries,
389
- statusPages,
390
- errorHandler,
391
- warnings,
392
- };
292
+ return root;
393
293
  }
394
294
 
395
295
  protected async scanWidgets(
@@ -173,7 +173,6 @@ export class BunFsRuntime extends Runtime {
173
173
  }
174
174
  const manifestPlugin = createManifestPlugin({
175
175
  runtime: this,
176
- basePath: '/html',
177
176
  resolveDir: this.root,
178
177
  });
179
178
  builds.push(esbuild.build({
@@ -129,7 +129,6 @@ export class BunSqliteRuntime extends Runtime {
129
129
  }
130
130
  const manifestPlugin = createManifestPlugin({
131
131
  runtime: this,
132
- basePath: '/html',
133
132
  resolveDir: process.cwd(),
134
133
  });
135
134
  builds.push(esbuild.build({
@@ -181,7 +181,6 @@ export class UniversalFsRuntime extends Runtime {
181
181
  }
182
182
  const manifestPlugin = createManifestPlugin({
183
183
  runtime: this,
184
- basePath: '/html',
185
184
  resolveDir: this.root,
186
185
  });
187
186
  builds.push(esbuild.build({
@@ -12,7 +12,7 @@ import type { SpaMode } from '../src/type/widget.type.ts';
12
12
  /**
13
13
  * Generate a main.ts entry point for SPA bootstrapping.
14
14
  *
15
- * Imports route and widget manifests from virtual `emroute:` specifiers
15
+ * Imports route tree and widget manifests from virtual `emroute:` specifiers
16
16
  * that the esbuild manifest plugin resolves at bundle time.
17
17
  */
18
18
  export function generateMainTs(
@@ -28,7 +28,7 @@ export function generateMainTs(
28
28
  const spaImport = `${importPath}/spa`;
29
29
 
30
30
  if (hasRoutes) {
31
- imports.push(`import { routesManifest } from 'emroute:routes';`);
31
+ imports.push(`import { routeTree, moduleLoaders } from 'emroute:routes';`);
32
32
  }
33
33
 
34
34
  if (hasWidgets) {
@@ -54,10 +54,14 @@ export function generateMainTs(
54
54
  }
55
55
 
56
56
  if ((spa === 'root' || spa === 'only') && hasRoutes) {
57
+ imports.push(`import { RouteTrie } from '${importPath}';`);
57
58
  imports.push(`import { createSpaHtmlRouter } from '${spaImport}';`);
59
+
60
+ body.push('const resolver = new RouteTrie(routeTree);');
61
+
58
62
  const bpOpt = basePath ? `basePath: { html: '${basePath.html}', md: '${basePath.md}' }` : '';
59
- const opts = bpOpt ? `{ ${bpOpt} }` : '';
60
- body.push(`await createSpaHtmlRouter(routesManifest${opts ? `, ${opts}` : ''});`);
63
+ const optsInner = [bpOpt, 'moduleLoaders'].filter(Boolean).join(', ');
64
+ body.push(`await createSpaHtmlRouter(resolver, { ${optsInner} });`);
61
65
  }
62
66
 
63
67
  return `/** Auto-generated entry point — do not edit. */\n${imports.join('\n')}\n\n${