@emkodev/emroute 1.7.2 → 1.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +1 -1
  2. package/core/component/abstract.component.ts +74 -0
  3. package/{src → core}/component/page.component.ts +3 -61
  4. package/core/component/widget.component.ts +54 -0
  5. package/core/pipeline/pipeline.ts +224 -0
  6. package/{src/renderer/ssr → core/renderer}/html.renderer.ts +19 -45
  7. package/{src/renderer/ssr → core/renderer}/md.renderer.ts +21 -40
  8. package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +42 -53
  9. package/{src/route → core/router}/route.resolver.ts +1 -10
  10. package/core/router/route.trie.ts +175 -0
  11. package/core/runtime/abstract.runtime.ts +47 -0
  12. package/core/server/emroute.server.ts +346 -0
  13. package/core/type/component.type.ts +39 -0
  14. package/core/type/element.type.ts +10 -0
  15. package/core/type/logger.type.ts +20 -0
  16. package/core/type/markdown.type.ts +8 -0
  17. package/core/type/route-tree.type.ts +28 -0
  18. package/core/type/route.type.ts +75 -0
  19. package/core/type/widget.type.ts +27 -0
  20. package/core/util/html.util.ts +50 -0
  21. package/{src → core}/util/md.util.ts +0 -2
  22. package/{src/route → core/util}/route-tree.util.ts +0 -2
  23. package/{src → core}/util/widget-resolve.util.ts +15 -45
  24. package/{src → core}/widget/widget.parser.ts +0 -21
  25. package/core/widget/widget.registry.ts +24 -0
  26. package/dist/core/component/abstract.component.d.ts +48 -0
  27. package/dist/core/component/abstract.component.js +42 -0
  28. package/dist/core/component/abstract.component.js.map +1 -0
  29. package/dist/core/component/page.component.d.ts +23 -0
  30. package/dist/core/component/page.component.js +49 -0
  31. package/dist/core/component/page.component.js.map +1 -0
  32. package/dist/core/component/widget.component.d.ts +17 -0
  33. package/dist/core/component/widget.component.js +37 -0
  34. package/dist/core/component/widget.component.js.map +1 -0
  35. package/dist/core/pipeline/pipeline.d.ts +61 -0
  36. package/dist/core/pipeline/pipeline.js +189 -0
  37. package/dist/core/pipeline/pipeline.js.map +1 -0
  38. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.d.ts +8 -24
  39. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.js +12 -33
  40. package/dist/core/renderer/html.renderer.js.map +1 -0
  41. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.d.ts +6 -21
  42. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.js +14 -31
  43. package/dist/core/renderer/md.renderer.js.map +1 -0
  44. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -17
  45. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +34 -36
  46. package/dist/core/renderer/ssr.renderer.js.map +1 -0
  47. package/dist/{src/route → core/router}/route.resolver.d.ts +1 -8
  48. package/dist/{src/route → core/router}/route.resolver.js +0 -1
  49. package/dist/core/router/route.resolver.js.map +1 -0
  50. package/dist/core/router/route.trie.d.ts +32 -0
  51. package/dist/core/router/route.trie.js +152 -0
  52. package/dist/core/router/route.trie.js.map +1 -0
  53. package/dist/core/runtime/abstract.runtime.d.ts +32 -0
  54. package/dist/core/runtime/abstract.runtime.js +26 -0
  55. package/dist/core/runtime/abstract.runtime.js.map +1 -0
  56. package/dist/core/server/emroute.server.d.ts +48 -0
  57. package/dist/core/server/emroute.server.js +261 -0
  58. package/dist/core/server/emroute.server.js.map +1 -0
  59. package/dist/core/server/server.type.d.ts +45 -0
  60. package/dist/core/server/server.type.js +11 -0
  61. package/dist/core/server/server.type.js.map +1 -0
  62. package/dist/core/type/component.type.d.ts +37 -0
  63. package/dist/core/type/component.type.js +7 -0
  64. package/dist/core/type/component.type.js.map +1 -0
  65. package/dist/core/type/element.type.d.ts +9 -0
  66. package/dist/core/type/element.type.js +5 -0
  67. package/dist/core/type/element.type.js.map +1 -0
  68. package/dist/core/type/logger.type.d.ts +14 -0
  69. package/dist/core/type/logger.type.js +8 -0
  70. package/dist/core/type/logger.type.js.map +1 -0
  71. package/dist/core/type/markdown.type.d.ts +7 -0
  72. package/dist/core/type/markdown.type.js +5 -0
  73. package/dist/core/type/markdown.type.js.map +1 -0
  74. package/dist/{src → core}/type/route-tree.type.d.ts +0 -12
  75. package/dist/{src → core}/type/route-tree.type.js +0 -1
  76. package/dist/core/type/route-tree.type.js.map +1 -0
  77. package/dist/core/type/route.type.d.ts +62 -0
  78. package/dist/core/type/route.type.js +7 -0
  79. package/dist/core/type/route.type.js.map +1 -0
  80. package/dist/core/type/widget.type.d.ts +27 -0
  81. package/dist/core/type/widget.type.js +5 -0
  82. package/dist/core/type/widget.type.js.map +1 -0
  83. package/dist/core/util/html.util.d.ts +14 -0
  84. package/dist/core/util/html.util.js +43 -0
  85. package/dist/core/util/html.util.js.map +1 -0
  86. package/dist/{src → core}/util/md.util.d.ts +0 -1
  87. package/dist/{src → core}/util/md.util.js +0 -2
  88. package/dist/core/util/md.util.js.map +1 -0
  89. package/dist/{src/route → core/util}/route-tree.util.js +0 -2
  90. package/dist/core/util/route-tree.util.js.map +1 -0
  91. package/dist/core/util/widget-resolve.util.d.ts +32 -0
  92. package/dist/{src → core}/util/widget-resolve.util.js +11 -41
  93. package/dist/core/util/widget-resolve.util.js.map +1 -0
  94. package/dist/{src → core}/widget/widget.parser.d.ts +0 -13
  95. package/dist/{src → core}/widget/widget.parser.js +0 -21
  96. package/dist/core/widget/widget.parser.js.map +1 -0
  97. package/dist/core/widget/widget.registry.d.ts +13 -0
  98. package/dist/core/widget/widget.registry.js +19 -0
  99. package/dist/core/widget/widget.registry.js.map +1 -0
  100. package/dist/emroute.js +1137 -1151
  101. package/dist/emroute.js.map +36 -5
  102. package/dist/runtime/abstract.runtime.d.ts +50 -5
  103. package/dist/runtime/abstract.runtime.js +446 -6
  104. package/dist/runtime/abstract.runtime.js.map +1 -1
  105. package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +1 -0
  106. package/dist/runtime/bun/fs/bun-fs.runtime.js +18 -2
  107. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
  108. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +2 -0
  109. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +11 -1
  110. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
  111. package/dist/runtime/fetch.runtime.d.ts +3 -3
  112. package/dist/runtime/fetch.runtime.js +3 -3
  113. package/dist/runtime/sitemap.generator.d.ts +1 -1
  114. package/dist/runtime/sitemap.generator.js +1 -1
  115. package/dist/runtime/sitemap.generator.js.map +1 -1
  116. package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +1 -0
  117. package/dist/runtime/universal/fs/universal-fs.runtime.js +18 -2
  118. package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
  119. package/dist/server/build.util.d.ts +9 -10
  120. package/dist/server/build.util.js +45 -36
  121. package/dist/server/build.util.js.map +1 -1
  122. package/dist/server/codegen.util.d.ts +1 -1
  123. package/dist/server/emroute.server.d.ts +8 -35
  124. package/dist/server/emroute.server.js +7 -341
  125. package/dist/server/emroute.server.js.map +1 -1
  126. package/dist/server/esbuild-manifest.plugin.js +1 -1
  127. package/dist/server/esbuild-manifest.plugin.js.map +1 -1
  128. package/dist/server/server-api.type.d.ts +3 -68
  129. package/dist/server/server-api.type.js +1 -8
  130. package/dist/server/server-api.type.js.map +1 -1
  131. package/dist/src/element/component.element.d.ts +4 -7
  132. package/dist/src/element/component.element.js +23 -22
  133. package/dist/src/element/component.element.js.map +1 -1
  134. package/dist/src/element/markdown.element.d.ts +2 -2
  135. package/dist/src/element/markdown.element.js +4 -3
  136. package/dist/src/element/markdown.element.js.map +1 -1
  137. package/dist/src/index.d.ts +15 -13
  138. package/dist/src/index.js +8 -8
  139. package/dist/src/index.js.map +1 -1
  140. package/dist/src/renderer/spa/emroute.app.d.ts +50 -0
  141. package/dist/src/renderer/spa/emroute.app.js +246 -0
  142. package/dist/src/renderer/spa/emroute.app.js.map +1 -0
  143. package/dist/src/renderer/spa/mod.d.ts +17 -16
  144. package/dist/src/renderer/spa/mod.js +9 -9
  145. package/dist/src/renderer/spa/mod.js.map +1 -1
  146. package/dist/src/renderer/spa/thin-client.d.ts +3 -3
  147. package/dist/src/renderer/spa/thin-client.js +34 -12
  148. package/dist/src/renderer/spa/thin-client.js.map +1 -1
  149. package/dist/src/route/route.core.d.ts +3 -3
  150. package/dist/src/route/route.core.js +21 -7
  151. package/dist/src/route/route.core.js.map +1 -1
  152. package/dist/src/util/html.util.d.ts +5 -22
  153. package/dist/src/util/html.util.js +8 -56
  154. package/dist/src/util/html.util.js.map +1 -1
  155. package/dist/src/widget/breadcrumb.widget.d.ts +2 -2
  156. package/dist/src/widget/breadcrumb.widget.js +2 -2
  157. package/dist/src/widget/breadcrumb.widget.js.map +1 -1
  158. package/dist/src/widget/page-title.widget.d.ts +1 -1
  159. package/dist/src/widget/page-title.widget.js +1 -1
  160. package/dist/src/widget/page-title.widget.js.map +1 -1
  161. package/package.json +8 -8
  162. package/runtime/abstract.runtime.ts +483 -8
  163. package/runtime/bun/fs/bun-fs.runtime.ts +17 -1
  164. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +11 -0
  165. package/runtime/fetch.runtime.ts +3 -3
  166. package/runtime/sitemap.generator.ts +2 -2
  167. package/runtime/universal/fs/universal-fs.runtime.ts +17 -1
  168. package/server/build.util.ts +53 -47
  169. package/server/codegen.util.ts +1 -1
  170. package/server/emroute.server.ts +12 -412
  171. package/src/element/component.element.ts +24 -31
  172. package/src/element/markdown.element.ts +5 -4
  173. package/src/index.ts +22 -18
  174. package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +46 -22
  175. package/src/renderer/spa/mod.ts +22 -22
  176. package/src/util/html.util.ts +16 -61
  177. package/src/widget/breadcrumb.widget.ts +3 -3
  178. package/src/widget/page-title.widget.ts +1 -1
  179. package/dist/src/component/abstract.component.d.ts +0 -199
  180. package/dist/src/component/abstract.component.js +0 -84
  181. package/dist/src/component/abstract.component.js.map +0 -1
  182. package/dist/src/component/page.component.d.ts +0 -74
  183. package/dist/src/component/page.component.js +0 -107
  184. package/dist/src/component/page.component.js.map +0 -1
  185. package/dist/src/component/widget.component.d.ts +0 -47
  186. package/dist/src/component/widget.component.js +0 -69
  187. package/dist/src/component/widget.component.js.map +0 -1
  188. package/dist/src/renderer/ssr/html.renderer.js.map +0 -1
  189. package/dist/src/renderer/ssr/md.renderer.js.map +0 -1
  190. package/dist/src/renderer/ssr/ssr.renderer.js.map +0 -1
  191. package/dist/src/route/route-tree.util.js.map +0 -1
  192. package/dist/src/route/route.matcher.d.ts +0 -86
  193. package/dist/src/route/route.matcher.js +0 -214
  194. package/dist/src/route/route.matcher.js.map +0 -1
  195. package/dist/src/route/route.resolver.js.map +0 -1
  196. package/dist/src/route/route.trie.d.ts +0 -38
  197. package/dist/src/route/route.trie.js +0 -206
  198. package/dist/src/route/route.trie.js.map +0 -1
  199. package/dist/src/type/logger.type.d.ts +0 -17
  200. package/dist/src/type/logger.type.js +0 -9
  201. package/dist/src/type/logger.type.js.map +0 -1
  202. package/dist/src/type/markdown.type.d.ts +0 -20
  203. package/dist/src/type/markdown.type.js +0 -2
  204. package/dist/src/type/markdown.type.js.map +0 -1
  205. package/dist/src/type/route-tree.type.js.map +0 -1
  206. package/dist/src/type/route.type.d.ts +0 -94
  207. package/dist/src/type/route.type.js +0 -8
  208. package/dist/src/type/route.type.js.map +0 -1
  209. package/dist/src/type/widget.type.d.ts +0 -55
  210. package/dist/src/type/widget.type.js +0 -10
  211. package/dist/src/type/widget.type.js.map +0 -1
  212. package/dist/src/util/logger.util.d.ts +0 -26
  213. package/dist/src/util/logger.util.js +0 -80
  214. package/dist/src/util/logger.util.js.map +0 -1
  215. package/dist/src/util/md.util.js.map +0 -1
  216. package/dist/src/util/widget-resolve.util.d.ts +0 -52
  217. package/dist/src/util/widget-resolve.util.js.map +0 -1
  218. package/dist/src/widget/widget.parser.js.map +0 -1
  219. package/dist/src/widget/widget.registry.d.ts +0 -23
  220. package/dist/src/widget/widget.registry.js +0 -42
  221. package/dist/src/widget/widget.registry.js.map +0 -1
  222. package/runtime/bun/esbuild-runtime-loader.plugin.ts +0 -112
  223. package/server/esbuild-manifest.plugin.ts +0 -209
  224. package/server/server-api.type.ts +0 -97
  225. package/src/component/abstract.component.ts +0 -231
  226. package/src/component/widget.component.ts +0 -85
  227. package/src/route/route.core.ts +0 -362
  228. package/src/route/route.trie.ts +0 -265
  229. package/src/type/logger.type.ts +0 -24
  230. package/src/type/markdown.type.ts +0 -21
  231. package/src/type/route-tree.type.ts +0 -51
  232. package/src/type/route.type.ts +0 -124
  233. package/src/type/widget.type.ts +0 -65
  234. package/src/util/logger.util.ts +0 -83
  235. package/src/widget/widget.registry.ts +0 -51
  236. /package/dist/{src/route → core/util}/route-tree.util.d.ts +0 -0
