@emkodev/emroute 1.7.2 → 1.8.0-beta.1

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 (236) hide show
  1. package/README.md +1 -1
  2. package/core/component/abstract.component.ts +74 -0
  3. package/{src → core}/component/page.component.ts +3 -61
  4. package/core/component/widget.component.ts +54 -0
  5. package/core/pipeline/pipeline.ts +224 -0
  6. package/{src/renderer/ssr → core/renderer}/html.renderer.ts +19 -45
  7. package/{src/renderer/ssr → core/renderer}/md.renderer.ts +21 -40
  8. package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +42 -53
  9. package/{src/route → core/router}/route.resolver.ts +1 -10
  10. package/core/router/route.trie.ts +175 -0
  11. package/core/runtime/abstract.runtime.ts +47 -0
  12. package/core/server/emroute.server.ts +346 -0
  13. package/core/type/component.type.ts +39 -0
  14. package/core/type/element.type.ts +10 -0
  15. package/core/type/logger.type.ts +20 -0
  16. package/core/type/markdown.type.ts +8 -0
  17. package/core/type/route-tree.type.ts +28 -0
  18. package/core/type/route.type.ts +75 -0
  19. package/core/type/widget.type.ts +27 -0
  20. package/core/util/html.util.ts +50 -0
  21. package/{src → core}/util/md.util.ts +0 -2
  22. package/{src/route → core/util}/route-tree.util.ts +0 -2
  23. package/{src → core}/util/widget-resolve.util.ts +15 -45
  24. package/{src → core}/widget/widget.parser.ts +0 -21
  25. package/core/widget/widget.registry.ts +24 -0
  26. package/dist/core/component/abstract.component.d.ts +48 -0
  27. package/dist/core/component/abstract.component.js +42 -0
  28. package/dist/core/component/abstract.component.js.map +1 -0
  29. package/dist/core/component/page.component.d.ts +23 -0
  30. package/dist/core/component/page.component.js +49 -0
  31. package/dist/core/component/page.component.js.map +1 -0
  32. package/dist/core/component/widget.component.d.ts +17 -0
  33. package/dist/core/component/widget.component.js +37 -0
  34. package/dist/core/component/widget.component.js.map +1 -0
  35. package/dist/core/pipeline/pipeline.d.ts +61 -0
  36. package/dist/core/pipeline/pipeline.js +189 -0
  37. package/dist/core/pipeline/pipeline.js.map +1 -0
  38. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.d.ts +8 -24
  39. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.js +12 -33
  40. package/dist/core/renderer/html.renderer.js.map +1 -0
  41. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.d.ts +6 -21
  42. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.js +14 -31
  43. package/dist/core/renderer/md.renderer.js.map +1 -0
  44. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -17
  45. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +34 -36
  46. package/dist/core/renderer/ssr.renderer.js.map +1 -0
  47. package/dist/{src/route → core/router}/route.resolver.d.ts +1 -8
  48. package/dist/{src/route → core/router}/route.resolver.js +0 -1
  49. package/dist/core/router/route.resolver.js.map +1 -0
  50. package/dist/core/router/route.trie.d.ts +32 -0
  51. package/dist/core/router/route.trie.js +152 -0
  52. package/dist/core/router/route.trie.js.map +1 -0
  53. package/dist/core/runtime/abstract.runtime.d.ts +32 -0
  54. package/dist/core/runtime/abstract.runtime.js +26 -0
  55. package/dist/core/runtime/abstract.runtime.js.map +1 -0
  56. package/dist/core/server/emroute.server.d.ts +48 -0
  57. package/dist/core/server/emroute.server.js +261 -0
  58. package/dist/core/server/emroute.server.js.map +1 -0
  59. package/dist/core/server/server.type.d.ts +45 -0
  60. package/dist/core/server/server.type.js +11 -0
  61. package/dist/core/server/server.type.js.map +1 -0
  62. package/dist/core/type/component.type.d.ts +37 -0
  63. package/dist/core/type/component.type.js +7 -0
  64. package/dist/core/type/component.type.js.map +1 -0
  65. package/dist/core/type/element.type.d.ts +9 -0
  66. package/dist/core/type/element.type.js +5 -0
  67. package/dist/core/type/element.type.js.map +1 -0
  68. package/dist/core/type/logger.type.d.ts +14 -0
  69. package/dist/core/type/logger.type.js +8 -0
  70. package/dist/core/type/logger.type.js.map +1 -0
  71. package/dist/core/type/markdown.type.d.ts +7 -0
  72. package/dist/core/type/markdown.type.js +5 -0
  73. package/dist/core/type/markdown.type.js.map +1 -0
  74. package/dist/{src → core}/type/route-tree.type.d.ts +0 -12
  75. package/dist/{src → core}/type/route-tree.type.js +0 -1
  76. package/dist/core/type/route-tree.type.js.map +1 -0
  77. package/dist/core/type/route.type.d.ts +62 -0
  78. package/dist/core/type/route.type.js +7 -0
  79. package/dist/core/type/route.type.js.map +1 -0
  80. package/dist/core/type/widget.type.d.ts +27 -0
  81. package/dist/core/type/widget.type.js +5 -0
  82. package/dist/core/type/widget.type.js.map +1 -0
  83. package/dist/core/util/html.util.d.ts +14 -0
  84. package/dist/core/util/html.util.js +43 -0
  85. package/dist/core/util/html.util.js.map +1 -0
  86. package/dist/{src → core}/util/md.util.d.ts +0 -1
  87. package/dist/{src → core}/util/md.util.js +0 -2
  88. package/dist/core/util/md.util.js.map +1 -0
  89. package/dist/{src/route → core/util}/route-tree.util.js +0 -2
  90. package/dist/core/util/route-tree.util.js.map +1 -0
  91. package/dist/core/util/widget-resolve.util.d.ts +32 -0
  92. package/dist/{src → core}/util/widget-resolve.util.js +11 -41
  93. package/dist/core/util/widget-resolve.util.js.map +1 -0
  94. package/dist/{src → core}/widget/widget.parser.d.ts +0 -13
  95. package/dist/{src → core}/widget/widget.parser.js +0 -21
  96. package/dist/core/widget/widget.parser.js.map +1 -0
  97. package/dist/core/widget/widget.registry.d.ts +13 -0
  98. package/dist/core/widget/widget.registry.js +19 -0
  99. package/dist/core/widget/widget.registry.js.map +1 -0
  100. package/dist/emroute.js +1137 -1151
  101. package/dist/emroute.js.map +36 -5
  102. package/dist/runtime/abstract.runtime.d.ts +50 -5
  103. package/dist/runtime/abstract.runtime.js +446 -6
  104. package/dist/runtime/abstract.runtime.js.map +1 -1
  105. package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +1 -0
  106. package/dist/runtime/bun/fs/bun-fs.runtime.js +18 -2
  107. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
  108. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +2 -0
  109. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +11 -1
  110. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
  111. package/dist/runtime/fetch.runtime.d.ts +3 -3
  112. package/dist/runtime/fetch.runtime.js +3 -3
  113. package/dist/runtime/sitemap.generator.d.ts +1 -1
  114. package/dist/runtime/sitemap.generator.js +1 -1
  115. package/dist/runtime/sitemap.generator.js.map +1 -1
  116. package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +1 -0
  117. package/dist/runtime/universal/fs/universal-fs.runtime.js +18 -2
  118. package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
  119. package/dist/server/build.util.d.ts +9 -10
  120. package/dist/server/build.util.js +45 -36
  121. package/dist/server/build.util.js.map +1 -1
  122. package/dist/server/codegen.util.d.ts +1 -1
  123. package/dist/server/emroute.server.d.ts +8 -35
  124. package/dist/server/emroute.server.js +7 -341
  125. package/dist/server/emroute.server.js.map +1 -1
  126. package/dist/server/esbuild-manifest.plugin.js +1 -1
  127. package/dist/server/esbuild-manifest.plugin.js.map +1 -1
  128. package/dist/server/server-api.type.d.ts +3 -68
  129. package/dist/server/server-api.type.js +1 -8
  130. package/dist/server/server-api.type.js.map +1 -1
  131. package/dist/src/element/component.element.d.ts +4 -7
  132. package/dist/src/element/component.element.js +23 -22
  133. package/dist/src/element/component.element.js.map +1 -1
  134. package/dist/src/element/markdown.element.d.ts +2 -2
  135. package/dist/src/element/markdown.element.js +4 -3
  136. package/dist/src/element/markdown.element.js.map +1 -1
  137. package/dist/src/index.d.ts +15 -13
  138. package/dist/src/index.js +8 -8
  139. package/dist/src/index.js.map +1 -1
  140. package/dist/src/renderer/spa/emroute.app.d.ts +50 -0
  141. package/dist/src/renderer/spa/emroute.app.js +246 -0
  142. package/dist/src/renderer/spa/emroute.app.js.map +1 -0
  143. package/dist/src/renderer/spa/mod.d.ts +17 -16
  144. package/dist/src/renderer/spa/mod.js +9 -9
  145. package/dist/src/renderer/spa/mod.js.map +1 -1
  146. package/dist/src/renderer/spa/thin-client.d.ts +3 -3
  147. package/dist/src/renderer/spa/thin-client.js +34 -12
  148. package/dist/src/renderer/spa/thin-client.js.map +1 -1
  149. package/dist/src/route/route.core.d.ts +3 -3
  150. package/dist/src/route/route.core.js +21 -7
  151. package/dist/src/route/route.core.js.map +1 -1
  152. package/dist/src/util/html.util.d.ts +5 -22
  153. package/dist/src/util/html.util.js +8 -56
  154. package/dist/src/util/html.util.js.map +1 -1
  155. package/dist/src/widget/breadcrumb.widget.d.ts +2 -2
  156. package/dist/src/widget/breadcrumb.widget.js +2 -2
  157. package/dist/src/widget/breadcrumb.widget.js.map +1 -1
  158. package/dist/src/widget/page-title.widget.d.ts +1 -1
  159. package/dist/src/widget/page-title.widget.js +1 -1
  160. package/dist/src/widget/page-title.widget.js.map +1 -1
  161. package/package.json +8 -8
  162. package/runtime/abstract.runtime.ts +483 -8
  163. package/runtime/bun/fs/bun-fs.runtime.ts +17 -1
  164. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +11 -0
  165. package/runtime/fetch.runtime.ts +3 -3
  166. package/runtime/sitemap.generator.ts +2 -2
  167. package/runtime/universal/fs/universal-fs.runtime.ts +17 -1
  168. package/server/build.util.ts +53 -47
  169. package/server/codegen.util.ts +1 -1
  170. package/server/emroute.server.ts +12 -412
  171. package/src/element/component.element.ts +24 -31
  172. package/src/element/markdown.element.ts +5 -4
  173. package/src/index.ts +22 -18
  174. package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +46 -22
  175. package/src/renderer/spa/mod.ts +22 -22
  176. package/src/util/html.util.ts +16 -61
  177. package/src/widget/breadcrumb.widget.ts +3 -3
  178. package/src/widget/page-title.widget.ts +1 -1
  179. package/dist/src/component/abstract.component.d.ts +0 -199
  180. package/dist/src/component/abstract.component.js +0 -84
  181. package/dist/src/component/abstract.component.js.map +0 -1
  182. package/dist/src/component/page.component.d.ts +0 -74
  183. package/dist/src/component/page.component.js +0 -107
  184. package/dist/src/component/page.component.js.map +0 -1
  185. package/dist/src/component/widget.component.d.ts +0 -47
  186. package/dist/src/component/widget.component.js +0 -69
  187. package/dist/src/component/widget.component.js.map +0 -1
  188. package/dist/src/renderer/ssr/html.renderer.js.map +0 -1
  189. package/dist/src/renderer/ssr/md.renderer.js.map +0 -1
  190. package/dist/src/renderer/ssr/ssr.renderer.js.map +0 -1
  191. package/dist/src/route/route-tree.util.js.map +0 -1
  192. package/dist/src/route/route.matcher.d.ts +0 -86
  193. package/dist/src/route/route.matcher.js +0 -214
  194. package/dist/src/route/route.matcher.js.map +0 -1
  195. package/dist/src/route/route.resolver.js.map +0 -1
  196. package/dist/src/route/route.trie.d.ts +0 -38
  197. package/dist/src/route/route.trie.js +0 -206
  198. package/dist/src/route/route.trie.js.map +0 -1
  199. package/dist/src/type/logger.type.d.ts +0 -17
  200. package/dist/src/type/logger.type.js +0 -9
  201. package/dist/src/type/logger.type.js.map +0 -1
  202. package/dist/src/type/markdown.type.d.ts +0 -20
  203. package/dist/src/type/markdown.type.js +0 -2
  204. package/dist/src/type/markdown.type.js.map +0 -1
  205. package/dist/src/type/route-tree.type.js.map +0 -1
  206. package/dist/src/type/route.type.d.ts +0 -94
  207. package/dist/src/type/route.type.js +0 -8
  208. package/dist/src/type/route.type.js.map +0 -1
  209. package/dist/src/type/widget.type.d.ts +0 -55
  210. package/dist/src/type/widget.type.js +0 -10
  211. package/dist/src/type/widget.type.js.map +0 -1
  212. package/dist/src/util/logger.util.d.ts +0 -26
  213. package/dist/src/util/logger.util.js +0 -80
  214. package/dist/src/util/logger.util.js.map +0 -1
  215. package/dist/src/util/md.util.js.map +0 -1
  216. package/dist/src/util/widget-resolve.util.d.ts +0 -52
  217. package/dist/src/util/widget-resolve.util.js.map +0 -1
  218. package/dist/src/widget/widget.parser.js.map +0 -1
  219. package/dist/src/widget/widget.registry.d.ts +0 -23
  220. package/dist/src/widget/widget.registry.js +0 -42
  221. package/dist/src/widget/widget.registry.js.map +0 -1
  222. package/runtime/bun/esbuild-runtime-loader.plugin.ts +0 -112
  223. package/server/esbuild-manifest.plugin.ts +0 -209
  224. package/server/server-api.type.ts +0 -97
  225. package/src/component/abstract.component.ts +0 -231
  226. package/src/component/widget.component.ts +0 -85
  227. package/src/route/route.core.ts +0 -362
  228. package/src/route/route.trie.ts +0 -265
  229. package/src/type/logger.type.ts +0 -24
  230. package/src/type/markdown.type.ts +0 -21
  231. package/src/type/route-tree.type.ts +0 -51
  232. package/src/type/route.type.ts +0 -124
  233. package/src/type/widget.type.ts +0 -65
  234. package/src/util/logger.util.ts +0 -83
  235. package/src/widget/widget.registry.ts +0 -51
  236. /package/dist/{src/route → core/util}/route-tree.util.d.ts +0 -0
