@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
@@ -5,13 +5,14 @@
5
5
  * Generates Markdown strings for LLM consumption, text clients, curl.
6
6
  */
7
7
 
8
- import type { RouteConfig, RouteInfo } from '../../type/route.type.ts';
9
- import type { RouteResolver } from '../../route/route.resolver.ts';
10
- import type { PageComponent } from '../../component/page.component.ts';
11
- import { DEFAULT_ROOT_ROUTE } from '../../route/route.core.ts';
12
- import { STATUS_MESSAGES } from '../../util/html.util.ts';
13
- import { resolveRecursively } from '../../util/widget-resolve.util.ts';
14
- import { parseWidgetBlocks, replaceWidgetBlocks } from '../../widget/widget.parser.ts';
8
+ import type { RouteConfig, RouteInfo } from '../type/route.type.ts';
9
+ import type { ComponentContext } from '../type/component.type.ts';
10
+ import type { PageComponent } from '../component/page.component.ts';
11
+ import type { Pipeline } from '../pipeline/pipeline.ts';
12
+ import { DEFAULT_ROOT_ROUTE } from '../pipeline/pipeline.ts';
13
+ import { STATUS_MESSAGES } from '../util/html.util.ts';
14
+ import { resolveRecursively } from '../util/widget-resolve.util.ts';
15
+ import { parseWidgetBlocks, replaceWidgetBlocks } from '../widget/widget.parser.ts';
15
16
  import { SsrRenderer, type SsrRendererOptions } from './ssr.renderer.ts';
16
17
 
17
18
  const BARE_SLOT_BLOCK = '```router-slot\n```';
@@ -20,17 +21,13 @@ function routerSlotBlock(pattern: string): string {
20
21
  return `\`\`\`router-slot\n{"pattern":"${pattern}"}\n\`\`\``;
21
22
  }
22
23
 
23
- /** Options for SSR Markdown Router */
24
- export type SsrMdRouterOptions = SsrRendererOptions;
24
+ export type SsrMdRendererOptions = SsrRendererOptions;
25
25
 
