@emkodev/emroute 1.7.3 → 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 (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 +19 -45
  7. package/{src/renderer/ssr → core/renderer}/md.renderer.ts +19 -39
  8. package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +41 -52
  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 +12 -42
  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 +13 -30
  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 +33 -35
  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 +10 -40
  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 +1077 -1195
  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 +394 -7
  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 +4 -7
  132. package/dist/src/element/component.element.js +6 -19
  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 +418 -10
  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 +8 -27
  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} +18 -19
  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
package/README.md CHANGED
@@ -145,7 +145,7 @@ Pick your runtime: [Bun](doc/01a-setup-bun.md) | [Node](doc/01b-setup-node.md) |
145
145
  - [Routing](doc/04-routing.md) — dynamic segments, catch-all, redirects
146
146
  - [Nesting](doc/05-nesting.md) — layouts, slots, passthrough pages, tips and tricks
147
147
  - [Widgets](doc/06-widgets.md) — interactive islands with data lifecycle
148
- - [Server](doc/07-server.md) — `createEmrouteServer`, composition, static files
148
+ - [Server](doc/07-server.md) — `Emroute.create`, composition, static files
149
149
  - [Markdown renderers](doc/08-markdown-renderer.md) — pluggable parser interface and setup
150
150
  - [Runtime](doc/09-runtime.md) — abstract runtime, UniversalFsRuntime, BunFsRuntime, BunSqliteRuntime
