@emkodev/emroute 1.7.3 → 1.8.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) 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 +26 -47
  7. package/{src/renderer/ssr → core/renderer}/md.renderer.ts +22 -41
  8. package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +44 -58
  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 +324 -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 +3 -5
  22. package/{src/route → core/util}/route-tree.util.ts +0 -2
  23. package/{src → core}/util/widget-resolve.util.ts +15 -46
  24. package/{src → core}/widget/widget.parser.ts +2 -23
  25. package/core/widget/widget.registry.ts +36 -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 +20 -35
  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 +16 -32
  43. package/dist/core/renderer/md.renderer.js.map +1 -0
  44. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -27
  45. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +33 -37
  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 +239 -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 +28 -0
  92. package/dist/{src → core}/util/widget-resolve.util.js +12 -42
  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 +1 -22
  96. package/dist/core/widget/widget.parser.js.map +1 -0
  97. package/dist/core/widget/widget.registry.d.ts +14 -0
  98. package/dist/core/widget/widget.registry.js +26 -0
  99. package/dist/core/widget/widget.registry.js.map +1 -0
  100. package/dist/emroute.js +1092 -1220
  101. package/dist/emroute.js.map +36 -5
  102. package/dist/runtime/abstract.runtime.d.ts +41 -7
  103. package/dist/runtime/abstract.runtime.js +404 -9
  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 +15 -1
  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 +8 -0
  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 +15 -1
  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 +11 -31
  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 -351
  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 -71
  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 +6 -14
  132. package/dist/src/element/component.element.js +13 -40
  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 +3 -2
  136. package/dist/src/element/markdown.element.js.map +1 -1
  137. package/dist/src/index.d.ts +15 -14
  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 +7 -7
  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/util/html.util.d.ts +5 -22
  151. package/dist/src/util/html.util.js +8 -56
  152. package/dist/src/util/html.util.js.map +1 -1
  153. package/dist/src/widget/breadcrumb.widget.d.ts +2 -2
  154. package/dist/src/widget/breadcrumb.widget.js +2 -2
  155. package/dist/src/widget/breadcrumb.widget.js.map +1 -1
  156. package/dist/src/widget/page-title.widget.d.ts +1 -1
  157. package/dist/src/widget/page-title.widget.js +1 -1
  158. package/dist/src/widget/page-title.widget.js.map +1 -1
  159. package/package.json +8 -8
  160. package/runtime/abstract.runtime.ts +433 -17
  161. package/runtime/bun/fs/bun-fs.runtime.ts +15 -1
  162. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +9 -0
  163. package/runtime/fetch.runtime.ts +3 -3
  164. package/runtime/sitemap.generator.ts +2 -2
  165. package/runtime/universal/fs/universal-fs.runtime.ts +15 -1
  166. package/server/build.util.ts +17 -43
  167. package/server/codegen.util.ts +1 -1
  168. package/server/emroute.server.ts +12 -426
  169. package/src/element/component.element.ts +14 -54
  170. package/src/element/markdown.element.ts +4 -3
  171. package/src/index.ts +22 -19
  172. package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +19 -20
  173. package/src/renderer/spa/mod.ts +22 -22
  174. package/src/util/html.util.ts +16 -61
  175. package/src/widget/breadcrumb.widget.ts +3 -3
  176. package/src/widget/page-title.widget.ts +1 -1
  177. package/dist/src/component/abstract.component.d.ts +0 -199
  178. package/dist/src/component/abstract.component.js +0 -84
  179. package/dist/src/component/abstract.component.js.map +0 -1
  180. package/dist/src/component/page.component.d.ts +0 -74
  181. package/dist/src/component/page.component.js +0 -107
  182. package/dist/src/component/page.component.js.map +0 -1
  183. package/dist/src/component/widget.component.d.ts +0 -47
  184. package/dist/src/component/widget.component.js +0 -69
  185. package/dist/src/component/widget.component.js.map +0 -1
  186. package/dist/src/renderer/ssr/html.renderer.js.map +0 -1
  187. package/dist/src/renderer/ssr/md.renderer.js.map +0 -1
  188. package/dist/src/renderer/ssr/ssr.renderer.js.map +0 -1
  189. package/dist/src/route/route-tree.util.js.map +0 -1
  190. package/dist/src/route/route.matcher.d.ts +0 -86
  191. package/dist/src/route/route.matcher.js +0 -214
  192. package/dist/src/route/route.matcher.js.map +0 -1
  193. package/dist/src/route/route.resolver.js.map +0 -1
  194. package/dist/src/route/route.trie.d.ts +0 -38
  195. package/dist/src/route/route.trie.js +0 -206
  196. package/dist/src/route/route.trie.js.map +0 -1
  197. package/dist/src/type/element.type.d.ts +0 -19
  198. package/dist/src/type/element.type.js +0 -9
  199. package/dist/src/type/element.type.js.map +0 -1
  200. package/dist/src/type/logger.type.d.ts +0 -17
  201. package/dist/src/type/logger.type.js +0 -9
  202. package/dist/src/type/logger.type.js.map +0 -1
  203. package/dist/src/type/markdown.type.d.ts +0 -20
  204. package/dist/src/type/markdown.type.js +0 -2
  205. package/dist/src/type/markdown.type.js.map +0 -1
  206. package/dist/src/type/route-tree.type.js.map +0 -1
  207. package/dist/src/type/route.type.d.ts +0 -94
  208. package/dist/src/type/route.type.js +0 -8
  209. package/dist/src/type/route.type.js.map +0 -1
  210. package/dist/src/type/widget.type.d.ts +0 -55
  211. package/dist/src/type/widget.type.js +0 -10
  212. package/dist/src/type/widget.type.js.map +0 -1
  213. package/dist/src/util/logger.util.d.ts +0 -26
  214. package/dist/src/util/logger.util.js +0 -80
  215. package/dist/src/util/logger.util.js.map +0 -1
  216. package/dist/src/util/md.util.js.map +0 -1
  217. package/dist/src/util/widget-resolve.util.d.ts +0 -52
  218. package/dist/src/util/widget-resolve.util.js.map +0 -1
  219. package/dist/src/widget/widget.parser.js.map +0 -1
  220. package/dist/src/widget/widget.registry.d.ts +0 -23
  221. package/dist/src/widget/widget.registry.js +0 -42
  222. package/dist/src/widget/widget.registry.js.map +0 -1
  223. package/runtime/bun/esbuild-runtime-loader.plugin.ts +0 -112
  224. package/server/esbuild-manifest.plugin.ts +0 -209
  225. package/server/server-api.type.ts +0 -101
  226. package/src/component/abstract.component.ts +0 -231
  227. package/src/component/widget.component.ts +0 -85
  228. package/src/route/route.core.ts +0 -371
  229. package/src/route/route.trie.ts +0 -265
  230. package/src/type/element.type.ts +0 -22
  231. package/src/type/logger.type.ts +0 -24
  232. package/src/type/markdown.type.ts +0 -21
  233. package/src/type/route-tree.type.ts +0 -51
  234. package/src/type/route.type.ts +0 -124
  235. package/src/type/widget.type.ts +0 -65
  236. package/src/util/logger.util.ts +0 -83
  237. package/src/widget/widget.registry.ts +0 -51
  238. /package/dist/{src/route → core/util}/route-tree.util.d.ts +0 -0