26
- /**
27
- * SSR Markdown Router for server-side markdown rendering.
28
- */
29
- export class SsrMdRouter extends SsrRenderer {
26
+ export class SsrMdRenderer extends SsrRenderer {
30
27
  protected override readonly label = 'SSR MD';
31
28
 
32
- constructor(resolver: RouteResolver, options: SsrMdRouterOptions = {}) {
33
- super(resolver, options);
29
+ constructor(pipeline: Pipeline, options: SsrMdRendererOptions = {}) {
30
+ super(pipeline, options);
34
31
  }
35
32
 
36
33
  protected override injectSlot(parent: string, child: string, parentPattern: string): string {
@@ -43,9 +40,6 @@ export class SsrMdRouter extends SsrRenderer {
43
40
  .trim();
44
41
  }
45
42
 
46
- /**
47
- * Render a single route's content to Markdown.
48
- */
49
43
  protected override async renderRouteContent(
50
44
  routeInfo: RouteInfo,
51
45
  route: RouteConfig,
@@ -60,15 +54,14 @@ export class SsrMdRouter extends SsrRenderer {
60
54
  let content = rawContent;
61
55
 
62
56
  // Attribute bare router-slot blocks with this route's pattern
63
- // (before widget resolution so widget-internal blocks are not affected)
64
57
  content = content.replaceAll(BARE_SLOT_BLOCK, routerSlotBlock(route.pattern));
65
58
 
66
- // Resolve fenced widget blocks: call getData() + renderMarkdown()
59
+ // Resolve fenced widget blocks
67
60
  if (this.widgets) {
68
61
  content = await this.resolveWidgets(content, routeInfo);
69
62
  }
70
63
 
71
- return { content, title };
64
+ return { content, ...(title !== undefined ? { title } : {}) };
72
65
  }
73
66
 
74
67
  protected override renderContent(
@@ -90,10 +83,6 @@ export class SsrMdRouter extends SsrRenderer {
90
83
  return `# Internal Server Error\n\nPath: \`${url.pathname}\``;
91
84
  }
92
85
 
93
- /**
94
- * Resolve fenced widget blocks in markdown content.
95
- * Replaces ```widget:name blocks with rendered markdown output.
96
- */
97
86
  private resolveWidgets(
98
87
  content: string,
99
88
  routeInfo: RouteInfo,
@@ -115,17 +104,17 @@ export class SsrMdRouter extends SsrRenderer {
115
104
  let files: { html?: string; md?: string } | undefined;
116
105
  const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
117
106
  if (filePaths) {
118
- files = await this.core.loadWidgetFiles(filePaths);
107
+ files = await this.pipeline.loadFiles(filePaths);
119
108
  }
120
109
 
121
- const baseContext = {
110
+ const baseContext: ComponentContext = {
122
111
  ...routeInfo,
123
112
  pathname: routeInfo.url.pathname,
124
113
  searchParams: routeInfo.url.searchParams,
125
- files,
114
+ ...(files ? { files } : {}),
126
115
  };
127
- const context = this.core.contextProvider
128
- ? this.core.contextProvider(baseContext)
116
+ const context: ComponentContext = this.pipeline.contextProvider
117
+ ? this.pipeline.contextProvider(baseContext)
129
118
  : baseContext;
130
119
  const data = await widget.getData({ params: block.params, context });
131
120
  return widget.renderMarkdown({ data, params: block.params, context });
@@ -134,16 +123,8 @@ export class SsrMdRouter extends SsrRenderer {
134
123
  }
135
124
  },
136
125
  replaceWidgetBlocks,
126
+ 0,
127
+ this.logger,
137
128
  );
138
129
  }
139
130
  }
140
-
141
- /**
142
- * Create SSR Markdown router.
143
- */
144
- export function createSsrMdRouter(
145
- resolver: RouteResolver,
146
- options?: SsrMdRouterOptions,
147
- ): SsrMdRouter {
148
- return new SsrMdRouter(resolver, options);
149
- }
@@ -9,24 +9,17 @@ import type {
9
9
  MatchedRoute,
10
10
  RouteConfig,
11
11
  RouteInfo,
12
- } from '../../type/route.type.ts';
13
- import { logger } from '../../type/logger.type.ts';
14
- import type { ComponentContext } from '../../component/abstract.component.ts';
15
- import defaultPageComponent, { type PageComponent } from '../../component/page.component.ts';
16
- import {
17
- assertSafeRedirect,
18
- DEFAULT_ROOT_ROUTE,
19
- RouteCore,
20
- type RouteCoreOptions,
21
- } from '../../route/route.core.ts';
22
- import type { RouteResolver } from '../../route/route.resolver.ts';
23
- import type { WidgetRegistry } from '../../widget/widget.registry.ts';
24
-
25
- /** Base options for SSR renderers */
26
- export interface SsrRendererOptions extends RouteCoreOptions {
27
- /** Widget registry for server-side widget rendering */
12
+ } from '../type/route.type.ts';
13
+ import type { ComponentContext } from '../type/component.type.ts';
14
+ import type { Logger } from '../type/logger.type.ts';
15
+ import defaultPageComponent, { type PageComponent } from '../component/page.component.ts';
16
+ import { DEFAULT_ROOT_ROUTE, type Pipeline } from '../pipeline/pipeline.ts';
17
+ import { assertSafeRedirect } from '../util/html.util.ts';
18
+ import type { WidgetRegistry } from '../widget/widget.registry.ts';
19
+
20
+ /** Options for SSR renderers. */
21
+ export interface SsrRendererOptions {
28
22
  widgets?: WidgetRegistry;
29
- /** Widget companion file paths, keyed by widget name */
30
23
  widgetFiles?: Record<string, { html?: string; md?: string; css?: string }>;
31
24
  }
32
25
 
@@ -34,13 +27,16 @@ export interface SsrRendererOptions extends RouteCoreOptions {
34
27
  * Abstract SSR renderer with shared routing pipeline.
35
28
  */
36
29
  export abstract class SsrRenderer {
37
- protected core: RouteCore;
30
+ protected readonly pipeline: Pipeline;
38
31
  protected widgets: WidgetRegistry | null;
39
32
  protected widgetFiles: Record<string, { html?: string; md?: string; css?: string }>;
40
33
  protected abstract readonly label: string;
41
34
 
42
- constructor(resolver: RouteResolver, options: SsrRendererOptions = {}) {
43
- this.core = new RouteCore(resolver, options);
35
+ protected readonly logger: Logger;
36
+
37
+ constructor(pipeline: Pipeline, options: SsrRendererOptions = {}) {
38
+ this.pipeline = pipeline;
39
+ this.logger = pipeline.logger;
44
40
  this.widgets = options.widgets ?? null;
45
41
  this.widgetFiles = options.widgetFiles ?? {};
46
42
  }
@@ -52,17 +48,17 @@ export abstract class SsrRenderer {
52
48
  url: URL,
53
49
  signal?: AbortSignal,
54
50
  ): Promise<{ content: string; status: number; title?: string; redirect?: string }> {
55
- const matched = this.core.match(url);
51
+ const matched = await this.pipeline.match(url);
56
52
 
57
53
  if (!matched) {
58
- const statusPage = this.core.getStatusPage(404);
54
+ const statusPage = await this.pipeline.getStatusPage(404);
59
55
  if (statusPage) {
60
56
  try {
61
57
  const ri: RouteInfo = { url, params: {} };
62
58
  const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
63
- return { content: this.stripSlots(result.content), status: 404, title: result.title };
59
+ return { content: this.stripSlots(result.content), status: 404, ...(result.title !== undefined ? { title: result.title } : {}) };
64
60
  } catch (e) {
65
- logger.error(
61
+ this.logger.error(
66
62
  `[${this.label}] Failed to render 404 status page for ${url.pathname}`,
67
63
  e instanceof Error ? e : undefined,
68
64
  );
@@ -73,7 +69,7 @@ export abstract class SsrRenderer {
73
69
 
74
70
  // Handle redirect
75
71
  if (matched.route.type === 'redirect') {
76
- const module = await this.core.loadModule<{ default: { to: string; status?: number } }>(
72
+ const module = await this.pipeline.loadModule<{ default: { to: string; status?: number } }>(
77
73
  matched.route.modulePath,
78
74
  );
79
75
  const redirectConfig = module.default;
@@ -85,14 +81,14 @@ export abstract class SsrRenderer {
85
81
  };
86
82
  }
87
83
 
88
- const routeInfo = this.core.toRouteInfo(matched, url);
84
+ const routeInfo = this.pipeline.toRouteInfo(matched, url);
89
85
 
90
86
  try {
91
87
  const { content, title } = await this.renderPage(routeInfo, matched, signal);
92
- return { content, status: 200, title };
88
+ return { content, status: 200, ...(title !== undefined ? { title } : {}) };
93
89
  } catch (error) {
94
90
  if (error instanceof Response) {
95
- const statusPage = this.core.getStatusPage(error.status);
91
+ const statusPage = await this.pipeline.getStatusPage(error.status);
96
92
  if (statusPage) {
97
93
  try {
98
94
  const ri: RouteInfo = { url, params: {} };
@@ -100,10 +96,10 @@ export abstract class SsrRenderer {
100
96
  return {
101
97
  content: this.stripSlots(result.content),
102
98
  status: error.status,
103
- title: result.title,
99
+ ...(result.title !== undefined ? { title: result.title } : {}),
104
100
  };
105
101
  } catch (e) {
106
- logger.error(
102
+ this.logger.error(
107
103
  `[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`,
108
104
  e instanceof Error ? e : undefined,
109
105
  );
@@ -111,18 +107,18 @@ export abstract class SsrRenderer {
111
107
  }
112
108
  return { content: this.renderStatusPage(error.status, url), status: error.status };
113
109
  }
114
- logger.error(
110
+ this.logger.error(
115
111
  `[${this.label}] Error rendering ${url.pathname}:`,
116
112
  error instanceof Error ? error : undefined,
117
113
  );
118
114
 
119
- const boundary = this.core.findErrorBoundary(url.pathname);
115
+ const boundary = await this.pipeline.findErrorBoundary(url.pathname);
120
116
  if (boundary) {
121
117
  const result = await this.tryRenderErrorModule(boundary.modulePath, url, 'boundary');
122
118
  if (result) return result;
123
119
  }
124
120
 
125
- const errorHandler = this.core.getErrorHandler();
121
+ const errorHandler = await this.pipeline.getErrorHandler();
126
122
  if (errorHandler) {
127
123
  const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, 'handler');
128
124
  if (result) return result;
@@ -140,13 +136,12 @@ export abstract class SsrRenderer {
140
136
  matched: MatchedRoute,
141
137
  signal?: AbortSignal,
142
138
  ): Promise<{ content: string; title?: string }> {
143
- const hierarchy = this.core.buildRouteHierarchy(matched.route.pattern);
139
+ const hierarchy = this.pipeline.buildRouteHierarchy(matched.route.pattern);
144
140
 
145
- // Resolve routes for each hierarchy segment (skip missing / duplicate wildcard)
146
141
  const segments: { route: RouteConfig; isLeaf: boolean }[] = [];
147
142
  for (let i = 0; i < hierarchy.length; i++) {
148
143
  const routePattern = hierarchy[i];
149
- let route = this.core.findRoute(routePattern);
144
+ let route = await this.pipeline.findRoute(routePattern);
150
145
 
151
146
  if (!route && routePattern === '/') {
152
147
  route = DEFAULT_ROOT_ROUTE;
@@ -158,14 +153,12 @@ export abstract class SsrRenderer {
158
153
  segments.push({ route, isLeaf: i === hierarchy.length - 1 });
159
154
  }
160
155
 
161
- // Fire all renderRouteContent calls in parallel
162
156
  const results = await Promise.all(
163
157
  segments.map(({ route, isLeaf }) =>
164
158
  this.renderRouteContent(routeInfo, route, isLeaf, signal),
165
159
  ),
166
160
  );
167
161
 
168
- // Sequential slot injection
169
162
  let result = '';
170
163
  let pageTitle: string | undefined;
171
164
  let lastRenderedPattern = '';
@@ -182,7 +175,7 @@ export abstract class SsrRenderer {
182
175
  } else {
183
176
  const injected = this.injectSlot(result, content, lastRenderedPattern);
184
177
  if (injected === result) {
185
- logger.warn(
178
+ this.logger.warn(
186
179
  `[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> ` +
187
180
  `for child route "${hierarchy[i]}" to render into. ` +
188
181
  `Add <router-slot></router-slot> to the parent template.`,
@@ -196,7 +189,7 @@ export abstract class SsrRenderer {
196
189
 
197
190
  result = this.stripSlots(result);
198
191
 
199
- return { content: result, title: pageTitle };
192
+ return { content: result, ...(pageTitle !== undefined ? { title: pageTitle } : {}) };
200
193
  }
201
194
 
202
195
  protected abstract renderRouteContent(
@@ -216,25 +209,24 @@ export abstract class SsrRenderer {
216
209
  const files = route.files ?? {};
217
210
 
218
211
  const tsModule = files.ts ?? files.js;
219
- const component: PageComponent = tsModule
220
- ? (await this.core.loadModule<{ default: PageComponent }>(tsModule)).default
221
- : defaultPageComponent;
212
+ const loadedModule = tsModule
213
+ ? await this.pipeline.loadModule<{ default: PageComponent }>(tsModule)
214
+ : undefined;
215
+ const component: PageComponent = loadedModule?.default ?? defaultPageComponent;
222
216
 
223
- const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
224
- const data = await component.getData({ params: routeInfo.params, signal, context });
217
+ const context = await this.pipeline.buildContext(routeInfo, route, signal, isLeaf, loadedModule);
218
+ const data = await component.getData({ params: routeInfo.params, ...(signal ? { signal } : {}), context });
225
219
  const content = this.renderContent(component, { data, params: routeInfo.params, context });
226
220
  const title = component.getTitle({ data, params: routeInfo.params, context });
227
221
 
228
- return { content, title };
222
+ return { content, ...(title !== undefined ? { title } : {}) };
229
223
  }
230
224
 
231
- /** Render a component to the output format (HTML or Markdown). */
232
225
  protected abstract renderContent(
233
226
  component: PageComponent,
234
227
  args: PageComponent['RenderArgs'],
235
228
  ): string;
236
229
 
237
- /** Render a component for error boundary/handler with minimal context. */
238
230
  protected renderComponent(
239
231
  component: PageComponent,
240
232
  data: unknown,
@@ -245,14 +237,13 @@ export abstract class SsrRenderer {
245
237
 
246
238
  private static readonly EMPTY_URL = new URL('http://error');
247
239
 
248
- /** Try to load and render an error boundary or handler module. Returns null on failure. */
249
240
  private async tryRenderErrorModule(
250
241
  modulePath: string,
251
242
  url: URL,
252
243
  kind: 'boundary' | 'handler',
253
244
  ): Promise<{ content: string; status: number } | null> {
254
245
  try {
255
- const module = await this.core.loadModule<{ default: PageComponent }>(modulePath);
246
+ const module = await this.pipeline.loadModule<{ default: PageComponent }>(modulePath);
256
247
  const component = module.default;
257
248
  const minCtx: ComponentContext = {
258
249
  url: SsrRenderer.EMPTY_URL,
@@ -264,7 +255,7 @@ export abstract class SsrRenderer {
264
255
  const content = this.renderComponent(component, data, minCtx);
265
256
  return { content, status: 500 };
266
257
  } catch (e) {
267
- logger.error(
258
+ this.logger.error(
268
259
  `[${this.label}] Error ${kind} failed for ${url.pathname}`,
269
260
  e instanceof Error ? e : undefined,
270
261
  );
@@ -278,9 +269,7 @@ export abstract class SsrRenderer {
278
269
 
279
270
  protected abstract renderErrorPage(error: unknown, url: URL): string;
280
271
 
281
- /** Inject child content into the slot owned by parentPattern. */
282
272
  protected abstract injectSlot(parent: string, child: string, parentPattern: string): string;
283
273
 
284
- /** Strip all unconsumed slot placeholders from the final result. */
285
274
  protected abstract stripSlots(result: string): string;
286
275
  }
@@ -5,29 +5,20 @@
5
5
  * provides O(depth) matching, error boundary lookup, and hierarchy traversal.
6
6
  *
7
7
  * Implementations: RouteTrie (in-memory trie from RouteNode tree).
8
- * RouteCore depends on this interface, not on the algorithm.
9
8
  */
10
9
 
11
10
  import type { RouteNode } from '../type/route-tree.type.ts';
12
11
 
13
12
  /** Result of matching a URL pathname against the route tree. */
14
13
  export interface ResolvedRoute {
15
- /** The matched route node. */
16
14
  readonly node: RouteNode;
17
- /** URL pattern reconstructed from the tree path (e.g. "/projects/:id"). */
18
15
  readonly pattern: string;
19
- /** Extracted URL parameters (e.g. { id: "42" }). */
20
16
  readonly params: Record<string, string>;
21
17
  }
22
18
 
23
- /** Route lookup interface. Decouples matching algorithm from the router. */
19
+ /** Route lookup interface. Decouples matching algorithm from the server. */
24
20
  export interface RouteResolver {
25
- /** Match a URL pathname to a route. */
26
21
  match(pathname: string): ResolvedRoute | undefined;
27
-
28
- /** Find the most specific error boundary for a pathname. */
29
22
  findErrorBoundary(pathname: string): string | undefined;
30
-
31
- /** Look up a route node by its exact pattern (e.g. "/projects/:id"). */
32
23
  findRoute(pattern: string): RouteNode | undefined;
33
24
  }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Route Trie
3
+ *
4
+ * Default RouteResolver implementation. O(depth) route matching over
5
+ * the RouteNode tree.
6
+ *
7
+ * Walks the RouteNode tree directly — no conversion step, no internal state
8
+ * beyond the tree reference. Each URL segment is matched in order:
9
+ * static → dynamic (:param) → wildcard (:rest*). Backtracking handles
10
+ * cases where a dynamic path leads to a dead end but a wildcard at an
11
+ * ancestor would match.
12
+ *
13
+ * Static segment matching is case-sensitive, per RFC 3986.
14
+ */
15
+
16
+ import type { RouteNode } from '../type/route-tree.type.ts';
17
+ import type { ResolvedRoute, RouteResolver } from './route.resolver.ts';
18
+
19
+ /**
20
+ * Default RouteResolver implementation.
21
+ * Walks the RouteNode tree directly — no conversion, no Maps.
22
+ */
23
+ export class RouteTrie implements RouteResolver {
24
+ constructor(private readonly tree: RouteNode) {}
25
+
26
+ match(pathname: string): ResolvedRoute | undefined {
27
+ pathname = this.normalizePath(pathname);
28
+ if (pathname === '/') {
29
+ if (this.tree.files || this.tree.redirect) {
30
+ return { node: this.tree, pattern: '/', params: {} };
31
+ }
32
+ return undefined;
33
+ }
34
+ return this.walk(this.tree, this.splitSegments(pathname), 0, {}, '/');
35
+ }
36
+
37
+ findErrorBoundary(pathname: string): string | undefined {
38
+ pathname = this.normalizePath(pathname);
39
+ if (pathname === '/') return this.tree.errorBoundary;
40
+ return this.walkForBoundary(this.tree, this.splitSegments(pathname), 0, this.tree.errorBoundary);
41
+ }
42
+
43
+ findRoute(pattern: string): RouteNode | undefined {
44
+ if (pattern === '/') {
45
+ return (this.tree.files || this.tree.redirect) ? this.tree : undefined;
46
+ }
47
+ const segments = this.splitSegments(pattern);
48
+ let node = this.tree;
49
+ for (const segment of segments) {
50
+ let child: RouteNode | undefined;
51
+ if (segment.startsWith(':') && segment.endsWith('*')) {
52
+ child = node.wildcard?.child;
53
+ } else if (segment.startsWith(':')) {
54
+ child = node.dynamic?.child;
55
+ } else {
56
+ child = node.children?.[segment];
57
+ }
58
+ if (!child) return undefined;
59
+ node = child;
60
+ }
61
+ return (node.files || node.redirect) ? node : undefined;
62
+ }
63
+
64
+ // ── Private helpers ─────────────────────────────────────────────────
65
+
66
+ private safeDecode(segment: string): string {
67
+ try {
68
+ return decodeURIComponent(segment);
69
+ } catch {
70
+ return segment;
71
+ }
72
+ }
73
+
74
+ private splitSegments(pathname: string): string[] {
75
+ return pathname.substring(1).split('/');
76
+ }
77
+
78
+ private normalizePath(pathname: string): string {
79
+ if (pathname.length > 1 && pathname.endsWith('/')) {
80
+ pathname = pathname.slice(0, -1);
81
+ }
82
+ if (!pathname.startsWith('/')) {
83
+ pathname = '/' + pathname;
84
+ }
85
+ return pathname;
86
+ }
87
+
88
+ private walk(
89
+ node: RouteNode,
90
+ segments: string[],
91
+ index: number,
92
+ params: Record<string, string>,
93
+ pattern: string,
94
+ ): ResolvedRoute | undefined {
95
+ if (index === segments.length) {
96
+ if (node.files || node.redirect) {
97
+ return { node, pattern, params: { ...params } };
98
+ }
99
+ if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
100
+ const wp = pattern === '/' ? `/:${node.wildcard.param}*` : `${pattern}/:${node.wildcard.param}*`;
101
+ return {
102
+ node: node.wildcard.child,
103
+ pattern: wp,
104
+ params: { ...params, [node.wildcard.param]: '' },
105
+ };
106
+ }
107
+ return undefined;
108
+ }
109
+
110
+ const segment = segments[index];
111
+
112
+ // Static
113
+ const staticChild = node.children?.[segment];
114
+ if (staticChild) {
115
+ const childPattern = pattern === '/' ? `/${segment}` : `${pattern}/${segment}`;
116
+ const result = this.walk(staticChild, segments, index + 1, params, childPattern);
117
+ if (result) return result;
118
+ }
119
+
120
+ // Dynamic
121
+ if (node.dynamic) {
122
+ const { param, child } = node.dynamic;
123
+ params[param] = this.safeDecode(segment);
124
+ const childPattern = pattern === '/' ? `/:${param}` : `${pattern}/:${param}`;
125
+ const result = this.walk(child, segments, index + 1, params, childPattern);
126
+ if (result) return result;
127
+ delete params[param];
128
+ }
129
+
130
+ // Wildcard
131
+ if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
132
+ const { param, child } = node.wildcard;
133
+ let rest = this.safeDecode(segments[index]);
134
+ for (let i = index + 1; i < segments.length; i++) {
135
+ rest += '/' + this.safeDecode(segments[i]);
136
+ }
137
+ const wp = pattern === '/' ? `/:${param}*` : `${pattern}/:${param}*`;
138
+ return {
139
+ node: child,
140
+ pattern: wp,
141
+ params: { ...params, [param]: rest },
142
+ };
143
+ }
144
+
145
+ return undefined;
146
+ }
147
+
148
+ private walkForBoundary(
149
+ node: RouteNode,
150
+ segments: string[],
151
+ index: number,
152
+ deepest: string | undefined,
153
+ ): string | undefined {
154
+ if (index === segments.length) {
155
+ return node.errorBoundary ?? deepest;
156
+ }
157
+
158
+ const segment = segments[index];
159
+
160
+ const staticChild = node.children?.[segment];
161
+ if (staticChild) {
162
+ return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
163
+ }
164
+
165
+ if (node.dynamic) {
166
+ return this.walkForBoundary(node.dynamic.child, segments, index + 1, node.dynamic.child.errorBoundary ?? deepest);
167
+ }
168
+
169
+ if (node.wildcard) {
170
+ return node.wildcard.child.errorBoundary ?? deepest;
171
+ }
172
+
173
+ return deepest;
174
+ }
175
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Abstract Runtime
3
+ *
4
+ * Storage contract. Speaks Request/Response.
5
+ * Concrete implementations decide how to store, cache, scan, and serve.
6
+ *
7
+ * Three access patterns:
8
+ * - handle() — raw passthrough
9
+ * - query() — read (Response or string)
10
+ * - command() — write/delete
11
+ */
12
+
13
+ export type FetchParams = Parameters<typeof fetch>;
14
+ export type FetchReturn = ReturnType<typeof fetch>;
15
+
16
+ /** Well-known manifest paths (convention between Runtime and consumers). */
17
+ export const ROUTES_MANIFEST_PATH = '/routes.manifest.json';
18
+ export const WIDGETS_MANIFEST_PATH = '/widgets.manifest.json';
19
+ export const ELEMENTS_MANIFEST_PATH = '/elements.manifest.json';
20
+
21
+ export abstract class Runtime {
22
+ /** Raw passthrough — same signature as fetch(). */
23
+ abstract handle(resource: FetchParams[0], init?: FetchParams[1]): FetchReturn;
24
+
25
+ /** Read. Returns Response, or string with { as: 'text' }. */
26
+ abstract query(
27
+ resource: FetchParams[0],
28
+ options: FetchParams[1] & { as: 'text' },
29
+ ): Promise<string>;
30
+ abstract query(
31
+ resource: FetchParams[0],
32
+ options?: FetchParams[1],
33
+ ): FetchReturn;
34
+
35
+ /** Write (PUT) or delete (DELETE). */
36
+ abstract command(resource: FetchParams[0], options?: FetchParams[1]): FetchReturn;
37
+
38
+ /** Dynamically import a module from storage. */
39
+ loadModule(_path: string): Promise<unknown> {
40
+ throw new Error(`loadModule not implemented for ${this.constructor.name}`);
41
+ }
42
+
43
+ /** Transpile TypeScript to JavaScript. */
44
+ transpile(_source: string): Promise<string> {
45
+ throw new Error(`transpile not implemented for ${this.constructor.name}`);
46
+ }
47
+ }