@@ -5,34 +5,37 @@
5
5
  * Handles:
6
6
  * - SSR hydration (ssr attribute)
7
7
  * - Client-side data fetching with AbortSignal
8
- * - Companion file loading (html, md, css) with caching
8
+ * - Companion file loading (html, md, css)
9
9
  * - Loading/error states
10
10
  */
11
11
 
12
- import type {
13
- Component,
14
- ComponentContext,
15
- ContextProvider,
16
- } from '../component/abstract.component.ts';
17
- import { HTMLElementBase, LAZY_ATTR, SSR_ATTR } from '../util/html.util.ts';
12
+ import type { Component } from '../../core/component/abstract.component.ts';
13
+ import type { ComponentContext, ContextProvider } from '../../core/type/component.type.ts';
14
+ import { HTMLElementBase } from '../util/html.util.ts';
15
+ import { LAZY_ATTR, SSR_ATTR } from '../../core/util/html.util.ts';
18
16
 
19
17
  type ComponentState = 'idle' | 'loading' | 'ready' | 'error';
20
18
 
19
+ /** Strip keys with undefined values — returns the filtered object, or undefined if all values are undefined. */
20
+ function filterUndefined<T extends Record<string, unknown>>(obj: T): { [K in keyof T as T[K] extends undefined ? never : K]: NonNullable<T[K]> } | undefined {
21
+ const result: Record<string, unknown> = {};
22
+ let hasValue = false;
23
+ for (const [k, v] of Object.entries(obj)) {
24
+ if (v !== undefined) { result[k] = v; hasValue = true; }
25
+ }
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ return hasValue ? result as any : undefined;
28
+ }
29
+
21
30
  type WidgetFiles = { html?: string; md?: string; css?: string };