@@ -5,14 +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 { ComponentContext } from '../../component/abstract.component.ts';
10
- import type { RouteResolver } from '../../route/route.resolver.ts';
11
- import type { PageComponent } from '../../component/page.component.ts';
12
- import { DEFAULT_ROOT_ROUTE } from '../../route/route.core.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';
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';
16
16
  import { SsrRenderer, type SsrRendererOptions } from './ssr.renderer.ts';
17
17
 
18
18
  const BARE_SLOT_BLOCK = '```router-slot\n```';
@@ -21,17 +21,13 @@ function routerSlotBlock(pattern: string): string {
21
21
  return `\`\`\`router-slot\n{"pattern":"${pattern}"}\n\`\`\``;
22
22
  }
23
23
 
24
- /** Options for SSR Markdown Router */
25
- export type SsrMdRouterOptions = SsrRendererOptions;
24
+ export type SsrMdRendererOptions = SsrRendererOptions;
26
25
 
27
- /**
28
- * SSR Markdown Router for server-side markdown rendering.
29
- */
30
- export class SsrMdRouter extends SsrRenderer {
26
+ export class SsrMdRenderer extends SsrRenderer {
31
27
  protected override readonly label = 'SSR MD';
32
28
 
33
- constructor(resolver: RouteResolver, options: SsrMdRouterOptions = {}) {
34
- super(resolver, options);
29
+ constructor(pipeline: Pipeline, options: SsrMdRendererOptions = {}) {
30
+ super(pipeline, options);
35
31
  }
36
32
 
37
33
  protected override injectSlot(parent: string, child: string, parentPattern: string): string {
@@ -44,9 +40,6 @@ export class SsrMdRouter extends SsrRenderer {
44
40
  .trim();
45
41
  }
46
42
 
47
- /**
48
- * Render a single route's content to Markdown.
49
- */
50
43
  protected override async renderRouteContent(
51
44
  routeInfo: RouteInfo,
52
45
  route: RouteConfig,
@@ -61,15 +54,14 @@ export class SsrMdRouter extends SsrRenderer {
61
54
  let content = rawContent;
62
55
 
63
56
  // Attribute bare router-slot blocks with this route's pattern
64
- // (before widget resolution so widget-internal blocks are not affected)
65
57
  content = content.replaceAll(BARE_SLOT_BLOCK, routerSlotBlock(route.pattern));
66
58
 
67
- // Resolve fenced widget blocks: call getData() + renderMarkdown()
59
+ // Resolve fenced widget blocks
68
60
  if (this.widgets) {
69
61
  content = await this.resolveWidgets(content, routeInfo);
70
62
  }
71
63
 
72
- return { content, ...(title != null ? { title } : {}) };
64
+ return { content, ...(title !== undefined ? { title } : {}) };
73
65
  }
74
66
 
75
67
  protected override renderContent(
@@ -91,10 +83,6 @@ export class SsrMdRouter extends SsrRenderer {
91
83
  return `# Internal Server Error\n\nPath: \`${url.pathname}\``;
92
84
  }
93
85
 
94
- /**
95
- * Resolve fenced widget blocks in markdown content.
96
- * Replaces ```widget:name blocks with rendered markdown output.
97
- */
98
86
  private resolveWidgets(
99
87
  content: string,
100
88
  routeInfo: RouteInfo,
@@ -114,9 +102,10 @@ export class SsrMdRouter extends SsrRenderer {
114
102
 
115
103
  try {
116
104
  let files: { html?: string; md?: string } | undefined;
117
- const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
118
- if (filePaths) {
119
- files = await this.core.loadWidgetFiles(filePaths);
105
+ const modulePath = this.widgets!.getModulePath(block.widgetName);
106
+ if (modulePath) {
107
+ const mod = await this.pipeline.loadModule(modulePath);
108
+ files = this.pipeline.getModuleFiles(mod);
120
109
  }
121
110
 
122
111
  const baseContext: ComponentContext = {
@@ -125,8 +114,8 @@ export class SsrMdRouter extends SsrRenderer {
125
114
  searchParams: routeInfo.url.searchParams,
126
115
  ...(files ? { files } : {}),
127
116
  };
128
- const context: ComponentContext = this.core.contextProvider
129
- ? this.core.contextProvider(baseContext)
117
+ const context: ComponentContext = this.pipeline.contextProvider
118
+ ? this.pipeline.contextProvider(baseContext)
130
119
  : baseContext;
131
120
  const data = await widget.getData({ params: block.params, context });
132
121
  return widget.renderMarkdown({ data, params: block.params, context });
@@ -135,16 +124,8 @@ export class SsrMdRouter extends SsrRenderer {
135
124
  }
136
125
  },
137
126
  replaceWidgetBlocks,
127
+ 0,
128
+ this.logger,
138
129
  );
139
130
  }
140
131
  }
141
-
142
- /**
143
- * Create SSR Markdown router.
144
- */
145
- export function createSsrMdRouter(
146
- resolver: RouteResolver,
147
- options?: SsrMdRouterOptions,
148
- ): SsrMdRouter {
149
- return new SsrMdRouter(resolver, options);
150
- }
@@ -9,40 +9,33 @@ 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
- widgetFiles?: Record<string, { html?: string; md?: string; css?: string }>;
31
23
  }
32
24
 
33
25
  /**
34
26
  * Abstract SSR renderer with shared routing pipeline.
35
27
  */
36
28
  export abstract class SsrRenderer {
37
- protected core: RouteCore;
29
+ protected readonly pipeline: Pipeline;
38
30
  protected widgets: WidgetRegistry | null;
39
- protected widgetFiles: Record<string, { html?: string; md?: string; css?: string }>;
40
31
  protected abstract readonly label: string;
41
32
 
42
- constructor(resolver: RouteResolver, options: SsrRendererOptions = {}) {
43
- this.core = new RouteCore(resolver, options);
33
+ protected readonly logger: Logger;
34
+
35
+ constructor(pipeline: Pipeline, options: SsrRendererOptions = {}) {
36
+ this.pipeline = pipeline;
37
+ this.logger = pipeline.logger;
44
38
  this.widgets = options.widgets ?? null;
45
- this.widgetFiles = options.widgetFiles ?? {};
46
39
  }
47
40
 
48
41
  /**
@@ -52,17 +45,17 @@ export abstract class SsrRenderer {
52
45
  url: URL,
53
46
  signal?: AbortSignal,
54
47
  ): Promise<{ content: string; status: number; title?: string; redirect?: string }> {
55
- const matched = this.core.match(url);
48
+ const matched = await this.pipeline.match(url);
56
49
 
57
50
  if (!matched) {
58
- const statusPage = this.core.getStatusPage(404);
51
+ const statusPage = await this.pipeline.getStatusPage(404);
59
52
  if (statusPage) {
60
53
  try {
61
54
  const ri: RouteInfo = { url, params: {} };
62
55
  const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
63
- return { content: this.stripSlots(result.content), status: 404, ...(result.title != null ? { title: result.title } : {}) };
56
+ return { content: this.stripSlots(result.content), status: 404, ...(result.title !== undefined ? { title: result.title } : {}) };
64
57
  } catch (e) {
65
- logger.error(
58
+ this.logger.error(
66
59
  `[${this.label}] Failed to render 404 status page for ${url.pathname}`,
67
60
  e instanceof Error ? e : undefined,
68
61
  );
@@ -73,7 +66,7 @@ export abstract class SsrRenderer {
73
66
 
74
67
  // Handle redirect
75
68
  if (matched.route.type === 'redirect') {
76
- const module = await this.core.loadModule<{ default: { to: string; status?: number } }>(
69
+ const module = await this.pipeline.loadModule<{ default: { to: string; status?: number } }>(
77
70
  matched.route.modulePath,
78
71
  );
79
72
  const redirectConfig = module.default;
@@ -85,14 +78,14 @@ export abstract class SsrRenderer {
85
78
  };
86
79
  }
87
80
 
88
- const routeInfo = this.core.toRouteInfo(matched, url);
81
+ const routeInfo = this.pipeline.toRouteInfo(matched, url);
89
82
 
90
83
  try {
91
84
  const { content, title } = await this.renderPage(routeInfo, matched, signal);
92
- return { content, status: 200, ...(title != null ? { title } : {}) };
85
+ return { content, status: 200, ...(title !== undefined ? { title } : {}) };
93
86
  } catch (error) {
94
87
  if (error instanceof Response) {
95
- const statusPage = this.core.getStatusPage(error.status);
88
+ const statusPage = await this.pipeline.getStatusPage(error.status);
96
89
  if (statusPage) {
97
90
  try {
98
91
  const ri: RouteInfo = { url, params: {} };
@@ -100,10 +93,10 @@ export abstract class SsrRenderer {
100
93
  return {
101
94
  content: this.stripSlots(result.content),
102
95
  status: error.status,
103
- ...(result.title != null ? { title: result.title } : {}),
96
+ ...(result.title !== undefined ? { title: result.title } : {}),
104
97
  };
105
98
  } catch (e) {
106
- logger.error(
99
+ this.logger.error(
107
100
  `[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`,
108
101
  e instanceof Error ? e : undefined,
109
102
  );
@@ -111,18 +104,18 @@ export abstract class SsrRenderer {
111
104
  }
112
105
  return { content: this.renderStatusPage(error.status, url), status: error.status };
113
106
  }
114
- logger.error(
107
+ this.logger.error(
115
108
  `[${this.label}] Error rendering ${url.pathname}:`,
116
109
  error instanceof Error ? error : undefined,
117
110
  );
118
111
 
119
- const boundary = this.core.findErrorBoundary(url.pathname);
112
+ const boundary = await this.pipeline.findErrorBoundary(url.pathname);
120
113
  if (boundary) {
121
114
  const result = await this.tryRenderErrorModule(boundary.modulePath, url, 'boundary');
122
115
  if (result) return result;
123
116
  }
124
117
 
125
- const errorHandler = this.core.getErrorHandler();
118
+ const errorHandler = await this.pipeline.getErrorHandler();
126
119
  if (errorHandler) {
127
120
  const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, 'handler');
128
121
  if (result) return result;
@@ -140,13 +133,12 @@ export abstract class SsrRenderer {
140
133
  matched: MatchedRoute,
141
134
  signal?: AbortSignal,
142
135
  ): Promise<{ content: string; title?: string }> {
143
- const hierarchy = this.core.buildRouteHierarchy(matched.route.pattern);
136
+ const hierarchy = this.pipeline.buildRouteHierarchy(matched.route.pattern);
144
137
 
145
- // Resolve routes for each hierarchy segment (skip missing / duplicate wildcard)
146
138
  const segments: { route: RouteConfig; isLeaf: boolean }[] = [];
147
139
  for (let i = 0; i < hierarchy.length; i++) {
148
- const routePattern = hierarchy[i];
149
- let route = this.core.findRoute(routePattern);
140
+ const routePattern = hierarchy[i]!;
141
+ let route = await this.pipeline.findRoute(routePattern);
150
142
 
151
143
  if (!route && routePattern === '/') {
152
144
  route = DEFAULT_ROOT_ROUTE;
@@ -158,20 +150,18 @@ export abstract class SsrRenderer {
158
150
  segments.push({ route, isLeaf: i === hierarchy.length - 1 });
159
151
  }
160
152
 
161
- // Fire all renderRouteContent calls in parallel
162
153
  const results = await Promise.all(
163
154
  segments.map(({ route, isLeaf }) =>
164
155
  this.renderRouteContent(routeInfo, route, isLeaf, signal),
165
156
  ),
166
157
  );
167
158
 
168
- // Sequential slot injection
169
159
  let result = '';
170
160
  let pageTitle: string | undefined;
171
161
  let lastRenderedPattern = '';
172
162
 
173
163
  for (let i = 0; i < segments.length; i++) {
174
- const { content, title } = results[i];
164
+ const { content, title } = results[i]!;
175
165
 
176
166
  if (title) {
177
167
  pageTitle = title;
@@ -182,7 +172,7 @@ export abstract class SsrRenderer {
182
172
  } else {
183
173
  const injected = this.injectSlot(result, content, lastRenderedPattern);
184
174
  if (injected === result) {
185
- logger.warn(
175
+ this.logger.warn(
186
176
  `[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> ` +
187
177
  `for child route "${hierarchy[i]}" to render into. ` +
188
178
  `Add <router-slot></router-slot> to the parent template.`,
@@ -191,12 +181,12 @@ export abstract class SsrRenderer {
191
181
  result = injected;
192
182
  }
193
183
 
194
- lastRenderedPattern = segments[i].route.pattern;
184
+ lastRenderedPattern = segments[i]!.route.pattern;
195
185
  }
196
186
 
197
187
  result = this.stripSlots(result);
198
188
 
199
- return { content: result, ...(pageTitle != null ? { title: pageTitle } : {}) };
189
+ return { content: result, ...(pageTitle !== undefined ? { title: pageTitle } : {}) };
200
190
  }
201
191
 
202
192
  protected abstract renderRouteContent(
@@ -216,25 +206,24 @@ export abstract class SsrRenderer {
216
206
  const files = route.files ?? {};
217
207
 
218
208
  const tsModule = files.ts ?? files.js;
219
- const component: PageComponent = tsModule
220
- ? (await this.core.loadModule<{ default: PageComponent }>(tsModule)).default
221
- : defaultPageComponent;
209
+ const loadedModule = tsModule
210
+ ? await this.pipeline.loadModule<{ default: PageComponent }>(tsModule)
211
+ : undefined;
212
+ const component: PageComponent = loadedModule?.default ?? defaultPageComponent;
222
213
 
223
- const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
214
+ const context = await this.pipeline.buildContext(routeInfo, route, signal, isLeaf, loadedModule);
224
215
  const data = await component.getData({ params: routeInfo.params, ...(signal ? { signal } : {}), context });
225
216
  const content = this.renderContent(component, { data, params: routeInfo.params, context });
226
217
  const title = component.getTitle({ data, params: routeInfo.params, context });
227
218
 
228
- return { content, ...(title != null ? { title } : {}) };
219
+ return { content, ...(title !== undefined ? { title } : {}) };
229
220
  }
230
221
 
231
- /** Render a component to the output format (HTML or Markdown). */
232
222
  protected abstract renderContent(
233
223
  component: PageComponent,
234
224
  args: PageComponent['RenderArgs'],
235
225
  ): string;
236
226
 
237
- /** Render a component for error boundary/handler with minimal context. */
238
227
  protected renderComponent(
239
228
  component: PageComponent,
240
229
  data: unknown,
@@ -245,14 +234,13 @@ export abstract class SsrRenderer {
245
234
 
246
235
  private static readonly EMPTY_URL = new URL('http://error');
247
236
 
248
- /** Try to load and render an error boundary or handler module. Returns null on failure. */
249
237
  private async tryRenderErrorModule(
250
238
  modulePath: string,
251
239
  url: URL,
252
240
  kind: 'boundary' | 'handler',
253
241
  ): Promise<{ content: string; status: number } | null> {
254
242
  try {
255
- const module = await this.core.loadModule<{ default: PageComponent }>(modulePath);
243
+ const module = await this.pipeline.loadModule<{ default: PageComponent }>(modulePath);
256
244
  const component = module.default;
257
245
  const minCtx: ComponentContext = {
258
246
  url: SsrRenderer.EMPTY_URL,
@@ -264,7 +252,7 @@ export abstract class SsrRenderer {
264
252
  const content = this.renderComponent(component, data, minCtx);
265
253
  return { content, status: 500 };
266
254
  } catch (e) {
267
- logger.error(
255
+ this.logger.error(
268
256
  `[${this.label}] Error ${kind} failed for ${url.pathname}`,
269
257
  e instanceof Error ? e : undefined,
270
258
  );
@@ -278,9 +266,7 @@ export abstract class SsrRenderer {
278
266
 
279
267
  protected abstract renderErrorPage(error: unknown, url: URL): string;
280
268
 
281
- /** Inject child content into the slot owned by parentPattern. */
282
269
  protected abstract injectSlot(parent: string, child: string, parentPattern: string): string;
283
270
 
284
- /** Strip all unconsumed slot placeholders from the final result. */
285
271
  protected abstract stripSlots(result: string): string;
286
272
  }
@@ -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(segment);
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
+ }