@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.
- package/dist/runtime/abstract.runtime.d.ts +6 -4
- package/dist/runtime/abstract.runtime.js +42 -121
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.js +0 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +0 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.js +0 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
- package/dist/server/codegen.util.d.ts +1 -1
- package/dist/server/codegen.util.js +6 -4
- package/dist/server/codegen.util.js.map +1 -1
- package/dist/server/emroute.server.js +54 -47
- package/dist/server/emroute.server.js.map +1 -1
- package/dist/server/esbuild-manifest.plugin.d.ts +0 -2
- package/dist/server/esbuild-manifest.plugin.js +67 -87
- package/dist/server/esbuild-manifest.plugin.js.map +1 -1
- package/dist/server/server-api.type.d.ts +5 -5
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/renderer/spa/base.renderer.js +4 -4
- package/dist/src/renderer/spa/base.renderer.js.map +1 -1
- package/dist/src/renderer/spa/hash.renderer.d.ts +3 -2
- package/dist/src/renderer/spa/hash.renderer.js +36 -17
- package/dist/src/renderer/spa/hash.renderer.js.map +1 -1
- package/dist/src/renderer/spa/html.renderer.d.ts +11 -3
- package/dist/src/renderer/spa/html.renderer.js +24 -11
- package/dist/src/renderer/spa/html.renderer.js.map +1 -1
- package/dist/src/renderer/spa/mod.d.ts +4 -1
- package/dist/src/renderer/spa/mod.js +1 -0
- package/dist/src/renderer/spa/mod.js.map +1 -1
- package/dist/src/renderer/ssr/html.renderer.d.ts +4 -3
- package/dist/src/renderer/ssr/html.renderer.js +4 -4
- package/dist/src/renderer/ssr/html.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/md.renderer.d.ts +4 -3
- package/dist/src/renderer/ssr/md.renderer.js +4 -4
- package/dist/src/renderer/ssr/md.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/ssr.renderer.d.ts +3 -2
- package/dist/src/renderer/ssr/ssr.renderer.js +10 -15
- package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -1
- package/dist/src/route/route-tree.util.d.ts +15 -0
- package/dist/src/route/route-tree.util.js +32 -0
- package/dist/src/route/route-tree.util.js.map +1 -0
- package/dist/src/route/route.core.d.ts +31 -25
- package/dist/src/route/route.core.js +82 -60
- package/dist/src/route/route.core.js.map +1 -1
- package/dist/src/route/route.resolver.d.ts +28 -0
- package/dist/src/route/route.resolver.js +11 -0
- package/dist/src/route/route.resolver.js.map +1 -0
- package/dist/src/route/route.trie.d.ts +38 -0
- package/dist/src/route/route.trie.js +206 -0
- package/dist/src/route/route.trie.js.map +1 -0
- package/dist/src/type/route-tree.type.d.ts +42 -0
- package/dist/src/type/route-tree.type.js +12 -0
- package/dist/src/type/route-tree.type.js.map +1 -0
- package/package.json +1 -1
- package/runtime/abstract.runtime.ts +46 -146
- package/runtime/bun/fs/bun-fs.runtime.ts +0 -1
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +0 -1
- package/runtime/universal/fs/universal-fs.runtime.ts +0 -1
- package/server/codegen.util.ts +8 -4
- package/server/emroute.server.ts +50 -48
- package/server/esbuild-manifest.plugin.ts +68 -104
- package/server/server-api.type.ts +5 -5
- package/src/index.ts +5 -2
- package/src/renderer/spa/base.renderer.ts +4 -4
- package/src/renderer/spa/hash.renderer.ts +43 -20
- package/src/renderer/spa/html.renderer.ts +29 -12
- package/src/renderer/spa/mod.ts +3 -1
- package/src/renderer/ssr/html.renderer.ts +6 -5
- package/src/renderer/ssr/md.renderer.ts +6 -5
- package/src/renderer/ssr/ssr.renderer.ts +11 -17
- package/src/route/route-tree.util.ts +35 -0
- package/src/route/route.core.ts +89 -64
- package/src/route/route.resolver.ts +33 -0
- package/src/route/route.trie.ts +265 -0
- 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.
|
|
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
|
-
|
|
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
|
|
202
|
-
for (const w of warnings) console.warn(w);
|
|
191
|
+
const tree = await this.scanRoutes(routesDir);
|
|
203
192
|
|
|
204
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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(
|
package/server/codegen.util.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
60
|
-
body.push(`await createSpaHtmlRouter(
|
|
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${
|