@@ -1,362 +0,0 @@
1
- /**
2
- * Route Core
3
- *
4
- * Shared routing logic used by all renderers:
5
- * - Route matching (delegates to RouteResolver)
6
- * - Module loading and caching
7
- * - Event emission
8
- * - URL normalization
9
- * - BasePath stripping
10
- */
11
-
12
- import type {
13
- MatchedRoute,
14
- RouteConfig,
15
- RouteInfo,
16
- RouteParams,
17
- RouterEvent,
18
- RouterEventListener,
19
- } from '../type/route.type.ts';
20
- import type { ComponentContext, ContextProvider } from '../component/abstract.component.ts';
21
- import type { RouteResolver, ResolvedRoute } from './route.resolver.ts';
22
-
23
- /** Base paths for the two SSR rendering endpoints. */
24
- export interface BasePath {
25
- /** Base path for SSR HTML rendering (default: '/html') */
26
- html: string;
27
- /** Base path for SSR Markdown rendering (default: '/md') */
28
- md: string;
29
- /** Base path for PWA/SPA rendering (default: '/app') */
30
- app: string;
31
- }
32
-
33
- /** Default base paths — backward compatible with existing /html/ and /md/ prefixes. */
34
- export const DEFAULT_BASE_PATH: BasePath = { html: '/html', md: '/md', app: '/app' };
35
-
36
- const BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
37
-
38
- /** Throw if a redirect URL uses a dangerous protocol. */
39
- export function assertSafeRedirect(url: string): void {
40
- if (BLOCKED_PROTOCOLS.test(url.trim())) {
41
- throw new Error(`Unsafe redirect URL blocked: ${url}`);
42
- }
43
- }
44
-
45
- /** Default root route — renders a slot for child routes. */
46
- export const DEFAULT_ROOT_ROUTE: RouteConfig = {
47
- pattern: '/',
48
- type: 'page',
49
- modulePath: '__default_root__',
50
- };
51
-
52
- /** Synthesize a RouteConfig from a ResolvedRoute (bridge for renderer compatibility). */
53
- function toRouteConfig(resolved: ResolvedRoute): RouteConfig {
54
- const node = resolved.node;
55
- return {
56
- pattern: resolved.pattern,
57
- type: node.redirect ? 'redirect' : 'page',
58
- modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? '',
59
- files: node.files,
60
- };
61
- }
62
-
63
- /** Options for RouteCore */
64
- export interface RouteCoreOptions {
65
- /**
66
- * Read a companion file (.html, .md, .css) by path — returns its text content.
67
- * SSR: `(path) => runtime.query(path, { as: 'text' })`.
68
- * SPA default: `fetch(path, { headers: { Accept: 'text/plain' } }).then(r => r.text())`.
69
- */
70
- fileReader?: (path: string) => Promise<string>;
71
- /** Enriches every ComponentContext with app-level services before it reaches components. */
72
- extendContext?: ContextProvider;
73
- /** Module loaders keyed by path — server provides these for SSR imports. */
74
- moduleLoaders?: Record<string, () => Promise<unknown>>;
75
- }
76
-
77
- /**
78
- * Core router functionality shared across all rendering contexts.
79
- */
80
- export class RouteCore {
81
- private readonly resolver: RouteResolver;
82
- /** Registered context provider (if any). Exposed so renderers can apply it to inline contexts. */
83
- readonly contextProvider: ContextProvider | undefined;
84
- private listeners: Set<RouterEventListener> = new Set();
85
- private moduleCache: Map<string, unknown> = new Map();
86
- private widgetFileCache: Map<string, string> = new Map();
87
- private moduleLoaders: Record<string, () => Promise<unknown>>;
88
- currentRoute: MatchedRoute | null = null;
89
- private readFile: (path: string) => Promise<string>;
90
-
91
- constructor(resolver: RouteResolver, options: RouteCoreOptions = {}) {
92
- this.resolver = resolver;
93
- this.readFile = options.fileReader ??
94
- ((path) => fetch(path, { headers: { Accept: 'text/plain' } }).then((r) => r.text()));
95
- this.contextProvider = options.extendContext;
96
- this.moduleLoaders = options.moduleLoaders ?? {};
97
- }
98
-
99
- /**
100
- * Get current route parameters.
101
- */
102
- getParams(): RouteParams {
103
- return this.currentRoute?.params ?? {};
104
- }
105
-
106
- /**
107
- * Add event listener for router events.
108
- */
109
- addEventListener(listener: RouterEventListener): () => void {
110
- this.listeners.add(listener);
111
- return () => this.listeners.delete(listener);
112
- }
113
-
114
- /**
115
- * Emit router event to listeners.
116
- */
117
- emit(event: RouterEvent): void {
118
- for (const listener of this.listeners) {
119
- try {
120
- listener(event);
121
- } catch (e) {
122
- console.error('[Router] Event listener error:', e);
123
- }
124
- }
125
- }
126
-
127
- /**
128
- * Match a URL to a route.
129
- * Falls back to the default root route for '/'.
130
- */
131
- match(url: URL): MatchedRoute | undefined {
132
- const pathname = url.pathname;
133
-
134
- const resolved = this.resolver.match(pathname);
135
- if (resolved) {
136
- return {
137
- route: toRouteConfig(resolved),
138
- params: resolved.params,
139
- };
140
- }
141
-
142
- if (pathname === '/' || pathname === '') {
143
- return {
144
- route: DEFAULT_ROOT_ROUTE,
145
- params: {},
146
- };
147
- }
148
-
149
- return undefined;
150
- }
151
-
152
- /** Get status-specific page (404, 401, 403). */
153
- getStatusPage(status: number): RouteConfig | undefined {
154
- const node = this.resolver.findRoute(`/${status}`);
155
- if (!node) return undefined;
156
- return {
157
- pattern: `/${status}`,
158
- type: 'page',
159
- modulePath: node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? '',
160
- files: node.files,
161
- };
162
- }
163
-
164
- /** Get global error handler (root errorBoundary). */
165
- getErrorHandler(): RouteConfig | undefined {
166
- const modulePath = this.resolver.findErrorBoundary('/');
167
- if (!modulePath) return undefined;
168
- return { pattern: '/', type: 'error', modulePath };
169
- }
170
-
171
- /**
172
- * Find error boundary for a given pathname.
173
- * Note: pattern is the input pathname, not the boundary's own pattern.
174
- * Callers should only rely on modulePath.
175
- */
176
- findErrorBoundary(pathname: string): { pattern: string; modulePath: string } | undefined {
177
- const modulePath = this.resolver.findErrorBoundary(pathname);
178
- if (!modulePath) return undefined;
179
- return { pattern: pathname, modulePath };
180
- }
181
-
182
- /**
183
- * Find a route by its exact pattern.
184
- * Used for building route hierarchy.
185
- */
186
- findRoute(pattern: string): RouteConfig | undefined {
187
- const node = this.resolver.findRoute(pattern);
188
- if (!node) return undefined;
189
- return {
190
- pattern,
191
- type: node.redirect ? 'redirect' : 'page',
192
- modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? '',
193
- files: node.files,
194
- };
195
- }
196
-
197
- /**
198
- * Build route hierarchy from a pattern.
199
- * Patterns are always unprefixed (no basePath).
200
- *
201
- * e.g., '/projects/:id/tasks'
202
- * → ['/', '/projects', '/projects/:id', '/projects/:id/tasks']
203
- */
204
- buildRouteHierarchy(pattern: string): string[] {
205
- if (pattern === '/') {
206
- return ['/'];
207
- }
208
-
209
- const segments = pattern.split('/').filter(Boolean);
210
-
211
- const hierarchy: string[] = ['/'];
212
- let current = '';
213
- for (const segment of segments) {
214
- current += '/' + segment;
215
- hierarchy.push(current);
216
- }
217
-
218
- return hierarchy;
219
- }
220
-
221
- /**
222
- * Normalize URL by removing trailing slashes (except bare '/').
223
- */
224
- normalizeUrl(url: string): string {
225
- if (url.length > 1 && url.endsWith('/')) {
226
- return url.slice(0, -1);
227
- }
228
- return url;
229
- }
230
-
231
- /**
232
- * Convert relative path to absolute path.
233
- */
234
- toAbsolutePath(path: string): string {
235
- return path.startsWith('/') ? path : '/' + path;
236
- }
237
-
238
- /**
239
- * Load a module with caching.
240
- * Uses pre-bundled loaders when available, falls back to dynamic import.
241
- */
242
- async loadModule<T>(modulePath: string): Promise<T> {
243
- if (this.moduleCache.has(modulePath)) {
244
- return this.moduleCache.get(modulePath) as T;
245
- }
246
-
247
- let module: unknown;
248
- const loader = this.moduleLoaders[modulePath];
249
- if (loader) {
250
- module = await loader();
251
- } else {
252
- const absolutePath = this.toAbsolutePath(modulePath);
253
- module = await import(absolutePath);
254
- }
255
-
256
- this.moduleCache.set(modulePath, module);
257
- return module as T;
258
- }
259
-
260
- /**
261
- * Load widget file contents with caching.
262
- */
263
- async loadWidgetFiles(
264
- widgetFiles: { html?: string; md?: string; css?: string },
265
- ): Promise<{ html?: string; md?: string; css?: string }> {
266
- const load = async (path: string): Promise<string | undefined> => {
267
- const absPath = this.toAbsolutePath(path);
268
- const cached = this.widgetFileCache.get(absPath);
269
- if (cached !== undefined) return cached;
270
-
271
- try {
272
- const content = await this.readFile(absPath);
273
- this.widgetFileCache.set(absPath, content);
274
- return content;
275
- } catch (e) {
276
- console.warn(
277
- `[RouteCore] Failed to load widget file ${path}:`,
278
- e instanceof Error ? e.message : e,
279
- );
280
- return undefined;
281
- }
282
- };
283
-
284
- const [html, md, css] = await Promise.all([
285
- widgetFiles.html ? load(widgetFiles.html) : undefined,
286
- widgetFiles.md ? load(widgetFiles.md) : undefined,
287
- widgetFiles.css ? load(widgetFiles.css) : undefined,
288
- ]);
289
-
290
- return { html, md, css };
291
- }
292
-
293
- /**
294
- * Build a RouteInfo from a matched route and the resolved URL pathname.
295
- * Called once per navigation; the result is reused across the route hierarchy.
296
- */
297
- toRouteInfo(matched: MatchedRoute, url: URL): RouteInfo {
298
- return {
299
- url,
300
- params: matched.params,
301
- };
302
- }
303
-
304
- /**
305
- * Get inlined `__files` from a cached module (merged module pattern).
306
- * Returns undefined if the module isn't cached or has no __files.
307
- */
308
- getModuleFiles(modulePath: string): { html?: string; md?: string; css?: string } | undefined {
309
- const cached = this.moduleCache.get(modulePath);
310
- if (!cached || typeof cached !== 'object') return undefined;
311
- const files = (cached as Record<string, unknown>).__files;
312
- if (!files || typeof files !== 'object') return undefined;
313
- return files as { html?: string; md?: string; css?: string };
314
- }
315
-
316
- /**
317
- * Build a ComponentContext by extending RouteInfo with loaded file contents.
318
- *
319
- * When the route module is a merged module (contains `__files`), uses
320
- * inlined content directly. Otherwise falls back to reading companion files.
321
- */
322
- async buildComponentContext(
323
- routeInfo: RouteInfo,
324
- route: RouteConfig,
325
- signal?: AbortSignal,
326
- isLeaf?: boolean,
327
- ): Promise<ComponentContext> {
328
- const rf = route.files;
329
- const modulePath = rf?.ts ?? rf?.js;
330
-
331
- // Try inlined __files from merged module (already cached by loadRouteContent)
332
- const inlined = modulePath ? this.getModuleFiles(modulePath) : undefined;
333
-
334
- let html: string | undefined;
335
- let md: string | undefined;
336
- let css: string | undefined;
337
-
338
- if (inlined) {
339
- html = inlined.html;
340
- md = inlined.md;
341
- css = inlined.css;
342
- } else {
343
- const fetchFile = (filePath: string): Promise<string> =>
344
- this.readFile(this.toAbsolutePath(filePath));
345
- [html, md, css] = await Promise.all([
346
- rf?.html ? fetchFile(rf.html) : undefined,
347
- rf?.md ? fetchFile(rf.md) : undefined,
348
- rf?.css ? fetchFile(rf.css) : undefined,
349
- ]);
350
- }
351
-
352
- const base: ComponentContext = {
353
- ...routeInfo,
354
- pathname: routeInfo.url.pathname,
355
- searchParams: routeInfo.url.searchParams,
356
- files: { html, md, css },
357
- signal,
358
- isLeaf,
359
- };
360
- return this.contextProvider ? this.contextProvider(base) : base;
361
- }
362
- }
@@ -1,265 +0,0 @@
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
-
18
- import type { RouteNode } from '../type/route-tree.type.ts';
19
- import type { RouteResolver, ResolvedRoute } from './route.resolver.ts';
20
-
21
- /** Internal trie node with Map for O(1) static child lookup. */
22
- interface TrieNode {
23
- /** RouteNode when this node is a terminal route. */
24
- route?: RouteNode;
25
- /** Reconstructed pattern for this node (e.g. "/projects/:id"). */
26
- pattern?: string;
27
- /** Error boundary module path scoped to this prefix. */
28
- errorBoundary?: string;
29
- /** Static children keyed by exact segment. */
30
- static: Map<string, TrieNode>;
31
- /** Dynamic child for single-segment params (:param). */
32
- dynamic?: { param: string; node: TrieNode };
33
- /** Wildcard child for catch-all params (:rest*). */
34
- wildcard?: { param: string; node: TrieNode };
35
- }
36
-
37
- function createNode(): TrieNode {
38
- return { static: new Map() };
39
- }
40
-
41
- /** Try decodeURIComponent, return the original segment on malformed input. */
42
- function safeDecode(segment: string): string {
43
- try {
44
- return decodeURIComponent(segment);
45
- } catch {
46
- return segment;
47
- }
48
- }
49
-
50
- /**
51
- * Split a normalized pathname into segments.
52
- * Assumes leading slash, no trailing slash. Must NOT be called with '/'.
53
- */
54
- function splitSegments(pathname: string): string[] {
55
- return pathname.substring(1).split('/');
56
- }
57
-
58
- /**
59
- * Convert a RouteNode tree into a TrieNode tree.
60
- * Recursively walks the RouteNode, converting Record children to Map
61
- * and reconstructing URL patterns at each node.
62
- */
63
- function convertNode(source: RouteNode, pattern: string): TrieNode {
64
- const node = createNode();
65
-
66
- // Terminal route (has files or redirect)
67
- if (source.files || source.redirect) {
68
- node.route = source;
69
- node.pattern = pattern;
70
- }
71
-
72
- // Error boundary
73
- if (source.errorBoundary) {
74
- node.errorBoundary = source.errorBoundary;
75
- }
76
-
77
- // Static children
78
- if (source.children) {
79
- for (const [segment, child] of Object.entries(source.children)) {
80
- const childPattern = pattern === '/' ? `/${segment}` : `${pattern}/${segment}`;
81
- node.static.set(segment, convertNode(child, childPattern));
82
- }
83
- }
84
-
85
- // Dynamic child
86
- if (source.dynamic) {
87
- const { param, child } = source.dynamic;
88
- const childPattern = pattern === '/' ? `/:${param}` : `${pattern}/:${param}`;
89
- node.dynamic = { param, node: convertNode(child, childPattern) };
90
- }
91
-
92
- // Wildcard child
93
- if (source.wildcard) {
94
- const { param, child } = source.wildcard;
95
- const childPattern = pattern === '/' ? `/:${param}*` : `${pattern}/:${param}*`;
96
- node.wildcard = { param, node: convertNode(child, childPattern) };
97
- }
98
-
99
- return node;
100
- }
101
-
102
- /**
103
- * Trie-based route resolver.
104
- *
105
- * Implements RouteResolver with O(depth) matching by walking the trie.
106
- * Constructed from a RouteNode tree (produced by Runtime.scanRoutes).
107
- */
108
- export class RouteTrie implements RouteResolver {
109
- private readonly root: TrieNode;
110
-
111
- constructor(tree: RouteNode) {
112
- this.root = convertNode(tree, '/');
113
- }
114
-
115
- match(pathname: string): ResolvedRoute | undefined {
116
- // Normalize: strip trailing slash (except bare '/')
117
- if (pathname.length > 1 && pathname.endsWith('/')) {
118
- pathname = pathname.slice(0, -1);
119
- }
120
-
121
- if (!pathname.startsWith('/')) {
122
- pathname = '/' + pathname;
123
- }
124
-
125
- if (pathname === '/') {
126
- if (this.root.route) {
127
- return { node: this.root.route, pattern: '/', params: {} };
128
- }
129
- return undefined;
130
- }
131
-
132
- const segments = splitSegments(pathname);
133
- return this.walk(this.root, segments, 0, {});
134
- }
135
-
136
- findErrorBoundary(pathname: string): string | undefined {
137
- if (pathname.length > 1 && pathname.endsWith('/')) {
138
- pathname = pathname.slice(0, -1);
139
- }
140
- if (!pathname.startsWith('/')) {
141
- pathname = '/' + pathname;
142
- }
143
-
144
- if (pathname === '/') return this.root.errorBoundary;
145
-
146
- const segments = splitSegments(pathname);
147
- return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
148
- }
149
-
150
- findRoute(pattern: string): RouteNode | undefined {
151
- if (pattern === '/') {
152
- return this.root.route;
153
- }
154
-
155
- const segments = splitSegments(pattern);
156
- let node = this.root;
157
-
158
- for (const segment of segments) {
159
- let child: TrieNode | undefined;
160
-
161
- if (segment.startsWith(':') && segment.endsWith('*')) {
162
- child = node.wildcard?.node;
163
- } else if (segment.startsWith(':')) {
164
- child = node.dynamic?.node;
165
- } else {
166
- child = node.static.get(segment);
167
- }
168
-
169
- if (!child) return undefined;
170
- node = child;
171
- }
172
-
173
- return node.route;
174
- }
175
-
176
- // ── Private matching ──────────────────────────────────────────────────
177
-
178
- private walk(
179
- node: TrieNode,
180
- segments: string[],
181
- index: number,
182
- params: Record<string, string>,
183
- ): ResolvedRoute | undefined {
184
- if (index === segments.length) {
185
- if (node.route) {
186
- return { node: node.route, pattern: node.pattern!, params: { ...params } };
187
- }
188
- if (node.wildcard?.node.route) {
189
- return {
190
- node: node.wildcard.node.route,
191
- pattern: node.wildcard.node.pattern!,
192
- params: { ...params, [node.wildcard.param]: '' },
193
- };
194
- }
195
- return undefined;
196
- }
197
-
198
- const segment = segments[index];
199
-
200
- // 1. Try static child
201
- const staticChild = node.static.get(segment);
202
- if (staticChild) {
203
- const result = this.walk(staticChild, segments, index + 1, params);
204
- if (result) return result;
205
- }
206
-
207
- // 2. Try dynamic child (single segment)
208
- if (node.dynamic) {
209
- const { param, node: dynamicNode } = node.dynamic;
210
- params[param] = safeDecode(segment);
211
- const result = this.walk(dynamicNode, segments, index + 1, params);
212
- if (result) return result;
213
- delete params[param];
214
- }
215
-
216
- // 3. Try wildcard (consumes all remaining segments)
217
- if (node.wildcard?.node.route) {
218
- const { param, node: wildcardNode } = node.wildcard;
219
- let rest = safeDecode(segments[index]);
220
- for (let i = index + 1; i < segments.length; i++) {
221
- rest += '/' + safeDecode(segments[i]);
222
- }
223
- return {
224
- node: wildcardNode.route!,
225
- pattern: wildcardNode.pattern!,
226
- params: { ...params, [param]: rest },
227
- };
228
- }
229
-
230
- return undefined;
231
- }
232
-
233
- /**
234
- * Walk for error boundary. Follows the same priority as match
235
- * (static → dynamic → wildcard) without backtracking across branches.
236
- * Returns the deepest error boundary module path found along the path.
237
- */
238
- private walkForBoundary(
239
- node: TrieNode,
240
- segments: string[],
241
- index: number,
242
- deepest: string | undefined,
243
- ): string | undefined {
244
- if (index === segments.length) {
245
- return node.errorBoundary ?? deepest;
246
- }
247
-
248
- const segment = segments[index];
249
-
250
- const staticChild = node.static.get(segment);
251
- if (staticChild) {
252
- return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
253
- }
254
-
255
- if (node.dynamic) {
256
- return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
257
- }
258
-
259
- if (node.wildcard) {
260
- return node.wildcard.node.errorBoundary ?? deepest;
261
- }
262
-
263
- return deepest;
264
- }
265
- }
@@ -1,24 +0,0 @@
1
- /**
2
- * Logger Interface
3
- *
4
- * Minimal pluggable logger for surfacing errors from silent catch blocks.
5
- * Structurally compatible with hardkore's StructuredLogger — any instance
6
- * of that class satisfies this interface without an explicit dependency.
7
- *
8
- * Default: no-op (silent degradation). Call setLogger() at startup to wire in.
9
- */
10
- export interface Logger {
11
- error(msg: string, error?: Error): void;
12
- warn(msg: string): void;
13
- }
14
-
15
- const noop = () => {};
16
-
17
- /** Module-level logger. Always callable — defaults to no-op. */
18
- export const logger: Logger = { error: noop, warn: noop };
19
-
20
- /** Replace the logger implementation. Call once at startup. */
21
- export function setLogger(impl: Logger): void {
22
- logger.error = impl.error.bind(impl);
23
- logger.warn = impl.warn.bind(impl);
24
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * Markdown Renderer Interface
3
- *
4
- * Implement this to provide custom markdown rendering.
5
- * Used by MarkdownElement (browser) and SsrHtmlRouter (server).
6
- */
7
- export interface MarkdownRenderer {
8
- /**
9
- * Initialize the renderer (e.g., load WASM).
10
- * Called once before first render.
11
- */
12
- init?(): Promise<void>;
13
-
14
- /**
15
- * Render markdown to HTML.
16
- *
17
- * **Security:** Output is assigned to `innerHTML` — the renderer must
18
- * sanitize dangerous markup. See `doc/08-markdown-renderer.md`.
19
- */
20
- render(markdown: string): string;
21
- }