22
31
 
23
32
  /**
24
33
  * Custom element that renders a Component in the browser.
25
34
  */
26
35
  export class ComponentElement<TParams, TData> extends HTMLElementBase {
27
- /** Shared file content cache — deduplicates fetches across all widget instances. */
28
- private static fileCache = new Map<string, Promise<string | undefined>>();
29
-
30
36
  /** Lazy module loaders keyed by tag name — set by registerLazy(). */
31
37
  private static lazyLoaders = new Map<string, () => Promise<unknown>>();
32
38
 
33
- /** Cached module promises for lazy-loaded widgets — avoids re-fetching. */
34
- private static lazyModules = new Map<string, Promise<unknown>>();
35
-
36
39
  /** App-level context provider set once during router initialization. */
37
40
  private static extendContext: ContextProvider | undefined;
38
41
 
@@ -42,7 +45,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
42
45
  }
43
46
 
44
47
  private component: Component<TParams, TData>;
45
- private effectiveFiles?: WidgetFiles;
48
+ private effectiveFiles?: WidgetFiles | undefined;
46
49
  private params: TParams | null = null;
47
50
  private data: TData | null = null;
48
51
  private context!: ComponentContext;
@@ -171,12 +174,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
171
174
  const lazyLoader = ComponentElement.lazyLoaders.get(tagName);
