@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
package/dist/emroute.js CHANGED
@@ -1,7 +1,32 @@
1
- // dist/src/util/html.util.js
1
+ // dist/core/util/html.util.js
2
2
  var SSR_ATTR = "ssr";
3
3
  var LAZY_ATTR = "lazy";
4
- var SsrShadowRoot = class {
4
+ var BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
5
+ function assertSafeRedirect(url) {
6
+ if (BLOCKED_PROTOCOLS.test(url.trim())) {
7
+ throw new Error(`Unsafe redirect URL blocked: ${url}`);
8
+ }
9
+ }
10
+ function escapeHtml(text) {
11
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("`", "&#96;");
12
+ }
13
+ function unescapeHtml(text) {
14
+ return text.replaceAll("&#96;", "`").replaceAll("&#39;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
15
+ }
16
+ function scopeWidgetCss(css, widgetName) {
17
+ return `@scope (widget-${widgetName}) {
18
+ ${css}
19
+ }`;
20
+ }
21
+ var STATUS_MESSAGES = {
22
+ 401: "Unauthorized",
23
+ 403: "Forbidden",
24
+ 404: "Not Found",
25
+ 500: "Internal Server Error"
26
+ };
27
+
28
+ // dist/src/util/html.util.js
29
+ class SsrShadowRoot {
5
30
  host;
6
31
  _innerHTML = "";
7
32
  constructor(host) {
@@ -16,8 +41,7 @@ var SsrShadowRoot = class {
16
41
  setHTMLUnsafe(html, _options) {
17
42
  this._innerHTML = html;
18
43
  }
19
- append(..._nodes) {
20
- }
44
+ append(..._nodes) {}
21
45
  querySelector(_selector) {
22
46
  return null;
23
47
  }
@@ -30,12 +54,12 @@ var SsrShadowRoot = class {
30
54
  get firstChild() {
31
55
  return null;
32
56
  }
33
- };
34
- var SsrHTMLElement = class {
57
+ }
58
+
59
+ class SsrHTMLElement {
35
60
  _innerHTML = "";
36
61
  _shadowRoot = null;
37
- _attributes = /* @__PURE__ */ new Map();
38
- // Accept any CSS property assignment without error
62
+ _attributes = new Map;
39
63
  style = new Proxy({}, {
40
64
  set(_target, _prop, _value) {
41
65
  return true;
@@ -43,7 +67,7 @@ var SsrHTMLElement = class {
43
67
  get(_target, prop) {
44
68
  if (typeof prop === "string")
45
69
  return "";
46
- return void 0;
70
+ return;
47
71
  }
48
72
  });
49
73
  get innerHTML() {
@@ -90,73 +114,41 @@ var SsrHTMLElement = class {
90
114
  querySelectorAll(_selector) {
91
115
  return [];
92
116
  }
93
- append(..._nodes) {
94
- }
117
+ append(..._nodes) {}
95
118
  appendChild(node) {
96
119
  return node;
97
120
  }
98
- };
99
- var HTMLElementBase = globalThis.HTMLElement ?? SsrHTMLElement;
100
- function escapeHtml(text) {
101
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("`", "&#96;");
102
- }
103
- function unescapeHtml(text) {
104
- return text.replaceAll("&#96;", "`").replaceAll("&#39;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
105
- }
106
- function scopeWidgetCss(css, widgetName) {
107
- return `@scope (widget-${widgetName}) {
108
- ${css}
109
- }`;
110
121
  }
111
- var STATUS_MESSAGES = {
112
- 401: "Unauthorized",
113
- 403: "Forbidden",
114
- 404: "Not Found",
115
- 500: "Internal Server Error"
116
- };
122
+ var HTMLElementBase = globalThis.HTMLElement ?? SsrHTMLElement;
117
123
 
118
124
  // dist/src/element/slot.element.js
119
- var RouterSlot = class extends HTMLElementBase {
120
- };
125
+ class RouterSlot extends HTMLElementBase {
126
+ }
121
127
 
122
128
  // dist/src/element/markdown.element.js
123
- var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
129
+ class MarkdownElement extends HTMLElementBase {
124
130
  static renderer = null;
125
131
  static rendererInitPromise = null;
126
132
  abortController = null;
127
- /**
128
- * Set the markdown renderer.
129
- * Must be called before any <mark-down> elements are connected.
130
- *
131
- * @example
132
- * ```ts
133
- * import { createEmkoRenderer } from './emko.renderer.ts';
134
- * MarkdownElement.setRenderer(await createEmkoRenderer());
135
- * ```
136
- */
137
133
  static setRenderer(renderer) {
138
- _MarkdownElement.renderer = renderer;
139
- _MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
134
+ MarkdownElement.renderer = renderer;
135
+ MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
140
136
  }
141
- /** Get the current renderer (if set). Used by bootEmrouteApp to pass through to createEmrouteServer. */
142
137
  static getConfiguredRenderer() {
143
- return _MarkdownElement.renderer;
138
+ return MarkdownElement.renderer;
144
139
  }
145
- /**
146
- * Get the current renderer, waiting for init if needed.
147
- */
148
140
  static async getRenderer() {
149
- const renderer = _MarkdownElement.renderer;
141
+ const renderer = MarkdownElement.renderer;
150
142
  if (!renderer) {
151
143
  throw new Error("No markdown renderer configured. Call MarkdownElement.setRenderer() before using <mark-down> elements.");
152
144
  }
153
- if (_MarkdownElement.rendererInitPromise) {
154
- await _MarkdownElement.rendererInitPromise;
145
+ if (MarkdownElement.rendererInitPromise) {
146
+ await MarkdownElement.rendererInitPromise;
155
147
  }
156
148
  return renderer;
157
149
  }
158
150
  async connectedCallback() {
159
- this.abortController = new AbortController();
151
+ this.abortController = new AbortController;
160
152
  await this.loadContent();
161
153
  }
162
154
  disconnectedCallback() {
@@ -177,7 +169,7 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
177
169
  async loadFromSrc(src) {
178
170
  const signal = this.abortController?.signal;
179
171
  try {
180
- const response = await fetch(src, { signal });
172
+ const response = await fetch(src, signal ? { signal } : {});
181
173
  if (!response.ok) {
182
174
  throw new Error(`Failed to fetch ${src}: ${response.status}`);
183
175
  }
@@ -192,7 +184,7 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
192
184
  }
193
185
  async renderContent(markdown) {
194
186
  try {
195
- const renderer = await _MarkdownElement.getRenderer();
187
+ const renderer = await MarkdownElement.getRenderer();
196
188
  this.innerHTML = renderer.render(markdown);
197
189
  } catch (error) {
198
190
  this.showError(error);
@@ -202,21 +194,26 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
202
194
  const message = error instanceof Error ? error.message : String(error);
203
195
  this.innerHTML = `<div>Markdown Error: ${escapeHtml(message)}</div>`;
204
196
  }
205
- };
197
+ }
206
198
 
207
199
  // dist/src/element/component.element.js
208
- var ComponentElement = class _ComponentElement extends HTMLElementBase {
209
- /** Shared file content cache — deduplicates fetches across all widget instances. */
210
- static fileCache = /* @__PURE__ */ new Map();
211
- /** Lazy module loaders keyed by tag name — set by registerLazy(). */
212
- static lazyLoaders = /* @__PURE__ */ new Map();
213
- /** Cached module promises for lazy-loaded widgets — avoids re-fetching. */
214
- static lazyModules = /* @__PURE__ */ new Map();
215
- /** App-level context provider set once during router initialization. */
200
+ function filterUndefined(obj) {
201
+ const result = {};
202
+ let hasValue = false;
203
+ for (const [k, v] of Object.entries(obj)) {
204
+ if (v !== undefined) {
205
+ result[k] = v;
206
+ hasValue = true;
207
+ }
208
+ }
209
+ return hasValue ? result : undefined;
210
+ }
211
+
212
+ class ComponentElement extends HTMLElementBase {
213
+ static lazyLoaders = new Map;
216
214
  static extendContext;
217
- /** Register (or clear) the context provider that enriches every widget's ComponentContext. */
218
215
  static setContextProvider(provider) {
219
- _ComponentElement.extendContext = provider;
216
+ ComponentElement.extendContext = provider;
220
217
  }
221
218
  component;
222
219
  effectiveFiles;
@@ -228,7 +225,6 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
228
225
  deferred = null;
229
226
  abortController = null;
230
227
  intersectionObserver = null;
231
- /** Promise that resolves with fetched data (available after loadData starts) */
232
228
  dataPromise = null;
233
229
  constructor(component, files) {
234
230
  super();
@@ -238,52 +234,36 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
238
234
  this.attachShadow({ mode: "open" });
239
235
  }
240
236
  }
241
- /**
242
- * Register a widget as a custom element: `widget-{name}`.
243
- * Creates a fresh widget instance per DOM element (per-element state).
244
- * Optional `files` parameter provides discovered file paths without mutating
245
- * the component instance.
246
- */
247
237
  static register(component, files) {
248
238
  const tagName = `widget-${component.name}`;
249
239
  if (!globalThis.customElements || customElements.get(tagName)) {
250
240
  return;
251
241
  }
252
242
  const WidgetClass = component.constructor;
253
- const BoundElement = class extends _ComponentElement {
243
+ const BoundElement = class extends ComponentElement {
254
244
  constructor() {
255
- super(new WidgetClass(), files);
245
+ super(new WidgetClass, files);
256
246
  }
257
247
  };
258
248
  customElements.define(tagName, BoundElement);
259
249
  }
260
- /**
261
- * Register a widget class (not instance) as a custom element: `widget-{name}`.
262
- * Used for manifest-based registration where classes are loaded dynamically.
263
- */
264
250
  static registerClass(WidgetClass, name, files) {
265
251
  const tagName = `widget-${name}`;
266
252
  if (!globalThis.customElements || customElements.get(tagName)) {
267
253
  return;
268
254
  }
269
- const BoundElement = class extends _ComponentElement {
255
+ const BoundElement = class extends ComponentElement {
270
256
  constructor() {
271
- super(new WidgetClass(), files);
257
+ super(new WidgetClass, files);
272
258
  }
273
259
  };
274
260
  customElements.define(tagName, BoundElement);
275
261
  }
276
- /**
277
- * Register a widget lazily: define the custom element immediately (so SSR
278
- * content via Declarative Shadow DOM is adopted), but defer loading the
279
- * module until connectedCallback fires. Once loaded, the real component
280
- * replaces the placeholder and hydration proceeds normally.
281
- */
282
262
  static registerLazy(name, files, loader) {
283
263
  const tagName = `widget-${name}`;
284
264
  if (!globalThis.customElements || customElements.get(tagName))
285
265
  return;
286
- _ComponentElement.lazyLoaders.set(tagName, loader);
266
+ ComponentElement.lazyLoaders.set(tagName, loader);
287
267
  const placeholder = {
288
268
  name,
289
269
  getData: () => Promise.resolve(null),
@@ -292,17 +272,13 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
292
272
  renderError: () => "",
293
273
  renderMarkdownError: () => ""
294
274
  };
295
- const BoundElement = class extends _ComponentElement {
275
+ const BoundElement = class extends ComponentElement {
296
276
  constructor() {
297
277
  super(placeholder, files);
298
278
  }
299
279
  };
300
280
  customElements.define(tagName, BoundElement);
301
281
  }
302
- /**
303
- * Promise that resolves when component is ready (data loaded and rendered).
304
- * Used by router to wait for async components.
305
- */
306
282
  get ready() {
307
283
  if (this.state === "ready") {
308
284
  return Promise.resolve();
@@ -312,23 +288,18 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
312
288
  }
313
289
  async connectedCallback() {
314
290
  const tagName = this.tagName.toLowerCase();
315
- const lazyLoader = _ComponentElement.lazyLoaders.get(tagName);
291
+ const lazyLoader = ComponentElement.lazyLoaders.get(tagName);
316
292
  if (lazyLoader) {
317
293
  try {
318
- let modulePromise = _ComponentElement.lazyModules.get(tagName);
319
- if (!modulePromise) {
320
- modulePromise = lazyLoader();
321
- _ComponentElement.lazyModules.set(tagName, modulePromise);
322
- }
323
- const mod = await modulePromise;
294
+ const mod = await lazyLoader();
324
295
  for (const exp of Object.values(mod)) {
325
296
  if (exp && typeof exp === "object" && "getData" in exp) {
326
297
  const WidgetClass = exp.constructor;
327
- this.component = new WidgetClass();
298
+ this.component = new WidgetClass;
328
299
  break;
329
300
  }
330
301
  if (typeof exp === "function" && exp.prototype?.getData) {
331
- this.component = new exp();
302
+ this.component = new exp;
332
303
  break;
333
304
  }
334
305
  }
@@ -342,7 +313,7 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
342
313
  }
343
314
  this.component.element = this;
344
315
  this.style.contentVisibility = "auto";
345
- this.abortController = new AbortController();
316
+ this.abortController = new AbortController;
346
317
  const signal = this.abortController.signal;
347
318
  const params = {};
348
319
  for (const attr of this.attributes) {
@@ -367,22 +338,22 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
367
338
  if (signal.aborted)
368
339
  return;
369
340
  const currentUrl = globalThis.location ? new URL(location.href) : new URL("http://localhost/");
341
+ const filteredFiles = filterUndefined(files);
370
342
  const base = {
371
343
  url: currentUrl,
372
344
  pathname: currentUrl.pathname,
373
345
  searchParams: currentUrl.searchParams,
374
346
  params: this.params ?? {},
375
- files: files.html || files.md || files.css ? files : void 0
347
+ ...filteredFiles ? { files: filteredFiles } : {}
376
348
  };
377
- this.context = _ComponentElement.extendContext ? _ComponentElement.extendContext(base) : base;
349
+ this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
378
350
  if (this.hasAttribute(SSR_ATTR)) {
379
351
  this.removeAttribute(SSR_ATTR);
380
352
  const lightText = this.textContent?.trim();
381
353
  if (lightText) {
382
354
  try {
383
355
  this.data = JSON.parse(lightText);
384
- } catch {
385
- }
356
+ } catch {}
386
357
  }
387
358
  this.textContent = "";
388
359
  this.state = "ready";
@@ -410,56 +381,42 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
410
381
  }
411
382
  disconnectedCallback() {
412
383
  this.component.destroy?.();
413
- this.component.element = void 0;
384
+ this.component.element = undefined;
414
385
  this.intersectionObserver?.disconnect();
415
386
  this.intersectionObserver = null;
416
387
  this.abortController?.abort();
417
388
  this.abortController = null;
418
389
  this.state = "idle";
419
390
  this.data = null;
420
- this.context = void 0;
391
+ this.context = undefined;
421
392
  this.dataPromise = null;
422
393
  this.errorMessage = "";
423
394
  this.signalReady();
424
395
  this.deferred = null;
425
396
  }
426
- /**
427
- * Reload component data. Aborts any in-flight request first.
428
- */
429
397
  async reload() {
430
398
  if (this.params === null)
431
399
  return;
432
400
  this.abortController?.abort();
433
- this.abortController = new AbortController();
401
+ this.abortController = new AbortController;
434
402
  await this.loadData();
435
403
  }
436
- /**
437
- * Fetch a single file by path, with caching.
438
- * Absolute URLs (http/https) pass through; relative paths get '/' prefix.
439
- */
440
404
  static loadFile(path) {
441
- const cached = _ComponentElement.fileCache.get(path);
442
- if (cached)
443
- return cached;
444
405
  const url = path.startsWith("http://") || path.startsWith("https://") ? path : path.startsWith("/") ? path : "/" + path;
445
- const promise = fetch(url).then((res) => res.ok ? res.text() : void 0, () => void 0);
446
- _ComponentElement.fileCache.set(path, promise);
447
- return promise;
406
+ return fetch(url).then((res) => res.ok ? res.text() : undefined, () => {
407
+ return;
408
+ });
448
409
  }
449
- /**
450
- * Load all companion files for this widget instance.
451
- * Uses effectiveFiles (from registration) falling back to component.files.
452
- */
453
410
  async loadFiles() {
454
411
  const filePaths = this.effectiveFiles ?? this.component.files;
455
412
  if (!filePaths)
456
413
  return {};
457
414
  const [html, md, css] = await Promise.all([
458
- filePaths.html ? _ComponentElement.loadFile(filePaths.html) : void 0,
459
- filePaths.md ? _ComponentElement.loadFile(filePaths.md) : void 0,
460
- filePaths.css ? _ComponentElement.loadFile(filePaths.css) : void 0
415
+ filePaths.html ? ComponentElement.loadFile(filePaths.html) : undefined,
416
+ filePaths.md ? ComponentElement.loadFile(filePaths.md) : undefined,
417
+ filePaths.css ? ComponentElement.loadFile(filePaths.css) : undefined
461
418
  ]);
462
- return { html, md, css };
419
+ return filterUndefined({ html, md, css }) ?? {};
463
420
  }
464
421
  async loadData() {
465
422
  if (this.params === null)
@@ -470,7 +427,7 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
470
427
  try {
471
428
  const promise = this.component.getData({
472
429
  params: this.params,
473
- signal,
430
+ ...signal ? { signal } : {},
474
431
  context: this.context
475
432
  });
476
433
  this.dataPromise = promise;
@@ -523,189 +480,235 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
523
480
  });
524
481
  }
525
482
  }
526
- };
483
+ }
527
484
 
528
- // dist/src/widget/widget.registry.js
529
- var WidgetRegistry = class {
530
- widgets = /* @__PURE__ */ new Map();
531
- /** Register a widget by its name. */
485
+ // dist/core/widget/widget.registry.js
486
+ class WidgetRegistry {
487
+ widgets = new Map;
532
488
  add(widget) {
533
489
  this.widgets.set(widget.name, widget);
534
490
  }
535
- /** Look up a widget by name. */
536
491
  get(name) {
537
492
  return this.widgets.get(name);
538
493
  }
539
- /** Iterate all registered widgets. */
540
494
  [Symbol.iterator]() {
541
495
  return this.widgets.values();
542
496
  }
543
- /** Emit a WidgetsManifest from registered widgets. */
544
- toManifest() {
545
- const widgets = [];
546
- const moduleLoaders = {};
547
- for (const [name, widget] of this.widgets) {
548
- const entry = {
549
- name,
550
- modulePath: name,
551
- tagName: `widget-${name}`,
552
- files: widget.files
553
- };
554
- widgets.push(entry);
555
- moduleLoaders[name] = () => Promise.resolve({ default: widget.constructor });
497
+ }
498
+
499
+ // dist/core/router/route.trie.js
500
+ class RouteTrie {
501
+ tree;
502
+ constructor(tree) {
503
+ this.tree = tree;
504
+ }
505
+ match(pathname) {
506
+ pathname = this.normalizePath(pathname);
507
+ if (pathname === "/") {
508
+ if (this.tree.files || this.tree.redirect) {
509
+ return { node: this.tree, pattern: "/", params: {} };
510
+ }
511
+ return;
556
512
  }
557
- return { widgets, moduleLoaders };
513
+ return this.walk(this.tree, this.splitSegments(pathname), 0, {}, "/");
558
514
  }
559
- };
560
-
561
- // dist/src/route/route.core.js
562
- var __rewriteRelativeImportExtension = function(path, preserveJsx) {
563
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
564
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
565
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
566
- });
515
+ findErrorBoundary(pathname) {
516
+ pathname = this.normalizePath(pathname);
517
+ if (pathname === "/")
518
+ return this.tree.errorBoundary;
519
+ return this.walkForBoundary(this.tree, this.splitSegments(pathname), 0, this.tree.errorBoundary);
567
520
  }
568
- return path;
569
- };
570
- var DEFAULT_BASE_PATH = { html: "/html", md: "/md", app: "/app" };
571
- var BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
572
- function assertSafeRedirect(url) {
573
- if (BLOCKED_PROTOCOLS.test(url.trim())) {
574
- throw new Error(`Unsafe redirect URL blocked: ${url}`);
521
+ findRoute(pattern) {
522
+ if (pattern === "/") {
523
+ return this.tree.files || this.tree.redirect ? this.tree : undefined;
524
+ }
525
+ const segments = this.splitSegments(pattern);
526
+ let node = this.tree;
527
+ for (const segment of segments) {
528
+ let child;
529
+ if (segment.startsWith(":") && segment.endsWith("*")) {
530
+ child = node.wildcard?.child;
531
+ } else if (segment.startsWith(":")) {
532
+ child = node.dynamic?.child;
533
+ } else {
534
+ child = node.children?.[segment];
535
+ }
536
+ if (!child)
537
+ return;
538
+ node = child;
539
+ }
540
+ return node.files || node.redirect ? node : undefined;
541
+ }
542
+ safeDecode(segment) {
543
+ try {
544
+ return decodeURIComponent(segment);
545
+ } catch {
546
+ return segment;
547
+ }
548
+ }
549
+ splitSegments(pathname) {
550
+ return pathname.substring(1).split("/");
551
+ }
552
+ normalizePath(pathname) {
553
+ if (pathname.length > 1 && pathname.endsWith("/")) {
554
+ pathname = pathname.slice(0, -1);
555
+ }
556
+ if (!pathname.startsWith("/")) {
557
+ pathname = "/" + pathname;
558
+ }
559
+ return pathname;
560
+ }
561
+ walk(node, segments, index, params, pattern) {
562
+ if (index === segments.length) {
563
+ if (node.files || node.redirect) {
564
+ return { node, pattern, params: { ...params } };
565
+ }
566
+ if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
567
+ const wp = pattern === "/" ? `/:${node.wildcard.param}*` : `${pattern}/:${node.wildcard.param}*`;
568
+ return {
569
+ node: node.wildcard.child,
570
+ pattern: wp,
571
+ params: { ...params, [node.wildcard.param]: "" }
572
+ };
573
+ }
574
+ return;
575
+ }
576
+ const segment = segments[index];
577
+ const staticChild = node.children?.[segment];
578
+ if (staticChild) {
579
+ const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
580
+ const result = this.walk(staticChild, segments, index + 1, params, childPattern);
581
+ if (result)
582
+ return result;
583
+ }
584
+ if (node.dynamic) {
585
+ const { param, child } = node.dynamic;
586
+ params[param] = this.safeDecode(segment);
587
+ const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
588
+ const result = this.walk(child, segments, index + 1, params, childPattern);
589
+ if (result)
590
+ return result;
591
+ delete params[param];
592
+ }
593
+ if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
594
+ const { param, child } = node.wildcard;
595
+ let rest = this.safeDecode(segments[index]);
596
+ for (let i = index + 1;i < segments.length; i++) {
597
+ rest += "/" + this.safeDecode(segments[i]);
598
+ }
599
+ const wp = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
600
+ return {
601
+ node: child,
602
+ pattern: wp,
603
+ params: { ...params, [param]: rest }
604
+ };
605
+ }
606
+ return;
575
607
  }
608
+ walkForBoundary(node, segments, index, deepest) {
609
+ if (index === segments.length) {
610
+ return node.errorBoundary ?? deepest;
611
+ }
612
+ const segment = segments[index];
613
+ const staticChild = node.children?.[segment];
614
+ if (staticChild) {
615
+ return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
616
+ }
617
+ if (node.dynamic) {
618
+ return this.walkForBoundary(node.dynamic.child, segments, index + 1, node.dynamic.child.errorBoundary ?? deepest);
619
+ }
620
+ if (node.wildcard) {
621
+ return node.wildcard.child.errorBoundary ?? deepest;
622
+ }
623
+ return deepest;
624
+ }
625
+ }
626
+
627
+ // dist/core/runtime/abstract.runtime.js
628
+ var ROUTES_MANIFEST_PATH = "/routes.manifest.json";
629
+ var WIDGETS_MANIFEST_PATH = "/widgets.manifest.json";
630
+ var ELEMENTS_MANIFEST_PATH = "/elements.manifest.json";
631
+
632
+ // dist/core/type/logger.type.js
633
+ var noop = () => {};
634
+ var defaultLogger = { error: noop, warn: noop };
635
+ function setLogger(_logger) {
636
+ console.warn("[emroute] setLogger() is deprecated. Pass `logger` in Emroute.create() config instead.");
576
637
  }
638
+
639
+ // dist/core/pipeline/pipeline.js
577
640
  var DEFAULT_ROOT_ROUTE = {
578
641
  pattern: "/",
579
642
  type: "page",
580
643
  modulePath: "__default_root__"
581
644
  };
582
- function toRouteConfig(resolved) {
583
- const node = resolved.node;
645
+ function toRouteConfig(node, pattern) {
584
646
  return {
585
- pattern: resolved.pattern,
647
+ pattern,
586
648
  type: node.redirect ? "redirect" : "page",
587
649
  modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
588
- files: node.files
650
+ ...node.files ? { files: node.files } : {}
589
651
  };
590
652
  }
591
- var RouteCore = class {
592
- resolver;
593
- /** Registered context provider (if any). Exposed so renderers can apply it to inline contexts. */
653
+
654
+ class Pipeline {
655
+ runtime;
594
656
  contextProvider;
595
- listeners = /* @__PURE__ */ new Set();
596
- moduleCache = /* @__PURE__ */ new Map();
597
- widgetFileCache = /* @__PURE__ */ new Map();
657
+ logger;
598
658
  moduleLoaders;
599
- currentRoute = null;
600
- readFile;
601
- constructor(resolver, options = {}) {
602
- this.resolver = resolver;
603
- this.readFile = options.fileReader ?? ((path) => fetch(path, { headers: { Accept: "text/plain" } }).then((r) => r.text()));
604
- this.contextProvider = options.extendContext;
659
+ constructor(options) {
660
+ this.runtime = options.runtime;
661
+ this.contextProvider = options.contextProvider;
662
+ this.logger = options.logger ?? defaultLogger;
605
663
  this.moduleLoaders = options.moduleLoaders ?? {};
606
664
  }
607
- /**
608
- * Get current route parameters.
609
- */
610
- getParams() {
611
- return this.currentRoute?.params ?? {};
612
- }
613
- /**
614
- * Add event listener for router events.
615
- */
616
- addEventListener(listener) {
617
- this.listeners.add(listener);
618
- return () => this.listeners.delete(listener);
619
- }
620
- /**
621
- * Emit router event to listeners.
622
- */
623
- emit(event) {
624
- for (const listener of this.listeners) {
625
- try {
626
- listener(event);
627
- } catch (e) {
628
- console.error("[Router] Event listener error:", e);
629
- }
630
- }
665
+ async getResolver() {
666
+ const response = await this.runtime.query(ROUTES_MANIFEST_PATH);
667
+ const tree = response.status === 404 ? {} : await response.json();
668
+ return new RouteTrie(tree);
631
669
  }
632
- /**
633
- * Match a URL to a route.
634
- * Falls back to the default root route for '/'.
635
- */
636
- match(url) {
637
- const pathname = url.pathname;
638
- const resolved = this.resolver.match(pathname);
670
+ async match(url) {
671
+ const resolver = await this.getResolver();
672
+ const resolved = resolver.match(url.pathname);
639
673
  if (resolved) {
640
- return {
641
- route: toRouteConfig(resolved),
642
- params: resolved.params
643
- };
674
+ return { route: toRouteConfig(resolved.node, resolved.pattern), params: resolved.params };
644
675
  }
645
- if (pathname === "/" || pathname === "") {
646
- return {
647
- route: DEFAULT_ROOT_ROUTE,
648
- params: {}
649
- };
676
+ if (url.pathname === "/" || url.pathname === "") {
677
+ return { route: DEFAULT_ROOT_ROUTE, params: {} };
650
678
  }
651
- return void 0;
679
+ return;
652
680
  }
653
- /** Get status-specific page (404, 401, 403). */
654
- getStatusPage(status) {
655
- const node = this.resolver.findRoute(`/${status}`);
681
+ async findRoute(pattern) {
682
+ const resolver = await this.getResolver();
683
+ const node = resolver.findRoute(pattern);
656
684
  if (!node)
657
- return void 0;
658
- return {
659
- pattern: `/${status}`,
660
- type: "page",
661
- modulePath: node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
662
- files: node.files
663
- };
664
- }
665
- /** Get global error handler (root errorBoundary). */
666
- getErrorHandler() {
667
- const modulePath = this.resolver.findErrorBoundary("/");
668
- if (!modulePath)
669
- return void 0;
670
- return { pattern: "/", type: "error", modulePath };
685
+ return;
686
+ return toRouteConfig(node, pattern);
671
687
  }
672
- /**
673
- * Find error boundary for a given pathname.
674
- * Note: pattern is the input pathname, not the boundary's own pattern.
675
- * Callers should only rely on modulePath.
676
- */
677
- findErrorBoundary(pathname) {
678
- const modulePath = this.resolver.findErrorBoundary(pathname);
688
+ async findErrorBoundary(pathname) {
689
+ const resolver = await this.getResolver();
690
+ const modulePath = resolver.findErrorBoundary(pathname);
679
691
  if (!modulePath)
680
- return void 0;
692
+ return;
681
693
  return { pattern: pathname, modulePath };
682
694
  }
683
- /**
684
- * Find a route by its exact pattern.
685
- * Used for building route hierarchy.
686
- */
687
- findRoute(pattern) {
688
- const node = this.resolver.findRoute(pattern);
695
+ async getStatusPage(status) {
696
+ const resolver = await this.getResolver();
697
+ const node = resolver.findRoute(`/${status}`);
689
698
  if (!node)
690
- return void 0;
691
- return {
692
- pattern,
693
- type: node.redirect ? "redirect" : "page",
694
- modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
695
- files: node.files
696
- };
699
+ return;
700
+ return toRouteConfig(node, `/${status}`);
701
+ }
702
+ async getErrorHandler() {
703
+ const resolver = await this.getResolver();
704
+ const modulePath = resolver.findErrorBoundary("/");
705
+ if (!modulePath)
706
+ return;
707
+ return { pattern: "/", type: "error", modulePath };
697
708
  }
698
- /**
699
- * Build route hierarchy from a pattern.
700
- * Patterns are always unprefixed (no basePath).
701
- *
702
- * e.g., '/projects/:id/tasks'
703
- * → ['/', '/projects', '/projects/:id', '/projects/:id/tasks']
704
- */
705
709
  buildRouteHierarchy(pattern) {
706
- if (pattern === "/") {
710
+ if (pattern === "/")
707
711
  return ["/"];
708
- }
709
712
  const segments = pattern.split("/").filter(Boolean);
710
713
  const hierarchy = ["/"];
711
714
  let current = "";
@@ -715,318 +718,100 @@ var RouteCore = class {
715
718
  }
716
719
  return hierarchy;
717
720
  }
718
- /**
719
- * Normalize URL by removing trailing slashes (except bare '/').
720
- */
721
- normalizeUrl(url) {
722
- if (url.length > 1 && url.endsWith("/")) {
723
- return url.slice(0, -1);
724
- }
725
- return url;
726
- }
727
- /**
728
- * Convert relative path to absolute path.
729
- */
730
- toAbsolutePath(path) {
731
- return path.startsWith("/") ? path : "/" + path;
732
- }
733
- /**
734
- * Load a module with caching.
735
- * Uses pre-bundled loaders when available, falls back to dynamic import.
736
- */
737
721
  async loadModule(modulePath) {
738
- if (this.moduleCache.has(modulePath)) {
739
- return this.moduleCache.get(modulePath);
740
- }
741
- let module;
742
722
  const loader = this.moduleLoaders[modulePath];
743
723
  if (loader) {
744
- module = await loader();
745
- } else {
746
- const absolutePath = this.toAbsolutePath(modulePath);
747
- module = await import(__rewriteRelativeImportExtension(absolutePath));
724
+ return await loader();
748
725
  }
749
- this.moduleCache.set(modulePath, module);
750
- return module;
726
+ const abs = modulePath.startsWith("/") ? modulePath : "/" + modulePath;
727
+ return await this.runtime.loadModule(abs);
728
+ }
729
+ getModuleFiles(mod) {
730
+ if (!mod || typeof mod !== "object")
731
+ return;
732
+ const files = mod.__files;
733
+ if (!files || typeof files !== "object")
734
+ return;
735
+ return files;
751
736
  }
752
- /**
753
- * Load widget file contents with caching.
754
- */
755
- async loadWidgetFiles(widgetFiles) {
737
+ async loadFiles(files) {
756
738
  const load = async (path) => {
757
- const absPath = this.toAbsolutePath(path);
758
- const cached = this.widgetFileCache.get(absPath);
759
- if (cached !== void 0)
760
- return cached;
739
+ const abs = path.startsWith("/") ? path : "/" + path;
761
740
  try {
762
- const content = await this.readFile(absPath);
763
- this.widgetFileCache.set(absPath, content);
764
- return content;
741
+ return await this.runtime.query(abs, { as: "text" });
765
742
  } catch (e) {
766
- console.warn(`[RouteCore] Failed to load widget file ${path}:`, e instanceof Error ? e.message : e);
767
- return void 0;
743
+ console.warn(`[Pipeline] Failed to load file ${path}:`, e instanceof Error ? e.message : e);
744
+ return;
768
745
  }
769
746
  };
770
747
  const [html, md, css] = await Promise.all([
771
- widgetFiles.html ? load(widgetFiles.html) : void 0,
772
- widgetFiles.md ? load(widgetFiles.md) : void 0,
773
- widgetFiles.css ? load(widgetFiles.css) : void 0
748
+ files.html ? load(files.html) : undefined,
749
+ files.md ? load(files.md) : undefined,
750
+ files.css ? load(files.css) : undefined
774
751
  ]);
775
- return { html, md, css };
752
+ const result = {};
753
+ if (html !== undefined)
754
+ result.html = html;
755
+ if (md !== undefined)
756
+ result.md = md;
757
+ if (css !== undefined)
758
+ result.css = css;
759
+ return result;
776
760
  }
777
- /**
778
- * Build a RouteInfo from a matched route and the resolved URL pathname.
779
- * Called once per navigation; the result is reused across the route hierarchy.
780
- */
781
761
  toRouteInfo(matched, url) {
782
- return {
783
- url,
784
- params: matched.params
785
- };
786
- }
787
- /**
788
- * Get inlined `__files` from a cached module (merged module pattern).
789
- * Returns undefined if the module isn't cached or has no __files.
790
- */
791
- getModuleFiles(modulePath) {
792
- const cached = this.moduleCache.get(modulePath);
793
- if (!cached || typeof cached !== "object")
794
- return void 0;
795
- const files = cached.__files;
796
- if (!files || typeof files !== "object")
797
- return void 0;
798
- return files;
762
+ return { url, params: matched.params };
799
763
  }
800
- /**
801
- * Build a ComponentContext by extending RouteInfo with loaded file contents.
802
- *
803
- * When the route module is a merged module (contains `__files`), uses
804
- * inlined content directly. Otherwise falls back to reading companion files.
805
- */
806
- async buildComponentContext(routeInfo, route, signal, isLeaf) {
764
+ async buildContext(routeInfo, route, signal, isLeaf, loadedModule) {
807
765
  const rf = route.files;
808
- const modulePath = rf?.ts ?? rf?.js;
809
- const inlined = modulePath ? this.getModuleFiles(modulePath) : void 0;
810
- let html;
811
- let md;
812
- let css;
766
+ const inlined = loadedModule ? this.getModuleFiles(loadedModule) : undefined;
767
+ let files;
813
768
  if (inlined) {
814
- html = inlined.html;
815
- md = inlined.md;
816
- css = inlined.css;
769
+ files = inlined;
770
+ } else if (rf) {
771
+ const filePaths = {};
772
+ if (rf.html)
773
+ filePaths.html = rf.html;
774
+ if (rf.md)
775
+ filePaths.md = rf.md;
776
+ if (rf.css)
777
+ filePaths.css = rf.css;
778
+ files = await this.loadFiles(filePaths);
817
779
  } else {
818
- const fetchFile = (filePath) => this.readFile(this.toAbsolutePath(filePath));
819
- [html, md, css] = await Promise.all([
820
- rf?.html ? fetchFile(rf.html) : void 0,
821
- rf?.md ? fetchFile(rf.md) : void 0,
822
- rf?.css ? fetchFile(rf.css) : void 0
823
- ]);
780
+ files = {};
824
781
  }
825
782
  const base = {
826
783
  ...routeInfo,
827
784
  pathname: routeInfo.url.pathname,
828
785
  searchParams: routeInfo.url.searchParams,
829
- files: { html, md, css },
830
- signal,
831
- isLeaf
786
+ files,
787
+ ...signal ? { signal } : {},
788
+ ...isLeaf !== undefined ? { isLeaf } : {}
832
789
  };
833
790
  return this.contextProvider ? this.contextProvider(base) : base;
834
791
  }
835
- };
836
-
837
- // dist/src/route/route.trie.js
838
- function createNode() {
839
- return { static: /* @__PURE__ */ new Map() };
840
- }
841
- function safeDecode(segment) {
842
- try {
843
- return decodeURIComponent(segment);
844
- } catch {
845
- return segment;
846
- }
847
792
  }
848
- function splitSegments(pathname) {
849
- return pathname.substring(1).split("/");
850
- }
851
- function convertNode(source, pattern) {
852
- const node = createNode();
853
- if (source.files || source.redirect) {
854
- node.route = source;
855
- node.pattern = pattern;
856
- }
857
- if (source.errorBoundary) {
858
- node.errorBoundary = source.errorBoundary;
859
- }
860
- if (source.children) {
861
- for (const [segment, child] of Object.entries(source.children)) {
862
- const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
863
- node.static.set(segment, convertNode(child, childPattern));
864
- }
865
- }
866
- if (source.dynamic) {
867
- const { param, child } = source.dynamic;
868
- const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
869
- node.dynamic = { param, node: convertNode(child, childPattern) };
870
- }
871
- if (source.wildcard) {
872
- const { param, child } = source.wildcard;
873
- const childPattern = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
874
- node.wildcard = { param, node: convertNode(child, childPattern) };
793
+
794
+ // dist/core/util/widget-resolve.util.js
795
+ var MAX_WIDGET_DEPTH = 10;
796
+ async function resolveRecursively(content, parse, resolve, replace, depth = 0, logger = defaultLogger) {
797
+ if (depth >= MAX_WIDGET_DEPTH) {
798
+ logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). ` + "Possible circular dependency or excessive nesting.");
799
+ return content;
875
800
  }
876
- return node;
801
+ const widgets = parse(content);
802
+ if (widgets.length === 0)
803
+ return content;
804
+ const replacements = new Map;
805
+ await Promise.all(widgets.map(async (widget) => {
806
+ let rendered = await resolve(widget);
807
+ rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1, logger);
808
+ replacements.set(widget, rendered);
809
+ }));
810
+ return replace(content, replacements);
877
811
  }
878
- var RouteTrie = class {
879
- root;
880
- constructor(tree) {
881
- this.root = convertNode(tree, "/");
882
- }
883
- match(pathname) {
884
- if (pathname.length > 1 && pathname.endsWith("/")) {
885
- pathname = pathname.slice(0, -1);
886
- }
887
- if (!pathname.startsWith("/")) {
888
- pathname = "/" + pathname;
889
- }
890
- if (pathname === "/") {
891
- if (this.root.route) {
892
- return { node: this.root.route, pattern: "/", params: {} };
893
- }
894
- return void 0;
895
- }
896
- const segments = splitSegments(pathname);
897
- return this.walk(this.root, segments, 0, {});
898
- }
899
- findErrorBoundary(pathname) {
900
- if (pathname.length > 1 && pathname.endsWith("/")) {
901
- pathname = pathname.slice(0, -1);
902
- }
903
- if (!pathname.startsWith("/")) {
904
- pathname = "/" + pathname;
905
- }
906
- if (pathname === "/")
907
- return this.root.errorBoundary;
908
- const segments = splitSegments(pathname);
909
- return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
910
- }
911
- findRoute(pattern) {
912
- if (pattern === "/") {
913
- return this.root.route;
914
- }
915
- const segments = splitSegments(pattern);
916
- let node = this.root;
917
- for (const segment of segments) {
918
- let child;
919
- if (segment.startsWith(":") && segment.endsWith("*")) {
920
- child = node.wildcard?.node;
921
- } else if (segment.startsWith(":")) {
922
- child = node.dynamic?.node;
923
- } else {
924
- child = node.static.get(segment);
925
- }
926
- if (!child)
927
- return void 0;
928
- node = child;
929
- }
930
- return node.route;
931
- }
932
- // ── Private matching ──────────────────────────────────────────────────
933
- walk(node, segments, index, params) {
934
- if (index === segments.length) {
935
- if (node.route) {
936
- return { node: node.route, pattern: node.pattern, params: { ...params } };
937
- }
938
- if (node.wildcard?.node.route) {
939
- return {
940
- node: node.wildcard.node.route,
941
- pattern: node.wildcard.node.pattern,
942
- params: { ...params, [node.wildcard.param]: "" }
943
- };
944
- }
945
- return void 0;
946
- }
947
- const segment = segments[index];
948
- const staticChild = node.static.get(segment);
949
- if (staticChild) {
950
- const result = this.walk(staticChild, segments, index + 1, params);
951
- if (result)
952
- return result;
953
- }
954
- if (node.dynamic) {
955
- const { param, node: dynamicNode } = node.dynamic;
956
- params[param] = safeDecode(segment);
957
- const result = this.walk(dynamicNode, segments, index + 1, params);
958
- if (result)
959
- return result;
960
- delete params[param];
961
- }
962
- if (node.wildcard?.node.route) {
963
- const { param, node: wildcardNode } = node.wildcard;
964
- let rest = safeDecode(segments[index]);
965
- for (let i = index + 1; i < segments.length; i++) {
966
- rest += "/" + safeDecode(segments[i]);
967
- }
968
- return {
969
- node: wildcardNode.route,
970
- pattern: wildcardNode.pattern,
971
- params: { ...params, [param]: rest }
972
- };
973
- }
974
- return void 0;
975
- }
976
- /**
977
- * Walk for error boundary. Follows the same priority as match
978
- * (static → dynamic → wildcard) without backtracking across branches.
979
- * Returns the deepest error boundary module path found along the path.
980
- */
981
- walkForBoundary(node, segments, index, deepest) {
982
- if (index === segments.length) {
983
- return node.errorBoundary ?? deepest;
984
- }
985
- const segment = segments[index];
986
- const staticChild = node.static.get(segment);
987
- if (staticChild) {
988
- return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
989
- }
990
- if (node.dynamic) {
991
- return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
992
- }
993
- if (node.wildcard) {
994
- return node.wildcard.node.errorBoundary ?? deepest;
995
- }
996
- return deepest;
997
- }
998
- };
999
-
1000
- // dist/src/type/logger.type.js
1001
- var noop = () => {
1002
- };
1003
- var logger = { error: noop, warn: noop };
1004
- function setLogger(impl) {
1005
- logger.error = impl.error.bind(impl);
1006
- logger.warn = impl.warn.bind(impl);
1007
- }
1008
-
1009
- // dist/src/util/widget-resolve.util.js
1010
- var MAX_WIDGET_DEPTH = 10;
1011
- async function resolveRecursively(content, parse, resolve, replace, depth = 0) {
1012
- if (depth >= MAX_WIDGET_DEPTH) {
1013
- logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). Possible circular dependency or excessive nesting.`);
1014
- return content;
1015
- }
1016
- const widgets = parse(content);
1017
- if (widgets.length === 0)
1018
- return content;
1019
- const replacements = /* @__PURE__ */ new Map();
1020
- await Promise.all(widgets.map(async (widget) => {
1021
- let rendered = await resolve(widget);
1022
- rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1);
1023
- replacements.set(widget, rendered);
1024
- }));
1025
- return replace(content, replacements);
1026
- }
1027
- function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider) {
812
+ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider, logger = defaultLogger) {
1028
813
  const tagPattern = /<widget-(?<name>[a-z][a-z0-9-]*)(?<attrs>\s[^>]*)?>(?<content>.*?)<\/widget-\k<name>>/gis;
1029
- const wrappers = /* @__PURE__ */ new Map();
814
+ const wrappers = new Map;
1030
815
  const ssrAttrPattern = new RegExp(`\\s${SSR_ATTR}(?:\\s|=|$)`);
1031
816
  const parse = (content) => {
1032
817
  const matches = content.matchAll(tagPattern).toArray();
@@ -1051,7 +836,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
1051
836
  ...routeInfo,
1052
837
  pathname: routeInfo.url.pathname,
1053
838
  searchParams: routeInfo.url.searchParams,
1054
- files
839
+ ...files ? { files } : {}
1055
840
  };
1056
841
  const context = contextProvider ? contextProvider(baseContext) : baseContext;
1057
842
  const data = await widget.getData({ params, context });
@@ -1063,7 +848,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
1063
848
  });
1064
849
  return rendered;
1065
850
  } catch (e) {
1066
- logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e : void 0);
851
+ logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e : undefined);
1067
852
  return match[0];
1068
853
  }
1069
854
  };
@@ -1080,7 +865,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
1080
865
  }
1081
866
  return result;
1082
867
  };
1083
- return resolveRecursively(html, parse, resolve, replace);
868
+ return resolveRecursively(html, parse, resolve, replace, 0, logger);
1084
869
  }
1085
870
  function parseAttrsToParams(attrsString) {
1086
871
  const params = {};
@@ -1093,7 +878,7 @@ function parseAttrsToParams(attrsString) {
1093
878
  continue;
1094
879
  const key = attrName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1095
880
  const rawValue = dq ?? sq ?? uq;
1096
- if (rawValue === void 0) {
881
+ if (rawValue === undefined) {
1097
882
  params[key] = "";
1098
883
  continue;
1099
884
  }
@@ -1110,32 +895,11 @@ function escapeAttr(value) {
1110
895
  return value.replaceAll("&", "&amp;").replaceAll("'", "&#39;");
1111
896
  }
1112
897
 
1113
- // dist/src/component/abstract.component.js
1114
- var Component = class {
1115
- /** Host element reference, set by ComponentElement in the browser. */
898
+ // dist/core/component/abstract.component.js
899
+ class Component {
1116
900
  element;
1117
- /** Associated file paths for pre-loaded content (html, md, css). */
1118
901
  files;
1119
- /**
1120
- * When true, SSR serializes the getData() result into the element's
1121
- * light DOM so the client can access it immediately in hydrate()
1122
- * without re-fetching.
1123
- *
1124
- * Default is false — hydrate() receives `data: null`. Most widgets
1125
- * don't need this because the rendered Shadow DOM already contains
1126
- * the visual representation of the data.
1127
- *
1128
- * If you find yourself parsing the shadow DOM in hydrate() trying to
1129
- * reconstruct the original data object, set this to true instead.
1130
- * The server-fetched data will be available as `args.data` in hydrate().
1131
- */
1132
902
  exposeSsrData;
1133
- /**
1134
- * Render as HTML for browser context.
1135
- *
1136
- * Default implementation converts renderMarkdown() output to HTML.
1137
- * Override for custom HTML rendering with rich styling/interactivity.
1138
- */
1139
903
  renderHTML(args) {
1140
904
  if (args.data === null) {
1141
905
  return `<div data-component="${this.name}">Loading...</div>`;
@@ -1147,56 +911,23 @@ var Component = class {
1147
911
  });
1148
912
  return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
1149
913
  }
1150
- /**
1151
- * Render error state.
1152
- */
1153
914
  renderError(args) {
1154
915
  const msg = args.error instanceof Error ? args.error.message : String(args.error);
1155
916
  return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
1156
917
  }
1157
- /**
1158
- * Render error as markdown.
1159
- */
1160
918
  renderMarkdownError(error) {
1161
919
  const msg = error instanceof Error ? error.message : String(error);
1162
920
  return `> **Error** (\`${this.name}\`): ${msg}`;
1163
921
  }
1164
- };
922
+ }
1165
923
 
1166
- // dist/src/component/page.component.js
1167
- var PageComponent = class extends Component {
924
+ // dist/core/component/page.component.js
925
+ class PageComponent extends Component {
1168
926
  name = "page";
1169
- /** Route pattern this page handles (optional — set by subclasses) */
1170
927
  pattern;
1171
- /**
1172
- * Fetch or compute page data. Override in subclasses.
1173
- * Default: returns null (no data needed).
1174
- *
1175
- * @example
1176
- * ```ts
1177
- * override getData({ params, context }: this['DataArgs']) {
1178
- * return fetch(`/api/${params.id}`, { signal: context?.signal });
1179
- * }
1180
- * ```
1181
- */
1182
928
  getData(_args) {
1183
929
  return Promise.resolve(null);
1184
930
  }
1185
- /**
1186
- * Render page as HTML.
1187
- *
1188
- * Fallback chain:
1189
- * 1. html file content from context
1190
- * 2. md file content wrapped in `<mark-down>`
1191
- * 3. `<router-slot />` (bare slot for child routes)
1192
- *
1193
- * @example
1194
- * ```ts
1195
- * override renderHTML({ data, params, context }: this['RenderArgs']) {
1196
- * return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
1197
- * }
1198
- * ```
1199
- */
1200
931
  renderHTML(args) {
1201
932
  const files = args.context.files;
1202
933
  const style = files?.css ? `<style>${files.css}</style>
@@ -1210,25 +941,12 @@ var PageComponent = class extends Component {
1210
941
  }
1211
942
  if (files?.md) {
1212
943
  const hasSlot = files.md.includes("```router-slot");
1213
- const slot = args.context.isLeaf || hasSlot ? "" : "\n<router-slot></router-slot>";
944
+ const slot = args.context.isLeaf || hasSlot ? "" : `
945
+ <router-slot></router-slot>`;
1214
946
  return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
1215
947
  }
1216
948
  return args.context.isLeaf ? "" : "<router-slot></router-slot>";
1217
949
  }
1218
- /**
1219
- * Render page as Markdown.
1220
- *
1221
- * Fallback chain:
1222
- * 1. md file content from context
1223
- * 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
1224
- *
1225
- * @example
1226
- * ```ts
1227
- * override renderMarkdown({ data, params, context }: this['RenderArgs']) {
1228
- * return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
1229
- * }
1230
- * ```
1231
- */
1232
950
  renderMarkdown(args) {
1233
951
  const files = args.context.files;
1234
952
  if (files?.md) {
@@ -1236,53 +954,41 @@ var PageComponent = class extends Component {
1236
954
  }
1237
955
  return args.context.isLeaf ? "" : "```router-slot\n```";
1238
956
  }
1239
- /**
1240
- * Page title. Override in subclasses.
1241
- * Default: undefined (no title).
1242
- *
1243
- * @example
1244
- * ```ts
1245
- * override getTitle({ data, params }: this['RenderArgs']) {
1246
- * return `Project ${params.id}`;
1247
- * }
1248
- * ```
1249
- */
1250
957
  getTitle(_args) {
1251
- return void 0;
958
+ return;
1252
959
  }
1253
- };
1254
- var page_component_default = new PageComponent();
960
+ }
961
+ var page_component_default = new PageComponent;
1255
962
 
1256
- // dist/src/renderer/ssr/ssr.renderer.js
1257
- var SsrRenderer = class _SsrRenderer {
1258
- core;
963
+ // dist/core/renderer/ssr.renderer.js
964
+ class SsrRenderer {
965
+ pipeline;
1259
966
  widgets;
1260
967
  widgetFiles;
1261
- constructor(resolver, options = {}) {
1262
- this.core = new RouteCore(resolver, options);
968
+ logger;
969
+ constructor(pipeline, options = {}) {
970
+ this.pipeline = pipeline;
971
+ this.logger = pipeline.logger;
1263
972
  this.widgets = options.widgets ?? null;
1264
973
  this.widgetFiles = options.widgetFiles ?? {};
1265
974
  }
1266
- /**
1267
- * Render a URL to a content string.
1268
- */
1269
975
  async render(url, signal) {
1270
- const matched = this.core.match(url);
976
+ const matched = await this.pipeline.match(url);
1271
977
  if (!matched) {
1272
- const statusPage = this.core.getStatusPage(404);
978
+ const statusPage = await this.pipeline.getStatusPage(404);
1273
979
  if (statusPage) {
1274
980
  try {
1275
981
  const ri = { url, params: {} };
1276
- const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
1277
- return { content: this.stripSlots(result.content), status: 404, title: result.title };
982
+ const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
983
+ return { content: this.stripSlots(result.content), status: 404, ...result.title !== undefined ? { title: result.title } : {} };
1278
984
  } catch (e) {
1279
- logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e : void 0);
985
+ this.logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e : undefined);
1280
986
  }
1281
987
  }
1282
988
  return { content: this.renderStatusPage(404, url), status: 404 };
1283
989
  }
1284
990
  if (matched.route.type === "redirect") {
1285
- const module = await this.core.loadModule(matched.route.modulePath);
991
+ const module = await this.pipeline.loadModule(matched.route.modulePath);
1286
992
  const redirectConfig = module.default;
1287
993
  assertSafeRedirect(redirectConfig.to);
1288
994
  return {
@@ -1291,36 +997,36 @@ var SsrRenderer = class _SsrRenderer {
1291
997
  redirect: redirectConfig.to
1292
998
  };
1293
999
  }
1294
- const routeInfo = this.core.toRouteInfo(matched, url);
1000
+ const routeInfo = this.pipeline.toRouteInfo(matched, url);
1295
1001
  try {
1296
1002
  const { content, title } = await this.renderPage(routeInfo, matched, signal);
1297
- return { content, status: 200, title };
1003
+ return { content, status: 200, ...title !== undefined ? { title } : {} };
1298
1004
  } catch (error) {
1299
1005
  if (error instanceof Response) {
1300
- const statusPage = this.core.getStatusPage(error.status);
1006
+ const statusPage = await this.pipeline.getStatusPage(error.status);
1301
1007
  if (statusPage) {
1302
1008
  try {
1303
1009
  const ri = { url, params: {} };
1304
- const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
1010
+ const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
1305
1011
  return {
1306
1012
  content: this.stripSlots(result.content),
1307
1013
  status: error.status,
1308
- title: result.title
1014
+ ...result.title !== undefined ? { title: result.title } : {}
1309
1015
  };
1310
1016
  } catch (e) {
1311
- logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e : void 0);
1017
+ this.logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e : undefined);
1312
1018
  }
1313
1019
  }
1314
1020
  return { content: this.renderStatusPage(error.status, url), status: error.status };
1315
1021
  }
1316
- logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error : void 0);
1317
- const boundary = this.core.findErrorBoundary(url.pathname);
1022
+ this.logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error : undefined);
1023
+ const boundary = await this.pipeline.findErrorBoundary(url.pathname);
1318
1024
  if (boundary) {
1319
1025
  const result = await this.tryRenderErrorModule(boundary.modulePath, url, "boundary");
1320
1026
  if (result)
1321
1027
  return result;
1322
1028
  }
1323
- const errorHandler = this.core.getErrorHandler();
1029
+ const errorHandler = await this.pipeline.getErrorHandler();
1324
1030
  if (errorHandler) {
1325
1031
  const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, "handler");
1326
1032
  if (result)
@@ -1329,15 +1035,12 @@ var SsrRenderer = class _SsrRenderer {
1329
1035
  return { content: this.renderErrorPage(error, url), status: 500 };
1330
1036
  }
1331
1037
  }
1332
- /**
1333
- * Render a matched page by composing the route hierarchy.
1334
- */
1335
1038
  async renderPage(routeInfo, matched, signal) {
1336
- const hierarchy = this.core.buildRouteHierarchy(matched.route.pattern);
1039
+ const hierarchy = this.pipeline.buildRouteHierarchy(matched.route.pattern);
1337
1040
  const segments = [];
1338
- for (let i = 0; i < hierarchy.length; i++) {
1041
+ for (let i = 0;i < hierarchy.length; i++) {
1339
1042
  const routePattern = hierarchy[i];
1340
- let route = this.core.findRoute(routePattern);
1043
+ let route = await this.pipeline.findRoute(routePattern);
1341
1044
  if (!route && routePattern === "/") {
1342
1045
  route = DEFAULT_ROOT_ROUTE;
1343
1046
  }
@@ -1351,7 +1054,7 @@ var SsrRenderer = class _SsrRenderer {
1351
1054
  let result = "";
1352
1055
  let pageTitle;
1353
1056
  let lastRenderedPattern = "";
1354
- for (let i = 0; i < segments.length; i++) {
1057
+ for (let i = 0;i < segments.length; i++) {
1355
1058
  const { content, title } = results[i];
1356
1059
  if (title) {
1357
1060
  pageTitle = title;
@@ -1361,59 +1064,57 @@ var SsrRenderer = class _SsrRenderer {
1361
1064
  } else {
1362
1065
  const injected = this.injectSlot(result, content, lastRenderedPattern);
1363
1066
  if (injected === result) {
1364
- logger.warn(`[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> for child route "${hierarchy[i]}" to render into. Add <router-slot></router-slot> to the parent template.`);
1067
+ this.logger.warn(`[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> ` + `for child route "${hierarchy[i]}" to render into. ` + `Add <router-slot></router-slot> to the parent template.`);
1365
1068
  }
1366
1069
  result = injected;
1367
1070
  }
1368
1071
  lastRenderedPattern = segments[i].route.pattern;
1369
1072
  }
1370
1073
  result = this.stripSlots(result);
1371
- return { content: result, title: pageTitle };
1074
+ return { content: result, ...pageTitle !== undefined ? { title: pageTitle } : {} };
1372
1075
  }
1373
- /** Load component, build context, get data, render content, get title. */
1374
1076
  async loadRouteContent(routeInfo, route, isLeaf, signal) {
1375
1077
  const files = route.files ?? {};
1376
1078
  const tsModule = files.ts ?? files.js;
1377
- const component = tsModule ? (await this.core.loadModule(tsModule)).default : page_component_default;
1378
- const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
1379
- const data = await component.getData({ params: routeInfo.params, signal, context });
1079
+ const loadedModule = tsModule ? await this.pipeline.loadModule(tsModule) : undefined;
1080
+ const component = loadedModule?.default ?? page_component_default;
1081
+ const context = await this.pipeline.buildContext(routeInfo, route, signal, isLeaf, loadedModule);
1082
+ const data = await component.getData({ params: routeInfo.params, ...signal ? { signal } : {}, context });
1380
1083
  const content = this.renderContent(component, { data, params: routeInfo.params, context });
1381
1084
  const title = component.getTitle({ data, params: routeInfo.params, context });
1382
- return { content, title };
1085
+ return { content, ...title !== undefined ? { title } : {} };
1383
1086
  }
1384
- /** Render a component for error boundary/handler with minimal context. */
1385
1087
  renderComponent(component, data, context) {
1386
1088
  return this.renderContent(component, { data, params: {}, context });
1387
1089
  }
1388
1090
  static EMPTY_URL = new URL("http://error");
1389
- /** Try to load and render an error boundary or handler module. Returns null on failure. */
1390
1091
  async tryRenderErrorModule(modulePath, url, kind) {
1391
1092
  try {
1392
- const module = await this.core.loadModule(modulePath);
1093
+ const module = await this.pipeline.loadModule(modulePath);
1393
1094
  const component = module.default;
1394
1095
  const minCtx = {
1395
- url: _SsrRenderer.EMPTY_URL,
1096
+ url: SsrRenderer.EMPTY_URL,
1396
1097
  params: {},
1397
1098
  pathname: "",
1398
- searchParams: new URLSearchParams()
1099
+ searchParams: new URLSearchParams
1399
1100
  };
1400
1101
  const data = await component.getData({ params: {}, context: minCtx });
1401
1102
  const content = this.renderComponent(component, data, minCtx);
1402
1103
  return { content, status: 500 };
1403
1104
  } catch (e) {
1404
- logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e : void 0);
1105
+ this.logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e : undefined);
1405
1106
  return null;
1406
1107
  }
1407
1108
  }
1408
- };
1109
+ }
1409
1110
 
1410
- // dist/src/renderer/ssr/html.renderer.js
1411
- var SsrHtmlRouter = class extends SsrRenderer {
1111
+ // dist/core/renderer/html.renderer.js
1112
+ class SsrHtmlRenderer extends SsrRenderer {
1412
1113
  label = "SSR HTML";
1413
1114
  markdownRenderer;
1414
1115
  markdownReady = null;
1415
- constructor(resolver, options = {}) {
1416
- super(resolver, options);
1116
+ constructor(pipeline, options = {}) {
1117
+ super(pipeline, options);
1417
1118
  this.markdownRenderer = options.markdownRenderer ?? null;
1418
1119
  if (this.markdownRenderer?.init) {
1419
1120
  this.markdownReady = this.markdownRenderer.init();
@@ -1426,9 +1127,6 @@ var SsrHtmlRouter = class extends SsrRenderer {
1426
1127
  stripSlots(result) {
1427
1128
  return result.replace(/<router-slot[^>]*><\/router-slot>/g, "");
1428
1129
  }
1429
- /**
1430
- * Render a single route's content.
1431
- */
1432
1130
  async renderRouteContent(routeInfo, route, isLeaf, signal) {
1433
1131
  if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
1434
1132
  return { content: `<router-slot pattern="${route.pattern}"></router-slot>` };
@@ -1440,10 +1138,10 @@ var SsrHtmlRouter = class extends SsrRenderer {
1440
1138
  if (this.widgets) {
1441
1139
  content = await resolveWidgetTags(content, this.widgets, routeInfo, (name, declared) => {
1442
1140
  const files = this.widgetFiles[name] ?? declared;
1443
- return files ? this.core.loadWidgetFiles(files) : Promise.resolve({});
1444
- }, this.core.contextProvider);
1141
+ return files ? this.pipeline.loadFiles(files) : Promise.resolve({});
1142
+ }, this.pipeline.contextProvider, this.logger);
1445
1143
  }
1446
- return { content, title };
1144
+ return { content, ...title !== undefined ? { title } : {} };
1447
1145
  }
1448
1146
  renderContent(component, args) {
1449
1147
  return component.renderHTML(args);
@@ -1465,14 +1163,9 @@ var SsrHtmlRouter = class extends SsrRenderer {
1465
1163
  <p>${escapeHtml(message)}</p>
1466
1164
  `;
1467
1165
  }
1468
- /** Add pattern attribute to bare <router-slot> tags. */
1469
1166
  attributeSlots(content, routePattern) {
1470
1167
  return content.replace(/<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g, `<router-slot pattern="${routePattern}"$1></router-slot>`);
1471
1168
  }
1472
- /**
1473
- * Expand <mark-down> tags by rendering markdown to HTML server-side.
1474
- * Leaves content unchanged if no markdown renderer is configured.
1475
- */
1476
1169
  async expandMarkdown(content) {
1477
1170
  if (!this.markdownRenderer)
1478
1171
  return content;
@@ -1485,13 +1178,12 @@ var SsrHtmlRouter = class extends SsrRenderer {
1485
1178
  const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
1486
1179
  return content.replace(pattern, (_match, escaped) => {
1487
1180
  const markdown = unescapeHtml(escaped);
1488
- const rendered = renderer.render(markdown);
1489
- return rendered;
1181
+ return renderer.render(markdown);
1490
1182
  });
1491
1183
  }
1492
- };
1184
+ }
1493
1185
 
1494
- // dist/src/widget/widget.parser.js
1186
+ // dist/core/widget/widget.parser.js
1495
1187
  var WIDGET_PATTERN = /```widget:(?<name>[a-z][a-z0-9-]*)\n(?<params>.*?)```/gs;
1496
1188
  function parseWidgetBlocks(markdown) {
1497
1189
  const blocks = [];
@@ -1534,17 +1226,18 @@ function replaceWidgetBlocks(markdown, replacements) {
1534
1226
  return result;
1535
1227
  }
1536
1228
 
1537
- // dist/src/renderer/ssr/md.renderer.js
1229
+ // dist/core/renderer/md.renderer.js
1538
1230
  var BARE_SLOT_BLOCK = "```router-slot\n```";
1539
1231
  function routerSlotBlock(pattern) {
1540
1232
  return `\`\`\`router-slot
1541
1233
  {"pattern":"${pattern}"}
1542
1234
  \`\`\``;
1543
1235
  }
1544
- var SsrMdRouter = class extends SsrRenderer {
1236
+
1237
+ class SsrMdRenderer extends SsrRenderer {
1545
1238
  label = "SSR MD";
1546
- constructor(resolver, options = {}) {
1547
- super(resolver, options);
1239
+ constructor(pipeline, options = {}) {
1240
+ super(pipeline, options);
1548
1241
  }
1549
1242
  injectSlot(parent, child, parentPattern) {
1550
1243
  return parent.replace(routerSlotBlock(parentPattern), child);
@@ -1552,9 +1245,6 @@ var SsrMdRouter = class extends SsrRenderer {
1552
1245
  stripSlots(result) {
1553
1246
  return result.replace(/```router-slot\n(?:\{[^}]*\}\n)?```/g, "").trim();
1554
1247
  }
1555
- /**
1556
- * Render a single route's content to Markdown.
1557
- */
1558
1248
  async renderRouteContent(routeInfo, route, isLeaf, signal) {
1559
1249
  if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
1560
1250
  return { content: routerSlotBlock(route.pattern) };
@@ -1565,7 +1255,7 @@ var SsrMdRouter = class extends SsrRenderer {
1565
1255
  if (this.widgets) {
1566
1256
  content = await this.resolveWidgets(content, routeInfo);
1567
1257
  }
1568
- return { content, title };
1258
+ return { content, ...title !== undefined ? { title } : {} };
1569
1259
  }
1570
1260
  renderContent(component, args) {
1571
1261
  return component.renderMarkdown(args);
@@ -1583,10 +1273,6 @@ Path: \`${url.pathname}\``;
1583
1273
 
1584
1274
  Path: \`${url.pathname}\``;
1585
1275
  }
1586
- /**
1587
- * Resolve fenced widget blocks in markdown content.
1588
- * Replaces ```widget:name blocks with rendered markdown output.
1589
- */
1590
1276
  resolveWidgets(content, routeInfo) {
1591
1277
  return resolveRecursively(content, parseWidgetBlocks, async (block) => {
1592
1278
  if (block.parseError || !block.params) {
@@ -1600,33 +1286,34 @@ Path: \`${url.pathname}\``;
1600
1286
  let files;
1601
1287
  const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
1602
1288
  if (filePaths) {
1603
- files = await this.core.loadWidgetFiles(filePaths);
1289
+ files = await this.pipeline.loadFiles(filePaths);
1604
1290
  }
1605
1291
  const baseContext = {
1606
1292
  ...routeInfo,
1607
1293
  pathname: routeInfo.url.pathname,
1608
1294
  searchParams: routeInfo.url.searchParams,
1609
- files
1295
+ ...files ? { files } : {}
1610
1296
  };
1611
- const context = this.core.contextProvider ? this.core.contextProvider(baseContext) : baseContext;
1297
+ const context = this.pipeline.contextProvider ? this.pipeline.contextProvider(baseContext) : baseContext;
1612
1298
  const data = await widget.getData({ params: block.params, context });
1613
1299
  return widget.renderMarkdown({ data, params: block.params, context });
1614
1300
  } catch (e) {
1615
1301
  return widget.renderMarkdownError(e);
1616
1302
  }
1617
- }, replaceWidgetBlocks);
1303
+ }, replaceWidgetBlocks, 0, this.logger);
1618
1304
  }
1619
- };
1305
+ }
1620
1306
 
1621
- // dist/src/util/md.util.js
1307
+ // dist/core/util/md.util.js
1622
1308
  function rewriteMdLinks(markdown, base, skipPrefixes) {
1623
1309
  const prefix = base + "/";
1624
1310
  const skip = skipPrefixes.map((p) => p.slice(1) + "/").join("|");
1625
1311
  const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, "g");
1626
1312
  const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, "g");
1627
- const lines = markdown.split("\n");
1313
+ const lines = markdown.split(`
1314
+ `);
1628
1315
  let inCodeBlock = false;
1629
- for (let i = 0; i < lines.length; i++) {
1316
+ for (let i = 0;i < lines.length; i++) {
1630
1317
  if (lines[i].startsWith("```")) {
1631
1318
  inCodeBlock = !inCodeBlock;
1632
1319
  continue;
@@ -1636,10 +1323,239 @@ function rewriteMdLinks(markdown, base, skipPrefixes) {
1636
1323
  lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
1637
1324
  lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
1638
1325
  }
1639
- return lines.join("\n");
1326
+ return lines.join(`
1327
+ `);
1328
+ }
1329
+
1330
+ // dist/core/server/emroute.server.js
1331
+ var DEFAULT_BASE_PATH = { html: "/html", md: "/md", app: "/app" };
1332
+
1333
+ class Emroute {
1334
+ runtime;
1335
+ htmlBase;
1336
+ mdBase;
1337
+ appBase;
1338
+ spa;
1339
+ title;
1340
+ htmlRenderer;
1341
+ mdRenderer;
1342
+ shell;
1343
+ constructor(htmlRenderer, mdRenderer, shell, runtime, htmlBase, mdBase, appBase, spa, title) {
1344
+ this.runtime = runtime;
1345
+ this.htmlBase = htmlBase;
1346
+ this.mdBase = mdBase;
1347
+ this.appBase = appBase;
1348
+ this.spa = spa;
1349
+ this.title = title;
1350
+ this.htmlRenderer = htmlRenderer;
1351
+ this.mdRenderer = mdRenderer;
1352
+ this.shell = shell;
1353
+ }
1354
+ static async create(config, runtime) {
1355
+ const { spa = "root" } = config;
1356
+ const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
1357
+ const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
1358
+ if (manifestResponse.status === 404 && !config.routeTree) {
1359
+ throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. ` + "Provide routeTree in config or ensure the runtime produces it.");
1360
+ }
1361
+ if (config.routeTree && manifestResponse.status === 404) {
1362
+ await runtime.command(ROUTES_MANIFEST_PATH, {
1363
+ body: JSON.stringify(config.routeTree)
1364
+ });
1365
+ }
1366
+ const pipeline = new Pipeline({
1367
+ runtime,
1368
+ ...config.extendContext ? { contextProvider: config.extendContext } : {},
1369
+ ...config.moduleLoaders ? { moduleLoaders: config.moduleLoaders } : {}
1370
+ });
1371
+ let widgets = config.widgets;
1372
+ let widgetFiles = {};
1373
+ const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
1374
+ if (widgetsResponse.status !== 404) {
1375
+ const entries = await widgetsResponse.json();
1376
+ if (config.widgets) {
1377
+ widgets = config.widgets;
1378
+ for (const entry of entries) {
1379
+ if (entry.files)
1380
+ widgetFiles[entry.name] = entry.files;
1381
+ }
1382
+ } else {
1383
+ const imported = await Emroute.importWidgets(entries, runtime);
1384
+ widgets = imported.registry;
1385
+ widgetFiles = imported.widgetFiles;
1386
+ }
1387
+ }
1388
+ let ssrHtmlRenderer = null;
1389
+ let ssrMdRenderer = null;
1390
+ if (spa !== "only") {
1391
+ ssrHtmlRenderer = new SsrHtmlRenderer(pipeline, {
1392
+ ...config.markdownRenderer ? { markdownRenderer: config.markdownRenderer } : {},
1393
+ ...widgets ? { widgets } : {},
1394
+ widgetFiles
1395
+ });
1396
+ ssrMdRenderer = new SsrMdRenderer(pipeline, {
1397
+ ...widgets ? { widgets } : {},
1398
+ widgetFiles
1399
+ });
1400
+ }
1401
+ const title = config.title ?? "emroute";
1402
+ const shellBase = spa === "root" || spa === "only" ? appBase : htmlBase;
1403
+ let shell = await Emroute.resolveShell(runtime, title, shellBase);
1404
+ if ((await runtime.query("/main.css")).status !== 404) {
1405
+ shell = shell.replace("</head>", ` <link rel="stylesheet" href="/main.css">
1406
+ </head>`);
1407
+ }
1408
+ return new Emroute(ssrHtmlRenderer, ssrMdRenderer, shell, runtime, htmlBase, mdBase, appBase, spa, title);
1409
+ }
1410
+ async handleRequest(req) {
1411
+ const url = new URL(req.url);
1412
+ const pathname = url.pathname;
1413
+ const mdPrefix = this.mdBase + "/";
1414
+ const htmlPrefix = this.htmlBase + "/";
1415
+ const appPrefix = this.appBase + "/";
1416
+ if (this.mdRenderer && (pathname.startsWith(mdPrefix) || pathname === this.mdBase)) {
1417
+ const routePath = pathname === this.mdBase ? "/" : pathname.slice(this.mdBase.length);
1418
+ if (routePath.length > 1 && routePath.endsWith("/")) {
1419
+ const canonical = this.mdBase + routePath.slice(0, -1) + (url.search || "");
1420
+ return Response.redirect(new URL(canonical, url.origin), 301);
1421
+ }
1422
+ try {
1423
+ const routeUrl = new URL(routePath + url.search, url.origin);
1424
+ const { content, status, redirect } = await this.mdRenderer.render(routeUrl, req.signal);
1425
+ if (redirect) {
1426
+ const target = redirect.startsWith("/") ? this.mdBase + redirect : redirect;
1427
+ return Response.redirect(new URL(target, url.origin), status);
1428
+ }
1429
+ return new Response(rewriteMdLinks(content, this.mdBase, [this.mdBase, this.htmlBase]), {
1430
+ status,
1431
+ headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
1432
+ });
1433
+ } catch (e) {
1434
+ console.error(`[emroute] Error rendering ${pathname}:`, e);
1435
+ return new Response("Internal Server Error", { status: 500 });
1436
+ }
1437
+ }
1438
+ if (this.htmlRenderer && (pathname.startsWith(htmlPrefix) || pathname === this.htmlBase)) {
1439
+ const routePath = pathname === this.htmlBase ? "/" : pathname.slice(this.htmlBase.length);
1440
+ if (routePath.length > 1 && routePath.endsWith("/")) {
1441
+ const canonical = this.htmlBase + routePath.slice(0, -1) + (url.search || "");
1442
+ return Response.redirect(new URL(canonical, url.origin), 301);
1443
+ }
1444
+ try {
1445
+ const routeUrl = new URL(routePath + url.search, url.origin);
1446
+ const result = await this.htmlRenderer.render(routeUrl, req.signal);
1447
+ if (result.redirect) {
1448
+ const target = result.redirect.startsWith("/") ? this.htmlBase + result.redirect : result.redirect;
1449
+ return Response.redirect(new URL(target, url.origin), result.status);
1450
+ }
1451
+ const ssrTitle = result.title ?? this.title;
1452
+ const html = Emroute.injectSsrContent(this.shell, result.content, ssrTitle, pathname);
1453
+ return new Response(html, {
1454
+ status: result.status,
1455
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1456
+ });
1457
+ } catch (e) {
1458
+ console.error(`[emroute] Error rendering ${pathname}:`, e);
1459
+ return new Response("Internal Server Error", { status: 500 });
1460
+ }
1461
+ }
1462
+ if (pathname.startsWith(appPrefix) || pathname === this.appBase) {
1463
+ return new Response(this.shell, {
1464
+ status: 200,
1465
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1466
+ });
1467
+ }
1468
+ if (pathname.startsWith(htmlPrefix) || pathname === this.htmlBase || pathname.startsWith(mdPrefix) || pathname === this.mdBase) {
1469
+ return new Response(this.shell, {
1470
+ status: 200,
1471
+ headers: { "Content-Type": "text/html; charset=utf-8" }
1472
+ });
1473
+ }
1474
+ const lastSegment = pathname.split("/").pop() ?? "";
1475
+ if (lastSegment.includes(".")) {
1476
+ const fileResponse = await this.runtime.handle(pathname);
1477
+ if (fileResponse.status === 200)
1478
+ return fileResponse;
1479
+ return null;
1480
+ }
1481
+ const base = this.spa === "root" || this.spa === "only" ? this.appBase : this.htmlBase;
1482
+ const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
1483
+ return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
1484
+ }
1485
+ static extractWidgetExport(mod) {
1486
+ for (const value of Object.values(mod)) {
1487
+ if (!value)
1488
+ continue;
1489
+ if (typeof value === "object" && "getData" in value) {
1490
+ return value;
1491
+ }
1492
+ if (typeof value === "function" && value.prototype?.getData) {
1493
+ return new value;
1494
+ }
1495
+ }
1496
+ return null;
1497
+ }
1498
+ static async importWidgets(entries, runtime) {
1499
+ const registry = new WidgetRegistry;
1500
+ const widgetFiles = {};
1501
+ for (const entry of entries) {
1502
+ try {
1503
+ const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
1504
+ const mod = await runtime.loadModule(runtimePath);
1505
+ const instance = Emroute.extractWidgetExport(mod);
1506
+ if (!instance)
1507
+ continue;
1508
+ registry.add(instance);
1509
+ const inlined = mod.__files;
1510
+ if (inlined && typeof inlined === "object") {
1511
+ widgetFiles[entry.name] = inlined;
1512
+ } else if (entry.files) {
1513
+ widgetFiles[entry.name] = entry.files;
1514
+ }
1515
+ } catch (e) {
1516
+ console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
1517
+ if (entry.files)
1518
+ widgetFiles[entry.name] = entry.files;
1519
+ }
1520
+ }
1521
+ return { registry, widgetFiles };
1522
+ }
1523
+ static buildHtmlShell(title, htmlBase) {
1524
+ const baseTag = htmlBase ? `
1525
+ <base href="${escapeHtml(htmlBase)}/">` : "";
1526
+ return `<!DOCTYPE html>
1527
+ <html>
1528
+ <head>${baseTag}
1529
+ <meta charset="utf-8">
1530
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1531
+ <title>${escapeHtml(title)}</title>
1532
+ <style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
1533
+ </head>
1534
+ <body>
1535
+ <router-slot></router-slot>
1536
+ </body>
1537
+ </html>`;
1538
+ }
1539
+ static injectSsrContent(html, content, title, ssrRoute) {
1540
+ const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
1541
+ if (!slotPattern.test(html))
1542
+ return html;
1543
+ const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
1544
+ html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
1545
+ if (title) {
1546
+ html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
1547
+ }
1548
+ return html;
1549
+ }
1550
+ static async resolveShell(runtime, title, htmlBase) {
1551
+ const response = await runtime.query("/index.html");
1552
+ if (response.status !== 404)
1553
+ return await response.text();
1554
+ return Emroute.buildHtmlShell(title, htmlBase);
1555
+ }
1640
1556
  }
1641
1557
 
1642
- // dist/src/route/route-tree.util.js
1558
+ // dist/core/util/route-tree.util.js
1643
1559
  function resolveTargetNode(node, name, isRoot) {
1644
1560
  if (name === "index") {
1645
1561
  if (isRoot)
@@ -1658,34 +1574,81 @@ function resolveTargetNode(node, name, isRoot) {
1658
1574
  }
1659
1575
 
1660
1576
  // dist/runtime/abstract.runtime.js
1577
+ var CONTENT_TYPES = new Map([
1578
+ [".html", "text/html; charset=utf-8"],
1579
+ [".css", "text/css; charset=utf-8"],
1580
+ [".js", "application/javascript; charset=utf-8"],
1581
+ [".mjs", "application/javascript; charset=utf-8"],
1582
+ [".ts", "text/typescript; charset=utf-8"],
1583
+ [".json", "application/json; charset=utf-8"],
1584
+ [".md", "text/plain; charset=utf-8"],
1585
+ [".txt", "text/plain; charset=utf-8"],
1586
+ [".wasm", "application/wasm"],
1587
+ [".map", "application/json; charset=utf-8"],
1588
+ [".png", "image/png"],
1589
+ [".jpg", "image/jpeg"],
1590
+ [".jpeg", "image/jpeg"],
1591
+ [".gif", "image/gif"],
1592
+ [".svg", "image/svg+xml"],
1593
+ [".ico", "image/x-icon"],
1594
+ [".webp", "image/webp"],
1595
+ [".avif", "image/avif"],
1596
+ [".woff", "font/woff"],
1597
+ [".woff2", "font/woff2"],
1598
+ [".ttf", "font/ttf"]
1599
+ ]);
1661
1600
  var DEFAULT_ROUTES_DIR = "/routes";
1662
1601
  var DEFAULT_WIDGETS_DIR = "/widgets";
1663
- var ROUTES_MANIFEST_PATH = "/routes.manifest.json";
1664
- var WIDGETS_MANIFEST_PATH = "/widgets.manifest.json";
1665
- var Runtime = class {
1602
+ var DEFAULT_ELEMENTS_DIR = "/elements";
1603
+ class Runtime {
1666
1604
  config;
1667
1605
  constructor(config = {}) {
1668
1606
  this.config = config;
1669
1607
  this.config = config;
1670
1608
  }
1671
- /** Write. Defaults to PUT; pass `{ method: "DELETE" }` etc. to override. */
1672
1609
  command(resource, options) {
1673
1610
  const path = typeof resource === "string" ? resource : new URL(resource instanceof Request ? resource.url : resource.toString()).pathname;
1674
- const result = this.handle(resource, { method: "PUT", ...options });
1611
+ const method = options?.method ?? "PUT";
1612
+ const isDelete = method === "DELETE";
1613
+ const result = this.handle(resource, { method, ...options });
1675
1614
  const routesDir = this.config.routesDir ?? DEFAULT_ROUTES_DIR;
1615
+ const widgetsDir = this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR;
1616
+ const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
1676
1617
  if (path.startsWith(routesDir + "/")) {
1677
1618
  return result.then(async (res) => {
1678
- await this.mergeRouteIntoManifest(path, routesDir);
1619
+ if (isDelete) {
1620
+ await this.pruneRouteFromManifest(path, routesDir);
1621
+ } else {
1622
+ await this.mergeRouteIntoManifest(path, routesDir);
1623
+ await this.retranspileIfNeeded(path, routesDir, "route");
1624
+ }
1625
+ return res;
1626
+ });
1627
+ }
1628
+ if (path.startsWith(widgetsDir + "/")) {
1629
+ return result.then(async (res) => {
1630
+ if (isDelete) {
1631
+ await this.pruneWidgetFromManifest(path, widgetsDir);
1632
+ } else {
1633
+ await this.mergeWidgetIntoManifest(path, widgetsDir);
1634
+ await this.retranspileIfNeeded(path, widgetsDir, "widget");
1635
+ }
1636
+ return res;
1637
+ });
1638
+ }
1639
+ if (path.startsWith(elementsDir + "/")) {
1640
+ return result.then(async (res) => {
1641
+ if (isDelete) {
1642
+ await this.pruneElementFromManifest(path, elementsDir);
1643
+ } else {
1644
+ await this.mergeElementIntoManifest(path, elementsDir);
1645
+ await this.retranspileIfNeeded(path, elementsDir, "element");
1646
+ }
1679
1647
  return res;
1680
1648
  });
1681
1649
  }
1682
1650
  return result;
1683
1651
  }
1684
- /**
1685
- * Parse a single route file path and merge it into the stored manifest.
1686
- * Avoids a full directory rescan — just reads the current manifest,
1687
- * inserts the new entry, and writes it back.
1688
- */
1689
1652
  async mergeRouteIntoManifest(filePath, routesDir) {
1690
1653
  const relativePath = filePath.slice(routesDir.length + 1);
1691
1654
  const parts = relativePath.split("/");
@@ -1694,64 +1657,336 @@ var Runtime = class {
1694
1657
  const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
1695
1658
  if (!match)
1696
1659
  return;
1697
- const [, name, kind, ext] = match;
1698
- const response = await this.handle(ROUTES_MANIFEST_PATH);
1699
- const tree = response.status === 404 ? {} : await response.json();
1700
- let node = tree;
1701
- for (const dir of dirSegments) {
1702
- if (dir.startsWith("[") && dir.endsWith("]")) {
1703
- const param = dir.slice(1, -1);
1704
- node.dynamic ??= { param, child: {} };
1705
- node = node.dynamic.child;
1660
+ const [, name, kind, ext] = match;
1661
+ const response = await this.handle(ROUTES_MANIFEST_PATH);
1662
+ const tree = response.status === 404 ? {} : await response.json();
1663
+ let node = tree;
1664
+ for (const dir of dirSegments) {
1665
+ if (dir.startsWith("[") && dir.endsWith("]")) {
1666
+ const param = dir.slice(1, -1);
1667
+ node.dynamic ??= { param, child: {} };
1668
+ node = node.dynamic.child;
1669
+ } else {
1670
+ node.children ??= {};
1671
+ node.children[dir] ??= {};
1672
+ node = node.children[dir];
1673
+ }
1674
+ }
1675
+ if (kind === "error") {
1676
+ node.errorBoundary = filePath;
1677
+ } else {
1678
+ const target = resolveTargetNode(node, name, dirSegments.length === 0);
1679
+ if (kind === "redirect") {
1680
+ target.redirect = filePath;
1681
+ } else {
1682
+ target.files ??= {};
1683
+ target.files[ext] = filePath;
1684
+ }
1685
+ }
1686
+ this.routesManifestCache = null;
1687
+ await this.handle(ROUTES_MANIFEST_PATH, {
1688
+ method: "PUT",
1689
+ body: JSON.stringify(tree)
1690
+ });
1691
+ }
1692
+ async pruneRouteFromManifest(filePath, routesDir) {
1693
+ const relativePath = filePath.slice(routesDir.length + 1);
1694
+ const parts = relativePath.split("/");
1695
+ const filename = parts[parts.length - 1];
1696
+ const dirSegments = parts.slice(0, -1);
1697
+ const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
1698
+ if (!match)
1699
+ return;
1700
+ const [, name, kind, ext] = match;
1701
+ const response = await this.handle(ROUTES_MANIFEST_PATH);
1702
+ if (response.status === 404)
1703
+ return;
1704
+ const tree = await response.json();
1705
+ const ancestors = [];
1706
+ let node = tree;
1707
+ for (const dir of dirSegments) {
1708
+ if (dir.startsWith("[") && dir.endsWith("]")) {
1709
+ if (!node.dynamic)
1710
+ return;
1711
+ ancestors.push({ node, key: dir, via: "dynamic" });
1712
+ node = node.dynamic.child;
1713
+ } else {
1714
+ if (!node.children?.[dir])
1715
+ return;
1716
+ ancestors.push({ node, key: dir, via: "children" });
1717
+ node = node.children[dir];
1718
+ }
1719
+ }
1720
+ if (kind === "error") {
1721
+ if (node.errorBoundary === filePath)
1722
+ delete node.errorBoundary;
1723
+ } else {
1724
+ const isRoot = dirSegments.length === 0;
1725
+ const target = this.findTargetNode(node, name, isRoot);
1726
+ if (!target)
1727
+ return;
1728
+ if (kind === "redirect") {
1729
+ if (target.redirect === filePath)
1730
+ delete target.redirect;
1731
+ } else {
1732
+ if (target.files?.[ext] === filePath) {
1733
+ delete target.files[ext];
1734
+ if (Object.keys(target.files).length === 0)
1735
+ delete target.files;
1736
+ }
1737
+ }
1738
+ if (target !== node && this.isEmptyNode(target)) {
1739
+ if (name === "index" && !isRoot) {
1740
+ delete node.wildcard;
1741
+ } else if (name.startsWith("[") && name.endsWith("]")) {
1742
+ delete node.dynamic;
1743
+ } else if (node.children) {
1744
+ delete node.children[name];
1745
+ if (Object.keys(node.children).length === 0)
1746
+ delete node.children;
1747
+ }
1748
+ }
1749
+ }
1750
+ for (let i = ancestors.length - 1;i >= 0; i--) {
1751
+ const { node: parent, key, via } = ancestors[i];
1752
+ const child = via === "dynamic" ? parent.dynamic?.child : parent.children?.[key];
1753
+ if (child && this.isEmptyNode(child)) {
1754
+ if (via === "dynamic") {
1755
+ delete parent.dynamic;
1756
+ } else if (parent.children) {
1757
+ delete parent.children[key];
1758
+ if (Object.keys(parent.children).length === 0)
1759
+ delete parent.children;
1760
+ }
1761
+ }
1762
+ }
1763
+ this.routesManifestCache = null;
1764
+ await this.handle(ROUTES_MANIFEST_PATH, {
1765
+ method: "PUT",
1766
+ body: JSON.stringify(tree)
1767
+ });
1768
+ }
1769
+ findTargetNode(node, name, isRoot) {
1770
+ if (name === "index") {
1771
+ return isRoot ? node : node.wildcard?.child ?? null;
1772
+ }
1773
+ if (name.startsWith("[") && name.endsWith("]")) {
1774
+ return node.dynamic?.child ?? null;
1775
+ }
1776
+ return node.children?.[name] ?? null;
1777
+ }
1778
+ isEmptyNode(node) {
1779
+ return !node.files && !node.errorBoundary && !node.redirect && !node.children && !node.dynamic && !node.wildcard;
1780
+ }
1781
+ async pruneWidgetFromManifest(filePath, widgetsDir) {
1782
+ const relativePath = filePath.slice(widgetsDir.length + 1);
1783
+ const parts = relativePath.split("/");
1784
+ if (parts.length !== 2)
1785
+ return;
1786
+ const [dirName, filename] = parts;
1787
+ const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
1788
+ if (!match)
1789
+ return;
1790
+ const [, name, ext] = match;
1791
+ if (name !== dirName)
1792
+ return;
1793
+ const response = await this.handle(WIDGETS_MANIFEST_PATH);
1794
+ if (response.status === 404)
1795
+ return;
1796
+ const entries = await response.json();
1797
+ if (ext === "ts" || ext === "js") {
1798
+ const idx = entries.findIndex((e) => e.name === name);
1799
+ if (idx === -1)
1800
+ return;
1801
+ entries.splice(idx, 1);
1802
+ } else {
1803
+ const entry = entries.find((e) => e.name === name);
1804
+ if (!entry?.files)
1805
+ return;
1806
+ delete entry.files[ext];
1807
+ if (Object.keys(entry.files).length === 0)
1808
+ delete entry.files;
1809
+ }
1810
+ this.widgetsManifestCache = null;
1811
+ await this.handle(WIDGETS_MANIFEST_PATH, {
1812
+ method: "PUT",
1813
+ body: JSON.stringify(entries)
1814
+ });
1815
+ }
1816
+ async pruneElementFromManifest(filePath, elementsDir) {
1817
+ const relativePath = filePath.slice(elementsDir.length + 1);
1818
+ const parts = relativePath.split("/");
1819
+ if (parts.length !== 2)
1820
+ return;
1821
+ const [dirName, filename] = parts;
1822
+ const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
1823
+ if (!match)
1824
+ return;
1825
+ const [, name] = match;
1826
+ if (name !== dirName)
1827
+ return;
1828
+ const response = await this.handle(ELEMENTS_MANIFEST_PATH);
1829
+ if (response.status === 404)
1830
+ return;
1831
+ const entries = await response.json();
1832
+ const idx = entries.findIndex((e) => e.name === name);
1833
+ if (idx === -1)
1834
+ return;
1835
+ entries.splice(idx, 1);
1836
+ this.elementsManifestCache = null;
1837
+ await this.handle(ELEMENTS_MANIFEST_PATH, {
1838
+ method: "PUT",
1839
+ body: JSON.stringify(entries)
1840
+ });
1841
+ }
1842
+ async retranspileIfNeeded(filePath, dir, kind) {
1843
+ if (filePath.endsWith(".js"))
1844
+ return;
1845
+ const relativePath = filePath.slice(dir.length + 1);
1846
+ const parts = relativePath.split("/");
1847
+ const filename = parts[parts.length - 1];
1848
+ let jsPath;
1849
+ if (kind === "route") {
1850
+ const match = filename.match(/^(.+?)\.(page)\.(ts|html|md|css)$/);
1851
+ if (!match)
1852
+ return;
1853
+ const [, name] = match;
1854
+ jsPath = `${dir}/${parts.slice(0, -1).join("/")}${parts.length > 1 ? "/" : ""}${name}.page.js`;
1855
+ } else if (kind === "widget") {
1856
+ const match = filename.match(/^(.+?)\.(widget)\.(ts|html|md|css)$/);
1857
+ if (!match)
1858
+ return;
1859
+ const [, name] = match;
1860
+ jsPath = `${dir}/${name}/${name}.widget.js`;
1861
+ } else {
1862
+ const match = filename.match(/^(.+?)\.(element)\.ts$/);
1863
+ if (!match)
1864
+ return;
1865
+ const [, name] = match;
1866
+ jsPath = `${dir}/${name}/${name}.element.js`;
1867
+ }
1868
+ const jsResponse = await this.handle(jsPath);
1869
+ if (jsResponse.status === 404)
1870
+ return;
1871
+ const tsPath = jsPath.replace(/\.js$/, ".ts");
1872
+ let tsSource;
1873
+ try {
1874
+ tsSource = await this.query(tsPath, { as: "text" });
1875
+ } catch {
1876
+ return;
1877
+ }
1878
+ const companionExts = kind === "element" ? [] : ["html", "md", "css"];
1879
+ const files = {};
1880
+ for (const ext of companionExts) {
1881
+ const companionPath = tsPath.replace(/\.ts$/, `.${ext}`);
1882
+ try {
1883
+ files[ext] = await this.query(companionPath, { as: "text" });
1884
+ } catch {}
1885
+ }
1886
+ let jsCode;
1887
+ try {
1888
+ jsCode = await this.transpile(tsSource);
1889
+ } catch {
1890
+ return;
1891
+ }
1892
+ if (Object.keys(files).length > 0) {
1893
+ const entries = Object.entries(files).map(([k, v]) => `${k}: \`${v.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\``).join(", ");
1894
+ jsCode += `
1895
+ export const __files = { ${entries} };
1896
+ `;
1897
+ }
1898
+ await this.handle(jsPath, { method: "PUT", body: jsCode });
1899
+ }
1900
+ loadModule(_path) {
1901
+ throw new Error(`loadModule not implemented for ${this.constructor.name}`);
1902
+ }
1903
+ transpile(_source) {
1904
+ throw new Error(`transpile not implemented for ${this.constructor.name}`);
1905
+ }
1906
+ async mergeWidgetIntoManifest(filePath, widgetsDir) {
1907
+ const relativePath = filePath.slice(widgetsDir.length + 1);
1908
+ const parts = relativePath.split("/");
1909
+ if (parts.length !== 2)
1910
+ return;
1911
+ const [dirName, filename] = parts;
1912
+ const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
1913
+ if (!match)
1914
+ return;
1915
+ const [, name, ext] = match;
1916
+ if (name !== dirName)
1917
+ return;
1918
+ const response = await this.handle(WIDGETS_MANIFEST_PATH);
1919
+ const entries = response.status === 404 ? [] : await response.json();
1920
+ const prefix = widgetsDir.replace(/^\//, "");
1921
+ if (ext === "ts" || ext === "js") {
1922
+ let entry = entries.find((e) => e.name === name);
1923
+ if (!entry) {
1924
+ entry = {
1925
+ name,
1926
+ modulePath: `${prefix}/${name}/${filename}`,
1927
+ tagName: `widget-${name}`
1928
+ };
1929
+ entries.push(entry);
1930
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1706
1931
  } else {
1707
- node.children ??= {};
1708
- node.children[dir] ??= {};
1709
- node = node.children[dir];
1932
+ entry.modulePath = `${prefix}/${name}/${filename}`;
1710
1933
  }
1711
- }
1712
- if (kind === "error") {
1713
- node.errorBoundary = filePath;
1714
1934
  } else {
1715
- const target = resolveTargetNode(node, name, dirSegments.length === 0);
1716
- if (kind === "redirect") {
1717
- target.redirect = filePath;
1718
- } else {
1719
- target.files ??= {};
1720
- target.files[ext] = filePath;
1721
- }
1935
+ const entry = entries.find((e) => e.name === name);
1936
+ if (!entry)
1937
+ return;
1938
+ entry.files ??= {};
1939
+ entry.files[ext] = `${prefix}/${name}/${filename}`;
1722
1940
  }
1723
- this.routesManifestCache = null;
1724
- await this.handle(ROUTES_MANIFEST_PATH, {
1941
+ this.widgetsManifestCache = null;
1942
+ await this.handle(WIDGETS_MANIFEST_PATH, {
1725
1943
  method: "PUT",
1726
- body: JSON.stringify(tree)
1944
+ body: JSON.stringify(entries)
1727
1945
  });
1728
1946
  }
1729
- /**
1730
- * Dynamically import a module from this runtime's storage.
1731
- * Used by the server for SSR imports of `.page.ts` and `.widget.ts` files.
1732
- */
1733
- loadModule(_path) {
1734
- throw new Error(`loadModule not implemented for ${this.constructor.name}`);
1735
- }
1736
- /**
1737
- * Transpile TypeScript source to JavaScript.
1738
- * Used by the build step to produce browser-loadable .js modules.
1739
- */
1740
- transpile(_source) {
1741
- throw new Error(`transpile not implemented for ${this.constructor.name}`);
1947
+ async mergeElementIntoManifest(filePath, elementsDir) {
1948
+ const relativePath = filePath.slice(elementsDir.length + 1);
1949
+ const parts = relativePath.split("/");
1950
+ if (parts.length !== 2)
1951
+ return;
1952
+ const [dirName, filename] = parts;
1953
+ const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
1954
+ if (!match)
1955
+ return;
1956
+ const [, name] = match;
1957
+ if (name !== dirName)
1958
+ return;
1959
+ if (!name.includes("-"))
1960
+ return;
1961
+ const response = await this.handle(ELEMENTS_MANIFEST_PATH);
1962
+ const entries = response.status === 404 ? [] : await response.json();
1963
+ const prefix = elementsDir.replace(/^\//, "");
1964
+ let entry = entries.find((e) => e.name === name);
1965
+ if (!entry) {
1966
+ entry = {
1967
+ name,
1968
+ modulePath: `${prefix}/${name}/${filename}`,
1969
+ tagName: name
1970
+ };
1971
+ entries.push(entry);
1972
+ entries.sort((a, b) => a.name.localeCompare(b.name));
1973
+ } else {
1974
+ entry.modulePath = `${prefix}/${name}/${filename}`;
1975
+ }
1976
+ this.elementsManifestCache = null;
1977
+ await this.handle(ELEMENTS_MANIFEST_PATH, {
1978
+ method: "PUT",
1979
+ body: JSON.stringify(entries)
1980
+ });
1742
1981
  }
1743
- // ── Manifest resolution ─────────────────────────────────────────────
1744
1982
  routesManifestCache = null;
1745
1983
  widgetsManifestCache = null;
1746
- /** Clear cached manifests so the next query triggers a fresh scan. */
1984
+ elementsManifestCache = null;
1747
1985
  invalidateManifests() {
1748
1986
  this.routesManifestCache = null;
1749
1987
  this.widgetsManifestCache = null;
1988
+ this.elementsManifestCache = null;
1750
1989
  }
1751
- /**
1752
- * Resolve the routes manifest. Called when the concrete runtime returns
1753
- * 404 for ROUTES_MANIFEST_PATH. Scans `config.routesDir` (or default).
1754
- */
1755
1990
  async resolveRoutesManifest() {
1756
1991
  if (this.routesManifestCache)
1757
1992
  return this.routesManifestCache.clone();
@@ -1764,10 +1999,6 @@ var Runtime = class {
1764
1999
  this.routesManifestCache = Response.json(tree);
1765
2000
  return this.routesManifestCache.clone();
1766
2001
  }
1767
- /**
1768
- * Resolve the widgets manifest. Called when the concrete runtime returns
1769
- * 404 for WIDGETS_MANIFEST_PATH. Scans `config.widgetsDir` (or default).
1770
- */
1771
2002
  async resolveWidgetsManifest() {
1772
2003
  if (this.widgetsManifestCache)
1773
2004
  return this.widgetsManifestCache.clone();
@@ -1780,8 +2011,19 @@ var Runtime = class {
1780
2011
  this.widgetsManifestCache = Response.json(entries);
1781
2012
  return this.widgetsManifestCache.clone();
1782
2013
  }
1783
- // ── Scanning ──────────────────────────────────────────────────────────
1784
- async *walkDirectory(dir) {
2014
+ async resolveElementsManifest() {
2015
+ if (this.elementsManifestCache)
2016
+ return this.elementsManifestCache.clone();
2017
+ const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
2018
+ const dirResponse = await this.query(elementsDir + "/");
2019
+ if (dirResponse.status === 404) {
2020
+ return new Response("Not Found", { status: 404 });
2021
+ }
2022
+ const entries = await this.scanElements(elementsDir, elementsDir.replace(/^\//, ""));
2023
+ this.elementsManifestCache = Response.json(entries);
2024
+ return this.elementsManifestCache.clone();
2025
+ }
2026
+ async* walkDirectory(dir) {
1785
2027
  const trailingDir = dir.endsWith("/") ? dir : dir + "/";
1786
2028
  const response = await this.query(trailingDir);
1787
2029
  const entries = await response.json();
@@ -1794,10 +2036,6 @@ var Runtime = class {
1794
2036
  }
1795
2037
  }
1796
2038
  }
1797
- /**
1798
- * Scan a routes directory and build a RouteNode tree.
1799
- * The filesystem structure maps directly to the tree — no intermediate array.
1800
- */
1801
2039
  async scanRoutes(routesDir) {
1802
2040
  const root = {};
1803
2041
  const allFiles = [];
@@ -1880,271 +2118,41 @@ var Runtime = class {
1880
2118
  entries.sort((a, b) => a.name.localeCompare(b.name));
1881
2119
  return entries;
1882
2120
  }
1883
- };
1884
-
1885
- // dist/server/emroute.server.js
1886
- function createModuleLoaders(tree, runtime) {
1887
- const paths = /* @__PURE__ */ new Set();
1888
- function walk(node) {
1889
- const modulePath = node.files?.ts ?? node.files?.js;
1890
- if (modulePath)
1891
- paths.add(modulePath);
1892
- if (node.redirect)
1893
- paths.add(node.redirect);
1894
- if (node.errorBoundary)
1895
- paths.add(node.errorBoundary);
1896
- if (node.children) {
1897
- for (const child of Object.values(node.children))
1898
- walk(child);
1899
- }
1900
- if (node.dynamic)
1901
- walk(node.dynamic.child);
1902
- if (node.wildcard)
1903
- walk(node.wildcard.child);
1904
- }
1905
- walk(tree);
1906
- const loaders = {};
1907
- for (const path of paths) {
1908
- loaders[path] = () => runtime.loadModule(path);
1909
- }
1910
- return loaders;
1911
- }
1912
- function extractWidgetExport(mod) {
1913
- for (const value of Object.values(mod)) {
1914
- if (!value)
1915
- continue;
1916
- if (typeof value === "object" && "getData" in value) {
1917
- return value;
1918
- }
1919
- if (typeof value === "function" && value.prototype?.getData) {
1920
- return new value();
1921
- }
1922
- }
1923
- return null;
1924
- }
1925
- async function importWidgets(entries, runtime, manual) {
1926
- const registry = new WidgetRegistry();
1927
- const widgetFiles = {};
1928
- for (const entry of entries) {
1929
- try {
1930
- const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
1931
- const mod = await runtime.loadModule(runtimePath);
1932
- const instance = extractWidgetExport(mod);
1933
- if (!instance)
2121
+ async scanElements(elementsDir, pathPrefix) {
2122
+ const entries = [];
2123
+ const trailingDir = elementsDir.endsWith("/") ? elementsDir : elementsDir + "/";
2124
+ const response = await this.query(trailingDir);
2125
+ const listing = await response.json();
2126
+ for (const item of listing) {
2127
+ if (!item.endsWith("/"))
2128
+ continue;
2129
+ const name = item.slice(0, -1);
2130
+ if (!name.includes("-")) {
2131
+ console.warn(`[emroute] Skipping element "${name}": custom element names must contain a hyphen (e.g. "my-element")`);
1934
2132
  continue;
1935
- registry.add(instance);
1936
- const inlined = mod.__files;
1937
- if (inlined && typeof inlined === "object") {
1938
- widgetFiles[entry.name] = inlined;
1939
- } else if (entry.files) {
1940
- widgetFiles[entry.name] = entry.files;
1941
- }
1942
- } catch (e) {
1943
- console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
1944
- if (entry.files)
1945
- widgetFiles[entry.name] = entry.files;
1946
- }
1947
- }
1948
- if (manual) {
1949
- for (const widget of manual) {
1950
- registry.add(widget);
1951
- }
1952
- }
1953
- return { registry, widgetFiles };
1954
- }
1955
- function buildHtmlShell(title, htmlBase) {
1956
- const baseTag = htmlBase ? `
1957
- <base href="${escapeHtml(htmlBase)}/">` : "";
1958
- return `<!DOCTYPE html>
1959
- <html>
1960
- <head>${baseTag}
1961
- <meta charset="utf-8">
1962
- <meta name="viewport" content="width=device-width, initial-scale=1">
1963
- <title>${escapeHtml(title)}</title>
1964
- <style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
1965
- </head>
1966
- <body>
1967
- <router-slot></router-slot>
1968
- </body>
1969
- </html>`;
1970
- }
1971
- function injectSsrContent(html, content, title, ssrRoute) {
1972
- const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
1973
- if (!slotPattern.test(html))
1974
- return html;
1975
- const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
1976
- html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
1977
- if (title) {
1978
- html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
1979
- }
1980
- return html;
1981
- }
1982
- async function resolveShell(runtime, title, htmlBase) {
1983
- const response = await runtime.query("/index.html");
1984
- if (response.status !== 404)
1985
- return await response.text();
1986
- return buildHtmlShell(title, htmlBase);
1987
- }
1988
- async function createEmrouteServer(config, runtime) {
1989
- const { spa = "root" } = config;
1990
- const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
1991
- let routeTree;
1992
- if (config.routeTree) {
1993
- routeTree = config.routeTree;
1994
- } else {
1995
- const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
1996
- if (manifestResponse.status === 404) {
1997
- throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. Provide routeTree in config or ensure the runtime produces it.`);
1998
- }
1999
- routeTree = await manifestResponse.json();
2000
- }
2001
- const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
2002
- const resolver = new RouteTrie(routeTree);
2003
- let widgets = config.widgets;
2004
- let widgetFiles = {};
2005
- let discoveredWidgetEntries = [];
2006
- const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
2007
- if (widgetsResponse.status !== 404) {
2008
- discoveredWidgetEntries = await widgetsResponse.json();
2009
- if (config.widgets) {
2010
- widgets = config.widgets;
2011
- for (const entry of discoveredWidgetEntries) {
2012
- if (entry.files)
2013
- widgetFiles[entry.name] = entry.files;
2014
- }
2015
- } else {
2016
- const imported = await importWidgets(discoveredWidgetEntries, runtime);
2017
- widgets = imported.registry;
2018
- widgetFiles = imported.widgetFiles;
2019
- }
2020
- }
2021
- let ssrHtmlRouter = null;
2022
- let ssrMdRouter = null;
2023
- function buildSsrRouters() {
2024
- if (spa === "only") {
2025
- ssrHtmlRouter = null;
2026
- ssrMdRouter = null;
2027
- return;
2028
- }
2029
- ssrHtmlRouter = new SsrHtmlRouter(resolver, {
2030
- fileReader: (path) => runtime.query(path, { as: "text" }),
2031
- moduleLoaders,
2032
- markdownRenderer: config.markdownRenderer,
2033
- extendContext: config.extendContext,
2034
- widgets,
2035
- widgetFiles
2036
- });
2037
- ssrMdRouter = new SsrMdRouter(resolver, {
2038
- fileReader: (path) => runtime.query(path, { as: "text" }),
2039
- moduleLoaders,
2040
- extendContext: config.extendContext,
2041
- widgets,
2042
- widgetFiles
2043
- });
2044
- }
2045
- buildSsrRouters();
2046
- const title = config.title ?? "emroute";
2047
- let shell = await resolveShell(runtime, title, htmlBase);
2048
- if ((await runtime.query("/main.css")).status !== 404) {
2049
- shell = shell.replace("</head>", ' <link rel="stylesheet" href="/main.css">\n</head>');
2050
- }
2051
- async function handleRequest(req) {
2052
- const url = new URL(req.url);
2053
- const pathname = url.pathname;
2054
- const mdPrefix = mdBase + "/";
2055
- const htmlPrefix = htmlBase + "/";
2056
- const appPrefix = appBase + "/";
2057
- if (ssrMdRouter && (pathname.startsWith(mdPrefix) || pathname === mdBase)) {
2058
- const routePath = pathname === mdBase ? "/" : pathname.slice(mdBase.length);
2059
- if (routePath.length > 1 && routePath.endsWith("/")) {
2060
- const canonical = mdBase + routePath.slice(0, -1) + (url.search || "");
2061
- return Response.redirect(new URL(canonical, url.origin), 301);
2062
- }
2063
- try {
2064
- const routeUrl = new URL(routePath + url.search, url.origin);
2065
- const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
2066
- if (redirect) {
2067
- const target = redirect.startsWith("/") ? mdBase + redirect : redirect;
2068
- return Response.redirect(new URL(target, url.origin), status);
2069
- }
2070
- return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
2071
- status,
2072
- headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
2073
- });
2074
- } catch (e) {
2075
- console.error(`[emroute] Error rendering ${pathname}:`, e);
2076
- return new Response("Internal Server Error", { status: 500 });
2077
- }
2078
- }
2079
- if (ssrHtmlRouter && (pathname.startsWith(htmlPrefix) || pathname === htmlBase)) {
2080
- const routePath = pathname === htmlBase ? "/" : pathname.slice(htmlBase.length);
2081
- if (routePath.length > 1 && routePath.endsWith("/")) {
2082
- const canonical = htmlBase + routePath.slice(0, -1) + (url.search || "");
2083
- return Response.redirect(new URL(canonical, url.origin), 301);
2084
2133
  }
2085
- try {
2086
- const routeUrl = new URL(routePath + url.search, url.origin);
2087
- const result = await ssrHtmlRouter.render(routeUrl, req.signal);
2088
- if (result.redirect) {
2089
- const target = result.redirect.startsWith("/") ? htmlBase + result.redirect : result.redirect;
2090
- return Response.redirect(new URL(target, url.origin), result.status);
2091
- }
2092
- const ssrTitle = result.title ?? title;
2093
- const html = injectSsrContent(shell, result.content, ssrTitle, pathname);
2094
- return new Response(html, {
2095
- status: result.status,
2096
- headers: { "Content-Type": "text/html; charset=utf-8" }
2097
- });
2098
- } catch (e) {
2099
- console.error(`[emroute] Error rendering ${pathname}:`, e);
2100
- return new Response("Internal Server Error", { status: 500 });
2134
+ let moduleFile = `${name}.element.ts`;
2135
+ let modulePath = `${trailingDir}${name}/${moduleFile}`;
2136
+ if ((await this.query(modulePath)).status === 404) {
2137
+ moduleFile = `${name}.element.js`;
2138
+ modulePath = `${trailingDir}${name}/${moduleFile}`;
2139
+ if ((await this.query(modulePath)).status === 404)
2140
+ continue;
2101
2141
  }
2102
- }
2103
- if (pathname.startsWith(appPrefix) || pathname === appBase) {
2104
- return new Response(shell, {
2105
- status: 200,
2106
- headers: { "Content-Type": "text/html; charset=utf-8" }
2107
- });
2108
- }
2109
- if (pathname.startsWith(htmlPrefix) || pathname === htmlBase || pathname.startsWith(mdPrefix) || pathname === mdBase) {
2110
- return new Response(shell, {
2111
- status: 200,
2112
- headers: { "Content-Type": "text/html; charset=utf-8" }
2142
+ const prefix = pathPrefix ? `${pathPrefix}/` : "";
2143
+ entries.push({
2144
+ name,
2145
+ modulePath: `${prefix}${name}/${moduleFile}`,
2146
+ tagName: name
2113
2147
  });
2114
2148
  }
2115
- const lastSegment = pathname.split("/").pop() ?? "";
2116
- if (lastSegment.includes(".")) {
2117
- const fileResponse = await runtime.handle(pathname);
2118
- if (fileResponse.status === 200)
2119
- return fileResponse;
2120
- return null;
2121
- }
2122
- const base = spa === "root" || spa === "only" ? appBase : htmlBase;
2123
- const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
2124
- return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
2149
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2150
+ return entries;
2125
2151
  }
2126
- return {
2127
- handleRequest,
2128
- get htmlRouter() {
2129
- return ssrHtmlRouter;
2130
- },
2131
- get mdRouter() {
2132
- return ssrMdRouter;
2133
- },
2134
- get routeTree() {
2135
- return routeTree;
2136
- },
2137
- get widgetEntries() {
2138
- return discoveredWidgetEntries;
2139
- },
2140
- get shell() {
2141
- return shell;
2142
- }
2143
- };
2144
2152
  }
2145
2153
 
2146
2154
  // dist/runtime/fetch.runtime.js
2147
- var __rewriteRelativeImportExtension2 = function(path, preserveJsx) {
2155
+ var __rewriteRelativeImportExtension = function(path, preserveJsx) {
2148
2156
  if (typeof path === "string" && /^\.\.?\//.test(path)) {
2149
2157
  return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
2150
2158
  return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
@@ -2152,11 +2160,9 @@ var __rewriteRelativeImportExtension2 = function(path, preserveJsx) {
2152
2160
  }
2153
2161
  return path;
2154
2162
  };
2155
- var FetchRuntime = class extends Runtime {
2163
+
2164
+ class FetchRuntime extends Runtime {
2156
2165
  origin;
2157
- /**
2158
- * @param origin — Server origin, e.g. `'http://localhost:4100'` or `location.origin`.
2159
- */
2160
2166
  constructor(origin, config = {}) {
2161
2167
  super(config);
2162
2168
  this.origin = origin.endsWith("/") ? origin.slice(0, -1) : origin;
@@ -2176,7 +2182,7 @@ var FetchRuntime = class extends Runtime {
2176
2182
  const response = await fetch(url);
2177
2183
  const js = await response.text();
2178
2184
  const blob = new Blob([js], { type: "application/javascript" });
2179
- return import(__rewriteRelativeImportExtension2(URL.createObjectURL(blob)));
2185
+ return import(__rewriteRelativeImportExtension(URL.createObjectURL(blob)));
2180
2186
  }
2181
2187
  toUrl(resource) {
2182
2188
  if (typeof resource === "string")
@@ -2185,10 +2191,10 @@ var FetchRuntime = class extends Runtime {
2185
2191
  return `${this.origin}${resource.pathname}${resource.search}`;
2186
2192
  return `${this.origin}${new URL(resource.url).pathname}`;
2187
2193
  }
2188
- };
2194
+ }
2189
2195
 
2190
- // dist/src/renderer/spa/thin-client.js
2191
- var EmrouteApp = class {
2196
+ // dist/src/renderer/spa/emroute.app.js
2197
+ class EmrouteApp {
2192
2198
  server;
2193
2199
  appBase;
2194
2200
  slot = null;
@@ -2208,7 +2214,7 @@ var EmrouteApp = class {
2208
2214
  console.warn("[EmrouteApp] Navigation API not available");
2209
2215
  return;
2210
2216
  }
2211
- this.abortController = new AbortController();
2217
+ this.abortController = new AbortController;
2212
2218
  const { signal } = this.abortController;
2213
2219
  navigation.addEventListener("navigate", (event) => {
2214
2220
  if (!event.canIntercept)
@@ -2264,12 +2270,12 @@ var EmrouteApp = class {
2264
2270
  return pathname;
2265
2271
  }
2266
2272
  async handleNavigation(url, signal) {
2267
- if (!this.slot || !this.server.htmlRouter)
2273
+ if (!this.slot || !this.server.htmlRenderer)
2268
2274
  return;
2269
2275
  const routePath = this.stripAppBase(url.pathname);
2270
2276
  const routeUrl = new URL(routePath + url.search, url.origin);
2271
2277
  try {
2272
- const { content, title, redirect } = await this.server.htmlRouter.render(routeUrl, signal);
2278
+ const { content, title, redirect } = await this.server.htmlRenderer.render(routeUrl, signal);
2273
2279
  if (signal.aborted)
2274
2280
  return;
2275
2281
  if (redirect) {
@@ -2299,7 +2305,7 @@ var EmrouteApp = class {
2299
2305
  }
2300
2306
  }
2301
2307
  }
2302
- };
2308
+ }
2303
2309
  async function createEmrouteApp(server, options) {
2304
2310
  const g = globalThis;
2305
2311
  if (g.__emroute_app) {
@@ -2321,21 +2327,37 @@ async function bootEmrouteApp(options) {
2321
2327
  const routeTree = await routesResponse.json();
2322
2328
  const widgetsResponse = await runtime.handle(WIDGETS_MANIFEST_PATH);
2323
2329
  const widgetEntries = widgetsResponse.ok ? await widgetsResponse.json() : [];
2324
- const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, runtime);
2325
- const widgets = new WidgetRegistry();
2330
+ const elementsResponse = await runtime.handle(ELEMENTS_MANIFEST_PATH);
2331
+ const elementEntries = elementsResponse.ok ? await elementsResponse.json() : [];
2332
+ const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, elementEntries, runtime);
2333
+ const widgets = new WidgetRegistry;
2326
2334
  for (const entry of widgetEntries) {
2327
2335
  ComponentElement.registerLazy(entry.name, entry.files, moduleLoaders[entry.modulePath]);
2328
2336
  }
2329
- const server = await createEmrouteServer({
2337
+ for (const entry of elementEntries) {
2338
+ const loader = moduleLoaders[entry.modulePath];
2339
+ if (loader) {
2340
+ loader().then((mod) => {
2341
+ const cls = mod.default;
2342
+ if (typeof cls === "function" && !customElements.get(entry.tagName)) {
2343
+ customElements.define(entry.tagName, cls);
2344
+ }
2345
+ }).catch((e) => {
2346
+ console.error(`[emroute] Failed to load element ${entry.tagName}:`, e);
2347
+ });
2348
+ }
2349
+ }
2350
+ const mdRenderer = MarkdownElement.getConfiguredRenderer();
2351
+ const server = await Emroute.create({
2330
2352
  routeTree,
2331
2353
  widgets,
2332
2354
  moduleLoaders,
2333
- markdownRenderer: MarkdownElement.getConfiguredRenderer() ?? void 0
2355
+ ...mdRenderer ? { markdownRenderer: mdRenderer } : {}
2334
2356
  }, runtime);
2335
2357
  return createEmrouteApp(server, options);
2336
2358
  }
2337
- function buildLazyLoaders(tree, widgetEntries, runtime) {
2338
- const paths = /* @__PURE__ */ new Set();
2359
+ function buildLazyLoaders(tree, widgetEntries, elementEntries, runtime) {
2360
+ const paths = new Set;
2339
2361
  function walk(node) {
2340
2362
  const modulePath = node.files?.ts ?? node.files?.js;
2341
2363
  if (modulePath)
@@ -2356,6 +2378,8 @@ function buildLazyLoaders(tree, widgetEntries, runtime) {
2356
2378
  walk(tree);
2357
2379
  for (const entry of widgetEntries)
2358
2380
  paths.add(entry.modulePath);
2381
+ for (const entry of elementEntries)
2382
+ paths.add(entry.modulePath);
2359
2383
  const loaders = {};
2360
2384
  for (const path of paths) {
2361
2385
  const absolute = path.startsWith("/") ? path : "/" + path;
@@ -2363,24 +2387,8 @@ function buildLazyLoaders(tree, widgetEntries, runtime) {
2363
2387
  }
2364
2388
  return loaders;
2365
2389
  }
2366
-
2367
- // dist/src/component/widget.component.js
2368
- var WidgetComponent = class extends Component {
2369
- /**
2370
- * Render widget as HTML.
2371
- *
2372
- * Fallback chain:
2373
- * 1. html file content from context
2374
- * 2. md file content wrapped in `<mark-down>`
2375
- * 3. base Component default (markdown→HTML conversion)
2376
- *
2377
- * @example
2378
- * ```ts
2379
- * override renderHTML({ data, params }: this['RenderArgs']) {
2380
- * return `<span>${params.coin}: $${data?.price}</span>`;
2381
- * }
2382
- * ```
2383
- */
2390
+ // dist/core/component/widget.component.js
2391
+ class WidgetComponent extends Component {
2384
2392
  renderHTML(args) {
2385
2393
  const files = args.context.files;
2386
2394
  const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>
@@ -2396,20 +2404,6 @@ var WidgetComponent = class extends Component {
2396
2404
  }
2397
2405
  return super.renderHTML(args);
2398
2406
  }
2399
- /**
2400
- * Render widget as Markdown.
2401
- *
2402
- * Fallback chain:
2403
- * 1. md file content from context
2404
- * 2. empty string
2405
- *
2406
- * @example
2407
- * ```ts
2408
- * override renderMarkdown({ data, params }: this['RenderArgs']) {
2409
- * return `**${params.coin}**: $${data?.price}`;
2410
- * }
2411
- * ```
2412
- */
2413
2407
  renderMarkdown(args) {
2414
2408
  const files = args.context.files;
2415
2409
  if (files?.md) {
@@ -2417,12 +2411,9 @@ var WidgetComponent = class extends Component {
2417
2411
  }
2418
2412
  return "";
2419
2413
  }
2420
- };
2421
-
2414
+ }
2422
2415
  // dist/src/overlay/overlay.css.js
2423
- var overlayCSS = (
2424
- /* css */
2425
- `
2416
+ var overlayCSS = `
2426
2417
  :root {
2427
2418
  --overlay-backdrop: oklch(0% 0 0 / 0.5);
2428
2419
  --overlay-surface: oklch(100% 0 0);
@@ -2583,8 +2574,7 @@ dialog[data-overlay-modal][data-dismissing]::backdrop {
2583
2574
  opacity: 0;
2584
2575
  scale: 0.95;
2585
2576
  }
2586
- `
2587
- );
2577
+ `;
2588
2578
 
2589
2579
  // dist/src/overlay/overlay.service.js
2590
2580
  var ANIMATION_SAFETY_TIMEOUT = 300;
@@ -2626,7 +2616,7 @@ function createOverlayService() {
2626
2616
  document.body.appendChild(dialog);
2627
2617
  dialog.addEventListener("click", (e) => {
2628
2618
  if (e.target === dialog) {
2629
- closeModal(void 0);
2619
+ closeModal(undefined);
2630
2620
  }
2631
2621
  });
2632
2622
  return dialog;
@@ -2657,12 +2647,12 @@ function createOverlayService() {
2657
2647
  if (d.open) {
2658
2648
  d.close();
2659
2649
  if (modalResolve) {
2660
- modalResolve(void 0);
2650
+ modalResolve(undefined);
2661
2651
  modalResolve = null;
2662
2652
  }
2663
2653
  if (modalOnClose) {
2664
2654
  modalOnClose();
2665
- modalOnClose = void 0;
2655
+ modalOnClose = undefined;
2666
2656
  }
2667
2657
  }
2668
2658
  d.innerHTML = "";
@@ -2680,7 +2670,7 @@ function createOverlayService() {
2680
2670
  const onClose = modalOnClose;
2681
2671
  const dialogRef = dialog;
2682
2672
  modalResolve = null;
2683
- modalOnClose = void 0;
2673
+ modalOnClose = undefined;
2684
2674
  animateDismiss(dialogRef, () => {
2685
2675
  if (dialogRef && dialogRef.open) {
2686
2676
  dialogRef.close();
@@ -2726,8 +2716,7 @@ function createOverlayService() {
2726
2716
  cleanupPopoverAnchorObserver();
2727
2717
  try {
2728
2718
  el.hidePopover();
2729
- } catch {
2730
- }
2719
+ } catch {}
2731
2720
  el.removeAttribute("data-dismissing");
2732
2721
  el.innerHTML = "";
2733
2722
  options.render(el);
@@ -2766,8 +2755,7 @@ function createOverlayService() {
2766
2755
  return;
2767
2756
  try {
2768
2757
  popoverEl.hidePopover();
2769
- } catch {
2770
- }
2758
+ } catch {}
2771
2759
  popoverEl.removeAttribute("data-dismissing");
2772
2760
  }
2773
2761
  function cleanupPopoverAnchorObserver() {
@@ -2791,8 +2779,7 @@ function createOverlayService() {
2791
2779
  animateDismiss(popoverEl, () => {
2792
2780
  try {
2793
2781
  popoverEl.hidePopover();
2794
- } catch {
2795
- }
2782
+ } catch {}
2796
2783
  });
2797
2784
  }
2798
2785
  function dismissAll() {
@@ -2800,11 +2787,11 @@ function createOverlayService() {
2800
2787
  const resolve = modalResolve;
2801
2788
  const onClose = modalOnClose;
2802
2789
  modalResolve = null;
2803
- modalOnClose = void 0;
2790
+ modalOnClose = undefined;
2804
2791
  dialog.removeAttribute("data-dismissing");
2805
2792
  dialog.close();
2806
2793
  if (resolve)
2807
- resolve(void 0);
2794
+ resolve(undefined);
2808
2795
  if (onClose)
2809
2796
  onClose();
2810
2797
  }
@@ -2818,8 +2805,7 @@ function createOverlayService() {
2818
2805
  for (const el of document.querySelectorAll(":popover-open")) {
2819
2806
  el.hidePopover();
2820
2807
  }
2821
- } catch {
2822
- }
2808
+ } catch {}
2823
2809
  for (const el of document.querySelectorAll("dialog[open]")) {
2824
2810
  if (el !== dialog)
2825
2811
  el.close();
@@ -2834,9 +2820,8 @@ function createOverlayService() {
2834
2820
  dismissAll
2835
2821
  };
2836
2822
  }
2837
-
2838
2823
  // dist/src/widget/page-title.widget.js
2839
- var PageTitleWidget = class extends WidgetComponent {
2824
+ class PageTitleWidget extends WidgetComponent {
2840
2825
  name = "page-title";
2841
2826
  getData(args) {
2842
2827
  return Promise.resolve({ title: args.params.title });
@@ -2855,14 +2840,14 @@ var PageTitleWidget = class extends WidgetComponent {
2855
2840
  if (!params.title || typeof params.title !== "string") {
2856
2841
  return 'page-title widget requires a "title" string param';
2857
2842
  }
2858
- return void 0;
2843
+ return;
2859
2844
  }
2860
- };
2861
-
2845
+ }
2862
2846
  // dist/src/widget/breadcrumb.widget.js
2863
- var DEFAULT_HTML_SEPARATOR = " \u203A ";
2847
+ var DEFAULT_HTML_SEPARATOR = " ";
2864
2848
  var DEFAULT_MD_SEPARATOR = " > ";
2865
- var BreadcrumbWidget = class extends WidgetComponent {
2849
+
2850
+ class BreadcrumbWidget extends WidgetComponent {
2866
2851
  name = "breadcrumb";
2867
2852
  getData(args) {
2868
2853
  const pathname = args.context.pathname || "/";
@@ -2900,8 +2885,7 @@ var BreadcrumbWidget = class extends WidgetComponent {
2900
2885
  const sep = args.params.separator ?? DEFAULT_MD_SEPARATOR;
2901
2886
  return args.data.segments.map((seg, i, arr) => i === arr.length - 1 ? `**${seg.label}**` : `[${seg.label}](${seg.href})`).join(sep);
2902
2887
  }
2903
- };
2904
-
2888
+ }
2905
2889
  // dist/src/renderer/spa/mod.js
2906
2890
  if (globalThis.customElements) {
2907
2891
  if (!customElements.get("router-slot"))
@@ -2910,25 +2894,27 @@ if (globalThis.customElements) {
2910
2894
  customElements.define("mark-down", MarkdownElement);
2911
2895
  }
2912
2896
  export {
2913
- BreadcrumbWidget,
2914
- Component,
2915
- ComponentElement,
2916
- DEFAULT_BASE_PATH,
2917
- EmrouteApp,
2918
- FetchRuntime,
2919
- MarkdownElement,
2920
- PageComponent,
2921
- PageTitleWidget,
2922
- RouteTrie,
2923
- RouterSlot,
2924
- WidgetComponent,
2925
- WidgetRegistry,
2926
- bootEmrouteApp,
2927
- createEmrouteApp,
2928
- createEmrouteServer,
2929
- createOverlayService,
2930
- escapeHtml,
2897
+ setLogger,
2931
2898
  scopeWidgetCss,
2932
- setLogger
2899
+ escapeHtml,
2900
+ createOverlayService,
2901
+ createEmrouteApp,
2902
+ bootEmrouteApp,
2903
+ WidgetRegistry,
2904
+ WidgetComponent,
2905
+ RouterSlot,
2906
+ RouteTrie,
2907
+ PageTitleWidget,
2908
+ PageComponent,
2909
+ MarkdownElement,
2910
+ FetchRuntime,
2911
+ EmrouteApp,
2912
+ Emroute,
2913
+ DEFAULT_BASE_PATH,
2914
+ ComponentElement,
2915
+ Component,
2916
+ BreadcrumbWidget
2933
2917
  };
2918
+
2919
+ //# debugId=90218169F64D555264756E2164756E21
2934
2920
  //# sourceMappingURL=emroute.js.map