151
151
  - [SPA modes](doc/10-spa-mode.md) — none, leaf, root, only
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Component Base Class
3
+ *
4
+ * Everything is a Component: pages and widgets.
5
+ * Components render differently based on context:
6
+ * - /md/* → Markdown (LLMs, text clients)
7
+ * - /html/* → Pre-rendered HTML (SSR)
8
+ * - SPA → Hydrated custom elements
9
+ */
10
+
11
+ import type { ComponentContext } from '../type/component.type.ts';
12
+ import { escapeHtml } from '../util/html.util.ts';
13
+
14
+ export abstract class Component<
15
+ TParams = unknown,
16
+ TData = unknown,
17
+ TContext extends ComponentContext = ComponentContext,
18
+ > {
19
+ declare readonly DataArgs: {
20
+ params: TParams;
21
+ signal?: AbortSignal;
22
+ context: TContext;
23
+ };
24
+
25
+ declare readonly RenderArgs: {
26
+ data: TData | null;
27
+ params: TParams;
28
+ context: TContext;
29
+ };
30
+
31
+ abstract readonly name: string;
32
+
33
+ /** Host element reference, set by ComponentElement in the browser. */
34
+ element?: HTMLElement | undefined;
35
+
36
+ /** Associated file paths for pre-loaded content (html, md, css). */
37
+ readonly files?: { html?: string; md?: string; css?: string };
38
+
39
+ /**
40
+ * When true, SSR serializes the getData() result into the element's
41
+ * light DOM so the client can access it immediately in hydrate()
42
+ * without re-fetching.
43
+ */
44
+ readonly exposeSsrData?: boolean;
45
+
46
+ abstract getData(args: this['DataArgs']): Promise<TData | null>;
47
+ abstract renderMarkdown(args: this['RenderArgs']): string;
48
+
49
+ renderHTML(args: this['RenderArgs']): string {
50
+ if (args.data === null) {
51
+ return `<div data-component="${this.name}">Loading...</div>`;
52
+ }
53
+ const markdown = this.renderMarkdown({
54
+ data: args.data,
55
+ params: args.params,
56
+ context: args.context,
57
+ });
58
+ return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
59
+ }
60
+
61
+ hydrate?(args: this['RenderArgs']): void;
62
+ destroy?(): void;
63
+ validateParams?(params: TParams): string | undefined;
64
+
65
+ renderError(args: { error: unknown; params: TParams }): string {
66
+ const msg = args.error instanceof Error ? args.error.message : String(args.error);
67
+ return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
68
+ }
69
+
70
+ renderMarkdownError(error: unknown): string {
71
+ const msg = error instanceof Error ? error.message : String(error);
72
+ return `> **Error** (\`${this.name}\`): ${msg}`;
73
+ }
74
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Page Component
3
3
  *
4
- * Page component — params come from URL, context carries file content.
4
+ * Params come from URL, context carries file content.
5
5
  *
6
6
  * Default implementations follow the fallback table:
7
7
  * - renderHTML: html file → md via <mark-down> → <router-slot /> (non-leaf only)
@@ -9,7 +9,8 @@
9
9
  * - getData: no-op (returns null)
10
10
  */
11
11
 
12
- import { Component, type ComponentContext } from './abstract.component.ts';
12
+ import { Component } from './abstract.component.ts';
13
+ import type { ComponentContext } from '../type/component.type.ts';
13
14
  import { escapeHtml } from '../util/html.util.ts';
14
15
 
15
16
  export class PageComponent<
@@ -18,42 +19,14 @@ export class PageComponent<
18
19
  TContext extends ComponentContext = ComponentContext,
19
20
  > extends Component<TParams, TData, TContext> {
20
21
  override readonly name: string = 'page';
21
-
22
- /** Route pattern this page handles (optional — set by subclasses) */
23
22
  readonly pattern?: string;
24
23
 
25
- /**
26
- * Fetch or compute page data. Override in subclasses.
27
- * Default: returns null (no data needed).
28
- *
29
- * @example
30
- * ```ts
31
- * override getData({ params, context }: this['DataArgs']) {
32
- * return fetch(`/api/${params.id}`, { signal: context?.signal });
33
- * }
34
- * ```
35
- */
36
24
  override getData(
37
25
  _args: this['DataArgs'],
38
26
  ): Promise<TData | null> {
39
27
  return Promise.resolve(null);
40
28
  }
41
29
 
42
- /**
43
- * Render page as HTML.
44
- *
45
- * Fallback chain:
46
- * 1. html file content from context
47
- * 2. md file content wrapped in `<mark-down>`
48
- * 3. `<router-slot />` (bare slot for child routes)
49
- *
50
- * @example
51
- * ```ts
52
- * override renderHTML({ data, params, context }: this['RenderArgs']) {
53
- * return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
54
- * }
55
- * ```
56
- */
57
30
  override renderHTML(
58
31
  args: this['RenderArgs'],
59
32
  ): string {
@@ -72,12 +45,6 @@ export class PageComponent<
72
45
  }
73
46
 
74
47
  if (files?.md) {
75
- // HOTFIX: skip external <router-slot> when markdown already defines one
76
- // via ```router-slot fenced block. Without this, SPA mode produces two
77
- // slots with the same pattern — one inside <mark-down> (from the fenced
78
- // block) and one outside (from this suffix). SSR strips empty duplicates
79
- // via stripSlots, but SPA has no equivalent cleanup.
80
- // See: issues/pending/spa-duplicate-router-slot.issue.md
81
48
  const hasSlot = files.md.includes('```router-slot');
82
49
  const slot = args.context.isLeaf || hasSlot ? '' : '\n<router-slot></router-slot>';
83
50
  return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
@@ -86,20 +53,6 @@ export class PageComponent<
86
53
  return args.context.isLeaf ? '' : '<router-slot></router-slot>';
87
54
  }
88
55
 
89
- /**
90
- * Render page as Markdown.
91
- *
92
- * Fallback chain:
93
- * 1. md file content from context
94
- * 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
95
- *
96
- * @example
97
- * ```ts
98
- * override renderMarkdown({ data, params, context }: this['RenderArgs']) {
99
- * return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
100
- * }
101
- * ```
102
- */
103
56
  override renderMarkdown(
104
57
  args: this['RenderArgs'],
105
58
  ): string {
@@ -112,17 +65,6 @@ export class PageComponent<
112
65
  return args.context.isLeaf ? '' : '```router-slot\n```';
113
66
  }
114
67
 
115
- /**
116
- * Page title. Override in subclasses.
117
- * Default: undefined (no title).
118
- *
119
- * @example
120
- * ```ts
121
- * override getTitle({ data, params }: this['RenderArgs']) {
122
- * return `Project ${params.id}`;
123
- * }
124
- * ```
125
- */
126
68
  getTitle(
127
69
  _args: this['RenderArgs'],
128
70
  ): string | undefined {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Widget Component
3
+ *
4
+ * Embeddable unit within page content. Everything reusable that is not
5
+ * a page is a Widget. Widgets render across all contexts (HTML, Markdown, SPA)
6
+ * and are resolved by name via WidgetRegistry.
7
+ *
8
+ * Default rendering fallback chains (parallel to PageComponent):
9
+ * - renderHTML: html file → md file in <mark-down> → base Component default
10
+ * - renderMarkdown: md file → ''
11
+ */
12
+
13
+ import { Component } from './abstract.component.ts';
14
+ import type { ComponentContext } from '../type/component.type.ts';
15
+ import { escapeHtml, scopeWidgetCss } from '../util/html.util.ts';
16
+
17
+ export abstract class WidgetComponent<
18
+ TParams = unknown,
19
+ TData = unknown,
20
+ TContext extends ComponentContext = ComponentContext,
21
+ > extends Component<TParams, TData, TContext> {
22
+ override renderHTML(
23
+ args: this['RenderArgs'],
24
+ ): string {
25
+ const files = args.context.files;
26
+ const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>\n` : '';
27
+
28
+ if (files?.html) {
29
+ return style + files.html;
30
+ }
31
+
32
+ if (files?.md) {
33
+ return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>`;
34
+ }
35
+
36
+ if (style) {
37
+ return style + super.renderHTML(args);
38
+ }
39
+
40
+ return super.renderHTML(args);
41
+ }
42
+
43
+ override renderMarkdown(
44
+ args: this['RenderArgs'],
45
+ ): string {
46
+ const files = args.context.files;
47
+
48
+ if (files?.md) {
49
+ return files.md;
50
+ }
51
+
52
+ return '';
53
+ }
54
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Pipeline
3
+ *
4
+ * Orchestration layer between Router, Runtime, and Component.
5
+ *
6
+ * Owns:
7
+ * - Route matching (reads manifest from Runtime, walks RouteNode tree)
8
+ * - Module loading (delegates to Runtime)
9
+ * - Companion file reading (delegates to Runtime)
10
+ * - ComponentContext building
11
+ * - Route hierarchy construction
12
+ *
13
+ * Does NOT own:
14
+ * - Rendering (that's Renderer)
15
+ * - HTTP routing / base paths (that's Server)
16
+ * - Storage I/O (that's Runtime)
17
+ * - Navigation events (that's the browser adapter)
18
+ */
19
+
20
+ import { RouteTrie } from '../router/route.trie.ts';
21
+ import type { RouteNode } from '../type/route-tree.type.ts';
22
+ import type { RouteConfig, MatchedRoute, RouteInfo } from '../type/route.type.ts';
23
+ import type { ComponentContext, ContextProvider, FileContents } from '../type/component.type.ts';
24
+ import type { Runtime } from '../runtime/abstract.runtime.ts';
25
+ import { ROUTES_MANIFEST_PATH } from '../runtime/abstract.runtime.ts';
26
+ import { type Logger, defaultLogger } from '../type/logger.type.ts';
27
+
28
+ /** Default root route — renders a slot for child routes. */
29
+ export const DEFAULT_ROOT_ROUTE: RouteConfig = {
30
+ pattern: '/',
31
+ type: 'page',
32
+ modulePath: '__default_root__',
33
+ };
34
+
35
+ /** Synthesize a RouteConfig from matched route data. */
36
+ function toRouteConfig(node: RouteNode, pattern: string): RouteConfig {
37
+ return {
38
+ pattern,
39
+ type: node.redirect ? 'redirect' : 'page',
40
+ modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? '',
41
+ ...(node.files ? { files: node.files } : {}),
42
+ };
43
+ }
44
+
45
+ /** Pipeline configuration. */
46
+ export interface PipelineOptions {
47
+ runtime: Runtime;
48
+ contextProvider?: ContextProvider;
49
+ /** Pre-bundled module loaders (browser boot passes these). */
50
+ moduleLoaders?: Record<string, () => Promise<unknown>>;
51
+ logger?: Logger;
52
+ }
53
+
54
+ export class Pipeline {
55
+ private readonly runtime: Runtime;
56
+ readonly contextProvider: ContextProvider | undefined;
57
+ readonly logger: Logger;
58
+ private readonly moduleLoaders: Record<string, () => Promise<unknown>>;
59
+
60
+ constructor(options: PipelineOptions) {
61
+ this.runtime = options.runtime;
62
+ this.contextProvider = options.contextProvider;
63
+ this.logger = options.logger ?? defaultLogger;
64
+ this.moduleLoaders = options.moduleLoaders ?? {};
65
+ }
66
+
67
+ // ── Route resolver (from Runtime) ───────────────────────────────────
68
+
69
+ private async getResolver(): Promise<RouteTrie> {
70
+ const response = await this.runtime.query(ROUTES_MANIFEST_PATH);
71
+ const tree: RouteNode = response.status === 404 ? {} : await response.json();
72
+ return new RouteTrie(tree);
73
+ }
74
+
75
+ // ── Matching ────────────────────────────────────────────────────────
76
+
77
+ async match(url: URL): Promise<MatchedRoute | undefined> {
78
+ const resolver = await this.getResolver();
79
+ const resolved = resolver.match(url.pathname);
80
+ if (resolved) {
81
+ return { route: toRouteConfig(resolved.node, resolved.pattern), params: resolved.params };
82
+ }
83
+ if (url.pathname === '/' || url.pathname === '') {
84
+ return { route: DEFAULT_ROOT_ROUTE, params: {} };
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ async findRoute(pattern: string): Promise<RouteConfig | undefined> {
90
+ const resolver = await this.getResolver();
91
+ const node = resolver.findRoute(pattern);
92
+ if (!node) return undefined;
93
+ return toRouteConfig(node, pattern);
94
+ }
95
+
96
+ async findErrorBoundary(pathname: string): Promise<{ pattern: string; modulePath: string } | undefined> {
97
+ const resolver = await this.getResolver();
98
+ const modulePath = resolver.findErrorBoundary(pathname);
99
+ if (!modulePath) return undefined;
100
+ return { pattern: pathname, modulePath };
101
+ }
102
+
103
+ async getStatusPage(status: number): Promise<RouteConfig | undefined> {
104
+ const resolver = await this.getResolver();
105
+ const node = resolver.findRoute(`/${status}`);
106
+ if (!node) return undefined;
107
+ return toRouteConfig(node, `/${status}`);
108
+ }
109
+
110
+ async getErrorHandler(): Promise<RouteConfig | undefined> {
111
+ const resolver = await this.getResolver();
112
+ const modulePath = resolver.findErrorBoundary('/');
113
+ if (!modulePath) return undefined;
114
+ return { pattern: '/', type: 'error', modulePath };
115
+ }
116
+
117
+ // ── Route hierarchy ────────────────────────────────────────────────
118
+
119
+ buildRouteHierarchy(pattern: string): string[] {
120
+ if (pattern === '/') return ['/'];
121
+ const segments = pattern.split('/').filter(Boolean);
122
+ const hierarchy: string[] = ['/'];
123
+ let current = '';
124
+ for (const segment of segments) {
125
+ current += '/' + segment;
126
+ hierarchy.push(current);
127
+ }
128
+ return hierarchy;
129
+ }
130
+
131
+ // ── Module loading ─────────────────────────────────────────────────
132
+
133
+ async loadModule<T>(modulePath: string): Promise<T> {
134
+ const loader = this.moduleLoaders[modulePath];
135
+ if (loader) {
136
+ return await loader() as T;
137
+ }
138
+ const abs = modulePath.startsWith('/') ? modulePath : '/' + modulePath;
139
+ return await this.runtime.loadModule(abs) as T;
140
+ }
141
+
142
+ /**
143
+ * Get inlined `__files` from a loaded module (merged module pattern).
144
+ */
145
+ getModuleFiles(mod: unknown): FileContents | undefined {
146
+ if (!mod || typeof mod !== 'object') return undefined;
147
+ const files = (mod as Record<string, unknown>).__files;
148
+ if (!files || typeof files !== 'object') return undefined;
149
+ return files as FileContents;
150
+ }
151
+
152
+ // ── File loading ───────────────────────────────────────────────────
153
+
154
+ async loadFiles(
155
+ files: { html?: string; md?: string; css?: string },
156
+ ): Promise<FileContents> {
157
+ const load = async (path: string): Promise<string | undefined> => {
158
+ const abs = path.startsWith('/') ? path : '/' + path;
159
+ try {
160
+ return await this.runtime.query(abs, { as: 'text' });
161
+ } catch (e) {
162
+ console.warn(
163
+ `[Pipeline] Failed to load file ${path}:`,
164
+ e instanceof Error ? e.message : e,
165
+ );
166
+ return undefined;
167
+ }
168
+ };
169
+
170
+ const [html, md, css] = await Promise.all([
171
+ files.html ? load(files.html) : undefined,
172
+ files.md ? load(files.md) : undefined,
173
+ files.css ? load(files.css) : undefined,
174
+ ]);
175
+
176
+ const result: FileContents = {};
177
+ if (html !== undefined) result.html = html;
178
+ if (md !== undefined) result.md = md;
179
+ if (css !== undefined) result.css = css;
180
+ return result;
181
+ }
182
+
183
+ // ── Context building ───────────────────────────────────────────────
184
+
185
+ toRouteInfo(matched: MatchedRoute, url: URL): RouteInfo {
186
+ return { url, params: matched.params };
187
+ }
188
+
189
+ async buildContext(
190
+ routeInfo: RouteInfo,
191
+ route: RouteConfig,
192
+ signal?: AbortSignal,
193
+ isLeaf?: boolean,
194
+ loadedModule?: unknown,
195
+ ): Promise<ComponentContext> {
196
+ const rf = route.files;
197
+
198
+ // Try inlined __files from merged module first
199
+ const inlined = loadedModule ? this.getModuleFiles(loadedModule) : undefined;
200
+
201
+ let files: FileContents;
202
+ if (inlined) {
203
+ files = inlined;
204
+ } else if (rf) {
205
+ const filePaths: { html?: string; md?: string; css?: string } = {};
206
+ if (rf.html) filePaths.html = rf.html;
207
+ if (rf.md) filePaths.md = rf.md;
208
+ if (rf.css) filePaths.css = rf.css;
209
+ files = await this.loadFiles(filePaths);
210
+ } else {
211
+ files = {};
212
+ }
213
+
214
+ const base: ComponentContext = {
215
+ ...routeInfo,
216
+ pathname: routeInfo.url.pathname,
217
+ searchParams: routeInfo.url.searchParams,
218
+ files,
219
+ ...(signal ? { signal } : {}),
220
+ ...(isLeaf !== undefined ? { isLeaf } : {}),
221
+ };
222
+ return this.contextProvider ? this.contextProvider(base) : base;
223
+ }
224
+ }
@@ -6,31 +6,27 @@
6
6
  * Expands <mark-down> tags server-side when a markdown renderer is provided.
7
7
  */
8
8
 
9
- import type { RouteConfig, RouteInfo } from '../../type/route.type.ts';
10
- import type { RouteResolver } from '../../route/route.resolver.ts';
11
- import type { MarkdownRenderer } from '../../type/markdown.type.ts';
12
- import type { PageComponent } from '../../component/page.component.ts';
13
- import { DEFAULT_ROOT_ROUTE } from '../../route/route.core.ts';
14
- import { escapeHtml, STATUS_MESSAGES, unescapeHtml } from '../../util/html.util.ts';
15
- import { resolveWidgetTags } from '../../util/widget-resolve.util.ts';
9
+ import type { RouteConfig, RouteInfo } from '../type/route.type.ts';
10
+ import type { MarkdownRenderer } from '../type/markdown.type.ts';
11
+ import type { PageComponent } from '../component/page.component.ts';
12
+ import type { Pipeline } from '../pipeline/pipeline.ts';
13
+ import { DEFAULT_ROOT_ROUTE } from '../pipeline/pipeline.ts';
14
+ import { escapeHtml, STATUS_MESSAGES, unescapeHtml } from '../util/html.util.ts';
15
+ import { resolveWidgetTags } from '../util/widget-resolve.util.ts';
16
16
  import { SsrRenderer, type SsrRendererOptions } from './ssr.renderer.ts';
17
17
 
18
- /** Options for SSR HTML Router */
19
- export interface SsrHtmlRouterOptions extends SsrRendererOptions {
20
- /** Markdown renderer for server-side <mark-down> expansion */
18
+ /** Options for SSR HTML Renderer. */
19
+ export interface SsrHtmlRendererOptions extends SsrRendererOptions {
21
20
  markdownRenderer?: MarkdownRenderer;
22
21
  }
23
22
 
24
- /**
25
- * SSR HTML Router for server-side rendering.
26
- */
27
- export class SsrHtmlRouter extends SsrRenderer {
23
+ export class SsrHtmlRenderer extends SsrRenderer {
28
24
  protected override readonly label = 'SSR HTML';
29
25
  private markdownRenderer: MarkdownRenderer | null;
30
26
  private markdownReady: Promise<void> | null = null;
31
27
 
32
- constructor(resolver: RouteResolver, options: SsrHtmlRouterOptions = {}) {
33
- super(resolver, options);
28
+ constructor(pipeline: Pipeline, options: SsrHtmlRendererOptions = {}) {
29
+ super(pipeline, options);
34
30
  this.markdownRenderer = options.markdownRenderer ?? null;
35
31
 
36
32
  if (this.markdownRenderer?.init) {
@@ -50,9 +46,6 @@ export class SsrHtmlRouter extends SsrRenderer {
50
46
  return result.replace(/<router-slot[^>]*><\/router-slot>/g, '');
51
47
  }
52
48
 
53
- /**
54
- * Render a single route's content.
55
- */
56
49
  protected override async renderRouteContent(
57
50
  routeInfo: RouteInfo,
58
51
  route: RouteConfig,
@@ -66,14 +59,12 @@ export class SsrHtmlRouter extends SsrRenderer {
66
59
  const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
67
60
  let content = rawContent;
68
61
 
69
- // Expand <mark-down> tags server-side
70
62
  content = await this.expandMarkdown(content);
71
63
 
72
- // Attribute bare <router-slot> tags with this route's pattern (before widget
73
- // resolution so widget-internal slots inside <template> are not affected)
64
+ // Attribute bare <router-slot> tags with this route's pattern
74
65
  content = this.attributeSlots(content, route.pattern);
75
66
 
76
- // Resolve <widget-*> tags: call getData() + renderHTML(), inject ssr attribute
67
+ // Resolve <widget-*> tags
77
68
  if (this.widgets) {
78
69
  content = await resolveWidgetTags(
79
70
  content,
@@ -81,13 +72,14 @@ export class SsrHtmlRouter extends SsrRenderer {
81
72
  routeInfo,
82
73
  (name, declared) => {
83
74
  const files = this.widgetFiles[name] ?? declared;
84
- return files ? this.core.loadWidgetFiles(files) : Promise.resolve({});
75
+ return files ? this.pipeline.loadFiles(files) : Promise.resolve({});
85
76
  },
86
- this.core.contextProvider,
77
+ this.pipeline.contextProvider,
78
+ this.logger,
87
79
  );
88
80
  }
89
81
 
90
- return { content, ...(title != null ? { title } : {}) };
82
+ return { content, ...(title !== undefined ? { title } : {}) };
91
83
  }
92
84
 
93
85
  protected override renderContent(
@@ -117,7 +109,6 @@ export class SsrHtmlRouter extends SsrRenderer {
117
109
  `;
118
110
  }
119
111
 
120
- /** Add pattern attribute to bare <router-slot> tags. */
121
112
  private attributeSlots(content: string, routePattern: string): string {
122
113
  return content.replace(
123
114
  /<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g,
@@ -125,10 +116,6 @@ export class SsrHtmlRouter extends SsrRenderer {
125
116
  );
126
117
  }
127
118
 
128
- /**
129
- * Expand <mark-down> tags by rendering markdown to HTML server-side.
130
- * Leaves content unchanged if no markdown renderer is configured.
131
- */
132
119
  private async expandMarkdown(content: string): Promise<string> {
133
120
  if (!this.markdownRenderer) return content;
134
121
  if (!content.includes('<mark-down>')) return content;
@@ -138,24 +125,11 @@ export class SsrHtmlRouter extends SsrRenderer {
138
125
  }
139
126
 
140
127
  const renderer = this.markdownRenderer;
141
-
142
- // Match <mark-down>escaped content</mark-down>
143
128
  const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
144
129
 
145
130
  return content.replace(pattern, (_match, escaped: string) => {
146
131
  const markdown = unescapeHtml(escaped);
147
- const rendered = renderer.render(markdown);
148
- return rendered;
132
+ return renderer.render(markdown);
149
133
  });
150
134
  }
151
135
  }
152
-
153
- /**
154
- * Create SSR HTML router.
155
- */
156
- export function createSsrHtmlRouter(
157
- resolver: RouteResolver,
158
- options?: SsrHtmlRouterOptions,
159
- ): SsrHtmlRouter {
160
- return new SsrHtmlRouter(resolver, options);
161
- }