172
175
  if (lazyLoader) {
173
176
  try {
174
- let modulePromise = ComponentElement.lazyModules.get(tagName);
175
- if (!modulePromise) {
176
- modulePromise = lazyLoader();
177
- ComponentElement.lazyModules.set(tagName, modulePromise);
178
- }
179
- const mod = await modulePromise as Record<string, unknown>;
177
+ const mod = await lazyLoader() as Record<string, unknown>;
180
178
  for (const exp of Object.values(mod)) {
181
179
  if (exp && typeof exp === 'object' && 'getData' in exp) {
182
180
  const WidgetClass = exp.constructor as new () => Component<TParams, TData>;
@@ -231,12 +229,13 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
231
229
  if (signal.aborted) return;
232
230
 
233
231
  const currentUrl = globalThis.location ? new URL(location.href) : new URL('http://localhost/');
232
+ const filteredFiles = filterUndefined(files);
234
233
  const base: ComponentContext = {
235
234
  url: currentUrl,
236
235
  pathname: currentUrl.pathname,
237
236
  searchParams: currentUrl.searchParams,
238
237
  params: this.params ?? {},
239
- files: (files.html || files.md || files.css) ? files : undefined,
238
+ ...(filteredFiles ? { files: filteredFiles } : {}),
240
239
  };
241
240
  this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
242
241
 
@@ -316,24 +315,18 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
316
315
  }
317
316
 
318
317
  /**
319
- * Fetch a single file by path, with caching.
318
+ * Fetch a single file by path.
320
319
  * Absolute URLs (http/https) pass through; relative paths get '/' prefix.
321
320
  */
322
321
  private static loadFile(path: string): Promise<string | undefined> {
323
- const cached = ComponentElement.fileCache.get(path);
324
- if (cached) return cached;
325
-
326
322
  const url = path.startsWith('http://') || path.startsWith('https://')
327
323
  ? path
328
324
  : (path.startsWith('/') ? path : '/' + path);
329
325
 
330
- const promise = fetch(url).then(
326
+ return fetch(url).then(
331
327
  (res) => res.ok ? res.text() : undefined,
332
328
  () => undefined,
333
329
  );
334
-
335
- ComponentElement.fileCache.set(path, promise);
336
- return promise;
337
330
  }
338
331
 
339
332
  /**
@@ -350,7 +343,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
350
343
  filePaths.css ? ComponentElement.loadFile(filePaths.css) : undefined,
351
344
  ]);
352
345
 
353
- return { html, md, css };
346
+ return filterUndefined({ html, md, css }) ?? {};
354
347
  }
355
348
 
356
349
  private async loadData(): Promise<void> {
@@ -364,7 +357,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
364
357
  try {
365
358
  const promise = this.component.getData({
366
359
  params: this.params,
367
- signal,
360
+ ...(signal ? { signal } : {}),
368
361
  context: this.context,
369
362
  });
370
363
  this.dataPromise = promise;
@@ -7,8 +7,9 @@
7
7
  * - Source attribute: <mark-down src="/path/to.md"></mark-down>
8
8
  */
9
9
 
10
- import { escapeHtml, HTMLElementBase } from '../util/html.util.ts';
11
- import type { MarkdownRenderer } from '../type/markdown.type.ts';
10
+ import { HTMLElementBase } from '../util/html.util.ts';
11
+ import { escapeHtml } from '../../core/util/html.util.ts';
12
+ import type { MarkdownRenderer } from '../../core/type/markdown.type.ts';
12
13
 
13
14
  export class MarkdownElement extends HTMLElementBase {
14
15
  private static renderer: MarkdownRenderer | null = null;
@@ -30,7 +31,7 @@ export class MarkdownElement extends HTMLElementBase {
30
31
  MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
31
32
  }
32
33
 
33
- /** Get the current renderer (if set). Used by bootEmrouteApp to pass through to createEmrouteServer. */
34
+ /** Get the current renderer (if set). Used by bootEmrouteApp to pass through to Emroute.create(). */
34
35
  static getConfiguredRenderer(): MarkdownRenderer | null {
35
36
  return MarkdownElement.renderer;
36
37
  }
@@ -80,7 +81,7 @@ export class MarkdownElement extends HTMLElementBase {
80
81
  const signal = this.abortController?.signal;
81
82
 
82
83
  try {
83
- const response = await fetch(src, { signal });
84
+ const response = await fetch(src, signal ? { signal } : {});
84
85
 
85
86
  if (!response.ok) {
86
87
  throw new Error(`Failed to fetch ${src}: ${response.status}`);
package/src/index.ts CHANGED
@@ -27,38 +27,42 @@ export type {
27
27
  RouterEventListener,
28
28
  RouterEventType,
29
29
  RouterState,
30
- } from './type/route.type.ts';
30
+ } from '../core/type/route.type.ts';
31
31
 
32
- export type { RouteNode } from './type/route-tree.type.ts';
33
- export type { RouteResolver, ResolvedRoute } from './route/route.resolver.ts';
34
- export { RouteTrie } from './route/route.trie.ts';
32
+ export type { RouteNode } from '../core/type/route-tree.type.ts';
33
+ export type { RouteResolver, ResolvedRoute } from '../core/router/route.resolver.ts';
34
+ export { RouteTrie } from '../core/router/route.trie.ts';
35
35
 
36
36
  export type {
37
37
  ParsedWidgetBlock,
38
38
  SpaMode,
39
39
  WidgetManifestEntry,
40
40
  WidgetsManifest,
41
- } from './type/widget.type.ts';
41
+ } from '../core/type/widget.type.ts';
42
42
 
43
- export type { MarkdownRenderer } from './type/markdown.type.ts';
44
- export { type Logger, setLogger } from './type/logger.type.ts';
43
+ export type { ElementManifestEntry } from '../core/type/element.type.ts';
44
+ export type { MarkdownRenderer } from '../core/type/markdown.type.ts';
45
+ export { setLogger, type Logger } from '../core/type/logger.type.ts';
45
46
 
46
47
  // Components
47
48
  export {
48
49
  Component,
49
- type ComponentContext,
50
- type ComponentManifestEntry,
51
- type ContextProvider,
52
- type FileContents,
53
- type RenderContext,
54
- } from './component/abstract.component.ts';
50
+ } from '../core/component/abstract.component.ts';
55
51
 
56
- export { PageComponent } from './component/page.component.ts';
57
- export { WidgetComponent } from './component/widget.component.ts';
58
- export { WidgetRegistry } from './widget/widget.registry.ts';
52
+ export type {
53
+ ComponentContext,
54
+ ComponentManifestEntry,
55
+ ContextProvider,
56
+ FileContents,
57
+ RenderContext,
58
+ } from '../core/type/component.type.ts';
59
+
60
+ export { PageComponent } from '../core/component/page.component.ts';
61
+ export { WidgetComponent } from '../core/component/widget.component.ts';
62
+ export { WidgetRegistry } from '../core/widget/widget.registry.ts';
59
63
 
60
64
  // Route config
61
- export { type BasePath, DEFAULT_BASE_PATH } from './route/route.core.ts';
65
+ export { type BasePath, DEFAULT_BASE_PATH } from '../core/server/emroute.server.ts';
62
66
 
63
67
  // Utils
64
- export { escapeHtml, scopeWidgetCss } from './util/html.util.ts';
68
+ export { escapeHtml, scopeWidgetCss } from '../core/util/html.util.ts';
@@ -3,37 +3,37 @@
3
3
  /**
4
4
  * Emroute App
5
5
  *
6
- * Browser entry point for `/app/` routes. Wraps an EmrouteServer instance
6
+ * Browser entry point for `/app/` routes. Wraps an Emroute instance
7
7
  * (same server, same pipeline) with Navigation API glue that intercepts
8
8
  * link clicks, calls `htmlRouter.render()`, and injects the result.
9
9
  */
10
10
 
11
- import type { EmrouteServer } from '../../../server/server-api.type.ts';
12
- import { createEmrouteServer } from '../../../server/emroute.server.ts';
11
+ import { Emroute } from '../../../core/server/emroute.server.ts';
13
12
  import { FetchRuntime } from '../../../runtime/fetch.runtime.ts';
14
- import { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH } from '../../../runtime/abstract.runtime.ts';
15
- import type { RouteNode } from '../../type/route-tree.type.ts';
16
- import type { NavigateOptions } from '../../type/route.type.ts';
17
- import type { WidgetManifestEntry } from '../../type/widget.type.ts';
18
- import { assertSafeRedirect, type BasePath, DEFAULT_BASE_PATH } from '../../route/route.core.ts';
19
- import { escapeHtml } from '../../util/html.util.ts';
13
+ import { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH, ELEMENTS_MANIFEST_PATH } from '../../../core/runtime/abstract.runtime.ts';
14
+ import type { RouteNode } from '../../../core/type/route-tree.type.ts';
15
+ import type { NavigateOptions } from '../../../core/type/route.type.ts';
16
+ import type { WidgetManifestEntry } from '../../../core/type/widget.type.ts';
17
+ import type { ElementManifestEntry } from '../../../core/type/element.type.ts';
18
+ import { type BasePath, DEFAULT_BASE_PATH } from '../../../core/server/emroute.server.ts';
19
+ import { assertSafeRedirect, escapeHtml } from '../../../core/util/html.util.ts';
20
20
  import { ComponentElement } from '../../element/component.element.ts';
21
21
  import { MarkdownElement } from '../../element/markdown.element.ts';
22
- import { WidgetRegistry } from '../../widget/widget.registry.ts';
22
+ import { WidgetRegistry } from '../../../core/widget/widget.registry.ts';
23
23
 
24
24
  /** Options for `createEmrouteApp`. */
25
25
  export interface EmrouteAppOptions {
26
26
  basePath?: BasePath;
27
27
  }
28
28
 
29
- /** Browser app — Navigation API wired to an EmrouteServer. */
29
+ /** Browser app — Navigation API wired to an Emroute. */
30
30
  export class EmrouteApp {
31
- private readonly server: EmrouteServer;
31
+ private readonly server: Emroute;
32
32
  private readonly appBase: string;
33
33
  private slot: Element | null = null;
34
34
  private abortController: AbortController | null = null;
35
35
 
36
- constructor(server: EmrouteServer, options?: EmrouteAppOptions) {
36
+ constructor(server: Emroute, options?: EmrouteAppOptions) {
37
37
  const bp = options?.basePath ?? DEFAULT_BASE_PATH;
38
38
  this.server = server;
39
39
  this.appBase = bp.app;
@@ -113,13 +113,13 @@ export class EmrouteApp {
113
113
  }
114
114
 
115
115
  private async handleNavigation(url: URL, signal: AbortSignal): Promise<void> {
116
- if (!this.slot || !this.server.htmlRouter) return;
116
+ if (!this.slot || !this.server.htmlRenderer) return;
117
117
 
118
118
  const routePath = this.stripAppBase(url.pathname);
119
119
  const routeUrl = new URL(routePath + url.search, url.origin);
120
120
 
121
121
  try {
122
- const { content, title, redirect } = await this.server.htmlRouter.render(routeUrl, signal);
122
+ const { content, title, redirect } = await this.server.htmlRenderer.render(routeUrl, signal);
123
123
 
124
124
  if (signal.aborted) return;
125
125
 
@@ -158,7 +158,7 @@ export class EmrouteApp {
158
158
  * Stored on `globalThis.__emroute_app` for programmatic access.
159
159
  */
160
160
  export async function createEmrouteApp(
161
- server: EmrouteServer,
161
+ server: Emroute,
162
162
  options?: EmrouteAppOptions,
163
163
  ): Promise<EmrouteApp> {
164
164
  const g = globalThis as Record<string, unknown>;
@@ -207,8 +207,14 @@ export async function bootEmrouteApp(options?: BootOptions): Promise<EmrouteApp>
207
207
  ? await widgetsResponse.json()
208
208
  : [];
209
209
 
210
- // Build lazy module loaders for all route + widget modules
211
- const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, runtime);
210
+ // Fetch element manifest (optional app may have no custom elements)
211
+ const elementsResponse = await runtime.handle(ELEMENTS_MANIFEST_PATH);
212
+ const elementEntries: ElementManifestEntry[] = elementsResponse.ok
213
+ ? await elementsResponse.json()
214
+ : [];
215
+
216
+ // Build lazy module loaders for all route + widget + element modules
217
+ const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, elementEntries, runtime);
212
218
 
213
219
  // Register widgets eagerly (tag defined immediately, module loads on connectedCallback)
214
220
  const widgets = new WidgetRegistry();
@@ -216,24 +222,41 @@ export async function bootEmrouteApp(options?: BootOptions): Promise<EmrouteApp>
216
222
  ComponentElement.registerLazy(entry.name, entry.files, moduleLoaders[entry.modulePath]);
217
223
  }
218
224
 
219
- // Create the server (reuses the same createEmrouteServer as SSR)
220
- const server = await createEmrouteServer({
225
+ // Register custom elements import all modules, define when loaded
226
+ for (const entry of elementEntries) {
227
+ const loader = moduleLoaders[entry.modulePath];
228
+ if (loader) {
229
+ loader().then((mod) => {
230
+ const cls = (mod as Record<string, unknown>).default;
231
+ if (typeof cls === 'function' && !customElements.get(entry.tagName)) {
232
+ customElements.define(entry.tagName, cls as CustomElementConstructor);
233
+ }
234
+ }).catch((e) => {
235
+ console.error(`[emroute] Failed to load element ${entry.tagName}:`, e);
236
+ });
237
+ }
238
+ }
239
+
240
+ // Create Emroute instance (same class as SSR)
241
+ const mdRenderer = MarkdownElement.getConfiguredRenderer();
242
+ const server = await Emroute.create({
221
243
  routeTree,
222
244
  widgets,
223
245
  moduleLoaders,
224
- markdownRenderer: MarkdownElement.getConfiguredRenderer() ?? undefined,
246
+ ...(mdRenderer ? { markdownRenderer: mdRenderer } : {}),
225
247
  }, runtime);
226
248
 
227
249
  return createEmrouteApp(server, options);
228
250
  }
229
251
 
230
252
  /**
231
- * Walk the route tree and widget entries to build a map of
253
+ * Walk the route tree, widget entries, and element entries to build a map of
232
254
  * `path → () => runtime.loadModule(path)` lazy loaders.
233
255
  */
234
256
  function buildLazyLoaders(
235
257
  tree: RouteNode,
236
258
  widgetEntries: WidgetManifestEntry[],
259
+ elementEntries: ElementManifestEntry[],
237
260
  runtime: FetchRuntime,
238
261
  ): Record<string, () => Promise<unknown>> {
239
262
  const paths = new Set<string>();
@@ -252,6 +275,7 @@ function buildLazyLoaders(
252
275
 
253
276
  walk(tree);
254
277
  for (const entry of widgetEntries) paths.add(entry.modulePath);
278
+ for (const entry of elementEntries) paths.add(entry.modulePath);
255
279
 
256
280
  const loaders: Record<string, () => Promise<unknown>> = {};
257
281
  for (const path of paths) {
@@ -10,22 +10,22 @@
10
10
  import { RouterSlot } from '../../element/slot.element.ts';
11
11
  import { MarkdownElement } from '../../element/markdown.element.ts';
12
12
  import { ComponentElement } from '../../element/component.element.ts';
13
- import { WidgetRegistry } from '../../widget/widget.registry.ts';
13
+ import { WidgetRegistry } from '../../../core/widget/widget.registry.ts';
14
14
 
15
- export { bootEmrouteApp, createEmrouteApp, EmrouteApp, type BootOptions, type EmrouteAppOptions } from './thin-client.ts';
15
+ export { bootEmrouteApp, createEmrouteApp, EmrouteApp, type BootOptions, type EmrouteAppOptions } from './emroute.app.ts';
16
16
  export { ComponentElement, MarkdownElement, RouterSlot, WidgetRegistry };
17
- export type { SpaMode, WidgetsManifest } from '../../type/widget.type.ts';
17
+ export type { SpaMode, WidgetsManifest } from '../../../core/type/widget.type.ts';
18
18
 
19
19
  // Re-export base classes and types for consumer code (pages, widgets)
20
- export { PageComponent } from '../../component/page.component.ts';
21
- export { WidgetComponent } from '../../component/widget.component.ts';
22
- export {
23
- Component,
24
- type ComponentContext,
25
- type ComponentManifestEntry,
26
- type ContextProvider,
27
- type RenderContext,
28
- } from '../../component/abstract.component.ts';
20
+ export { PageComponent } from '../../../core/component/page.component.ts';
21
+ export { WidgetComponent } from '../../../core/component/widget.component.ts';
22
+ export { Component } from '../../../core/component/abstract.component.ts';
23
+ export type {
24
+ ComponentContext,
25
+ ComponentManifestEntry,
26
+ ContextProvider,
27
+ RenderContext,
28
+ } from '../../../core/type/component.type.ts';
29
29
  export type {
30
30
  MatchedRoute,
31
31
  NavigateOptions,
@@ -33,13 +33,13 @@ export type {
33
33
  RouterEvent,
34
34
  RouterEventListener,
35
35
  RouterEventType,
36
- } from '../../type/route.type.ts';
37
- export type { RouteNode } from '../../type/route-tree.type.ts';
38
- export type { RouteResolver, ResolvedRoute } from '../../route/route.resolver.ts';
39
- export { RouteTrie } from '../../route/route.trie.ts';
40
- export type { MarkdownRenderer } from '../../type/markdown.type.ts';
41
- export { type BasePath, DEFAULT_BASE_PATH } from '../../route/route.core.ts';
42
- export { escapeHtml, scopeWidgetCss } from '../../util/html.util.ts';
36
+ } from '../../../core/type/route.type.ts';
37
+ export type { RouteNode } from '../../../core/type/route-tree.type.ts';
38
+ export type { RouteResolver, ResolvedRoute } from '../../../core/router/route.resolver.ts';
39
+ export { RouteTrie } from '../../../core/router/route.trie.ts';
40
+ export type { MarkdownRenderer } from '../../../core/type/markdown.type.ts';
41
+ export { type BasePath, DEFAULT_BASE_PATH } from '../../../core/server/emroute.server.ts';
42
+ export { escapeHtml, scopeWidgetCss } from '../../../core/util/html.util.ts';
43
43
  export type {
44
44
  ErrorBoundary,
45
45
  RedirectConfig,
@@ -48,9 +48,9 @@ export type {
48
48
  RouteFileType,
49
49
  RouteInfo,
50
50
  RouterState,
51
- } from '../../type/route.type.ts';
52
- export type { ParsedWidgetBlock, WidgetManifestEntry } from '../../type/widget.type.ts';
53
- export { type Logger, setLogger } from '../../type/logger.type.ts';
51
+ } from '../../../core/type/route.type.ts';
52
+ export type { ParsedWidgetBlock, WidgetManifestEntry } from '../../../core/type/widget.type.ts';
53
+ export { setLogger, type Logger } from '../../../core/type/logger.type.ts';
54
54
 
55
55
  // Register core custom elements in the browser
56
56
  if (globalThis.customElements) {
@@ -1,16 +1,23 @@
1
1
  /**
2
- * Core HTML utilities for emroute
2
+ * HTML Utilities (Browser Layer)
3
+ *
4
+ * Re-exports pure functions from core/ and provides browser-specific
5
+ * SSR-compatible HTMLElement mock.
3
6
  */
4
7
 
5
- /** HTML attribute name marking a widget as server-rendered (skip client getData + render). */
6
- export const SSR_ATTR = 'ssr';
7
-
8
- /** HTML attribute name for lazy-loading widgets via IntersectionObserver. */
9
- export const LAZY_ATTR = 'lazy';
8
+ // Re-export everything from core
9
+ export {
10
+ SSR_ATTR,
11
+ LAZY_ATTR,
12
+ assertSafeRedirect,
13
+ escapeHtml,
14
+ unescapeHtml,
15
+ scopeWidgetCss,
16
+ STATUS_MESSAGES,
17
+ } from '../../core/util/html.util.ts';
10
18
 
11
19
  /**
12
20
  * SSR-compatible ShadowRoot mock.
13
- * Provides a 1-to-1 subset of the browser ShadowRoot API for server-side rendering.
14
21
  */
15
22
  class SsrShadowRoot {
16
23
  private _innerHTML = '';
@@ -29,9 +36,7 @@ class SsrShadowRoot {
29
36
  this._innerHTML = html;
30
37
  }
31
38
 
32
- append(..._nodes: (Node | string)[]): void {
33
- // On the server, append is a no-op — SSR content is already serialized via innerHTML.
34
- }
39
+ append(..._nodes: (Node | string)[]): void {}
35
40
 
36
41
  querySelector(_selector: string): Element | null {
37
42
  return null;
@@ -52,15 +57,11 @@ class SsrShadowRoot {
52
57
 
53
58
  /**
54
59
  * SSR-compatible HTMLElement mock.
55
- * Provides a 1-to-1 subset of the browser HTMLElement API for server-side rendering.
56
- * Methods that require DOM parsing (querySelector, childNodes) return empty results —
57
- * SSR code should use innerHTML for content, not DOM traversal.
58
60
  */
59
61
  class SsrHTMLElement {
60
62
  private _innerHTML = '';
61
63
  private _shadowRoot: SsrShadowRoot | null = null;
62
64
  private _attributes = new Map<string, string>();
63
- // Accept any CSS property assignment without error
64
65
  readonly style = new Proxy({} as CSSStyleDeclaration, {
65
66
  set(_target, _prop, _value) {
66
67
  return true;
@@ -128,9 +129,7 @@ class SsrHTMLElement {
128
129
  return [];
129
130
  }
130
131
 
131
- append(..._nodes: (Node | string)[]): void {
132
- // No-op on server — use innerHTML for content
133
- }
132
+ append(..._nodes: (Node | string)[]): void {}
134
133
 
135
134
  appendChild(node: Node): Node {
136
135
  return node;
@@ -140,47 +139,3 @@ class SsrHTMLElement {
140
139
  /** Server-safe base class: HTMLElement in browser, SSR mock on server. */
141
140
  export const HTMLElementBase = globalThis.HTMLElement ??
142
141
  (SsrHTMLElement as unknown as typeof HTMLElement);
143
-
144
- /**
145
- * Escape HTML entities for safe display.
146
- */
147
- export function escapeHtml(text: string): string {
148
- return text
149
- .replaceAll('&', '&amp;')
150
- .replaceAll('<', '&lt;')
151
- .replaceAll('>', '&gt;')
152
- .replaceAll('"', '&quot;')
153
- .replaceAll("'", '&#39;')
154
- .replaceAll('`', '&#96;');
155
- }
156
-
157
- /**
158
- * Unescape HTML entities back to plain text (server-side, no DOM).
159
- */
160
- export function unescapeHtml(text: string): string {
161
- return text
162
- .replaceAll('&#96;', '`')
163
- .replaceAll('&#39;', "'")
164
- .replaceAll('&quot;', '"')
165
- .replaceAll('&gt;', '>')
166
- .replaceAll('&lt;', '<')
167
- .replaceAll('&amp;', '&');
168
- }
169
-
170
- /**
171
- * Wrap CSS in a `@scope` rule scoped to the widget's custom element tag.
172
- * Used by `WidgetComponent.renderHTML()` for companion CSS files.
173
- */
174
- export function scopeWidgetCss(css: string, widgetName: string): string {
175
- return `@scope (widget-${widgetName}) {\n${css}\n}`;
176
- }
177
-
178
- /**
179
- * Status code to message mapping.
180
- */
181
- export const STATUS_MESSAGES: Record<number, string> = {
182
- 401: 'Unauthorized',
183
- 403: 'Forbidden',
184
- 404: 'Not Found',
185
- 500: 'Internal Server Error',
186
- };
@@ -14,9 +14,9 @@
14
14
  * ```
15
15
  */
16
16
 
17
- import { WidgetComponent } from '../component/widget.component.ts';
18
- import { escapeHtml } from '../util/html.util.ts';
19
- import type { ComponentContext } from '../component/abstract.component.ts';
17
+ import { WidgetComponent } from '../../core/component/widget.component.ts';
18
+ import { escapeHtml } from '../../core/util/html.util.ts';
19
+ import type { ComponentContext } from '../../core/type/component.type.ts';
20
20
 
21
21
  const DEFAULT_HTML_SEPARATOR = ' \u203A ';
22
22
  const DEFAULT_MD_SEPARATOR = ' > ';
@@ -8,7 +8,7 @@
8
8
  * <widget-page-title title="About Us"></widget-page-title>
9
9
  */
10
10
 
11
- import { WidgetComponent } from '../component/widget.component.ts';
11
+ import { WidgetComponent } from '../../core/component/widget.component.ts';
12
12
 
13
13
  interface PageTitleParams {
14
14
  title: string;