@emkodev/emroute 1.7.3 → 1.8.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +1 -1
  2. package/core/component/abstract.component.ts +74 -0
  3. package/{src → core}/component/page.component.ts +3 -61
  4. package/core/component/widget.component.ts +54 -0
  5. package/core/pipeline/pipeline.ts +224 -0
  6. package/{src/renderer/ssr → core/renderer}/html.renderer.ts +26 -47
  7. package/{src/renderer/ssr → core/renderer}/md.renderer.ts +22 -41
  8. package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +44 -58
  9. package/{src/route → core/router}/route.resolver.ts +1 -10
  10. package/core/router/route.trie.ts +175 -0
  11. package/core/runtime/abstract.runtime.ts +47 -0
  12. package/core/server/emroute.server.ts +324 -0
  13. package/core/type/component.type.ts +39 -0
  14. package/core/type/element.type.ts +10 -0
  15. package/core/type/logger.type.ts +20 -0
  16. package/core/type/markdown.type.ts +8 -0
  17. package/core/type/route-tree.type.ts +28 -0
  18. package/core/type/route.type.ts +75 -0
  19. package/core/type/widget.type.ts +27 -0
  20. package/core/util/html.util.ts +50 -0
  21. package/{src → core}/util/md.util.ts +3 -5
  22. package/{src/route → core/util}/route-tree.util.ts +0 -2
  23. package/{src → core}/util/widget-resolve.util.ts +15 -46
  24. package/{src → core}/widget/widget.parser.ts +2 -23
  25. package/core/widget/widget.registry.ts +36 -0
  26. package/dist/core/component/abstract.component.d.ts +48 -0
  27. package/dist/core/component/abstract.component.js +42 -0
  28. package/dist/core/component/abstract.component.js.map +1 -0
  29. package/dist/core/component/page.component.d.ts +23 -0
  30. package/dist/core/component/page.component.js +49 -0
  31. package/dist/core/component/page.component.js.map +1 -0
  32. package/dist/core/component/widget.component.d.ts +17 -0
  33. package/dist/core/component/widget.component.js +37 -0
  34. package/dist/core/component/widget.component.js.map +1 -0
  35. package/dist/core/pipeline/pipeline.d.ts +61 -0
  36. package/dist/core/pipeline/pipeline.js +189 -0
  37. package/dist/core/pipeline/pipeline.js.map +1 -0
  38. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.d.ts +8 -24
  39. package/dist/{src/renderer/ssr → core/renderer}/html.renderer.js +20 -35
  40. package/dist/core/renderer/html.renderer.js.map +1 -0
  41. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.d.ts +6 -21
  42. package/dist/{src/renderer/ssr → core/renderer}/md.renderer.js +16 -32
  43. package/dist/core/renderer/md.renderer.js.map +1 -0
  44. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -27
  45. package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +33 -37
  46. package/dist/core/renderer/ssr.renderer.js.map +1 -0
  47. package/dist/{src/route → core/router}/route.resolver.d.ts +1 -8
  48. package/dist/{src/route → core/router}/route.resolver.js +0 -1
  49. package/dist/core/router/route.resolver.js.map +1 -0
  50. package/dist/core/router/route.trie.d.ts +32 -0
  51. package/dist/core/router/route.trie.js +152 -0
  52. package/dist/core/router/route.trie.js.map +1 -0
  53. package/dist/core/runtime/abstract.runtime.d.ts +32 -0
  54. package/dist/core/runtime/abstract.runtime.js +26 -0
  55. package/dist/core/runtime/abstract.runtime.js.map +1 -0
  56. package/dist/core/server/emroute.server.d.ts +48 -0
  57. package/dist/core/server/emroute.server.js +239 -0
  58. package/dist/core/server/emroute.server.js.map +1 -0
  59. package/dist/core/server/server.type.d.ts +45 -0
  60. package/dist/core/server/server.type.js +11 -0
  61. package/dist/core/server/server.type.js.map +1 -0
  62. package/dist/core/type/component.type.d.ts +37 -0
  63. package/dist/core/type/component.type.js +7 -0
  64. package/dist/core/type/component.type.js.map +1 -0
  65. package/dist/core/type/element.type.d.ts +9 -0
  66. package/dist/core/type/element.type.js +5 -0
  67. package/dist/core/type/element.type.js.map +1 -0
  68. package/dist/core/type/logger.type.d.ts +14 -0
  69. package/dist/core/type/logger.type.js +8 -0
  70. package/dist/core/type/logger.type.js.map +1 -0
  71. package/dist/core/type/markdown.type.d.ts +7 -0
  72. package/dist/core/type/markdown.type.js +5 -0
  73. package/dist/core/type/markdown.type.js.map +1 -0
  74. package/dist/{src → core}/type/route-tree.type.d.ts +0 -12
  75. package/dist/{src → core}/type/route-tree.type.js +0 -1
  76. package/dist/core/type/route-tree.type.js.map +1 -0
  77. package/dist/core/type/route.type.d.ts +62 -0
  78. package/dist/core/type/route.type.js +7 -0
  79. package/dist/core/type/route.type.js.map +1 -0
  80. package/dist/core/type/widget.type.d.ts +27 -0
  81. package/dist/core/type/widget.type.js +5 -0
  82. package/dist/core/type/widget.type.js.map +1 -0
  83. package/dist/core/util/html.util.d.ts +14 -0
  84. package/dist/core/util/html.util.js +43 -0
  85. package/dist/core/util/html.util.js.map +1 -0
  86. package/dist/{src → core}/util/md.util.d.ts +0 -1
  87. package/dist/{src → core}/util/md.util.js +0 -2
  88. package/dist/core/util/md.util.js.map +1 -0
  89. package/dist/{src/route → core/util}/route-tree.util.js +0 -2
  90. package/dist/core/util/route-tree.util.js.map +1 -0
  91. package/dist/core/util/widget-resolve.util.d.ts +28 -0
  92. package/dist/{src → core}/util/widget-resolve.util.js +12 -42
  93. package/dist/core/util/widget-resolve.util.js.map +1 -0
  94. package/dist/{src → core}/widget/widget.parser.d.ts +0 -13
  95. package/dist/{src → core}/widget/widget.parser.js +1 -22
  96. package/dist/core/widget/widget.parser.js.map +1 -0
  97. package/dist/core/widget/widget.registry.d.ts +14 -0
  98. package/dist/core/widget/widget.registry.js +26 -0
  99. package/dist/core/widget/widget.registry.js.map +1 -0
  100. package/dist/emroute.js +1092 -1220
  101. package/dist/emroute.js.map +36 -5
  102. package/dist/runtime/abstract.runtime.d.ts +41 -7
  103. package/dist/runtime/abstract.runtime.js +404 -9
  104. package/dist/runtime/abstract.runtime.js.map +1 -1
  105. package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +1 -0
  106. package/dist/runtime/bun/fs/bun-fs.runtime.js +15 -1
  107. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
  108. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +2 -0
  109. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +8 -0
  110. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
  111. package/dist/runtime/fetch.runtime.d.ts +3 -3
  112. package/dist/runtime/fetch.runtime.js +3 -3
  113. package/dist/runtime/sitemap.generator.d.ts +1 -1
  114. package/dist/runtime/sitemap.generator.js +1 -1
  115. package/dist/runtime/sitemap.generator.js.map +1 -1
  116. package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +1 -0
  117. package/dist/runtime/universal/fs/universal-fs.runtime.js +15 -1
  118. package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
  119. package/dist/server/build.util.d.ts +9 -10
  120. package/dist/server/build.util.js +11 -31
  121. package/dist/server/build.util.js.map +1 -1
  122. package/dist/server/codegen.util.d.ts +1 -1
  123. package/dist/server/emroute.server.d.ts +8 -35
  124. package/dist/server/emroute.server.js +7 -351
  125. package/dist/server/emroute.server.js.map +1 -1
  126. package/dist/server/esbuild-manifest.plugin.js +1 -1
  127. package/dist/server/esbuild-manifest.plugin.js.map +1 -1
  128. package/dist/server/server-api.type.d.ts +3 -71
  129. package/dist/server/server-api.type.js +1 -8
  130. package/dist/server/server-api.type.js.map +1 -1
  131. package/dist/src/element/component.element.d.ts +6 -14
  132. package/dist/src/element/component.element.js +13 -40
  133. package/dist/src/element/component.element.js.map +1 -1
  134. package/dist/src/element/markdown.element.d.ts +2 -2
  135. package/dist/src/element/markdown.element.js +3 -2
  136. package/dist/src/element/markdown.element.js.map +1 -1
  137. package/dist/src/index.d.ts +15 -14
  138. package/dist/src/index.js +8 -8
  139. package/dist/src/index.js.map +1 -1
  140. package/dist/src/renderer/spa/emroute.app.d.ts +50 -0
  141. package/dist/src/renderer/spa/emroute.app.js +246 -0
  142. package/dist/src/renderer/spa/emroute.app.js.map +1 -0
  143. package/dist/src/renderer/spa/mod.d.ts +17 -16
  144. package/dist/src/renderer/spa/mod.js +9 -9
  145. package/dist/src/renderer/spa/mod.js.map +1 -1
  146. package/dist/src/renderer/spa/thin-client.d.ts +3 -3
  147. package/dist/src/renderer/spa/thin-client.js +7 -7
  148. package/dist/src/renderer/spa/thin-client.js.map +1 -1
  149. package/dist/src/route/route.core.d.ts +3 -3
  150. package/dist/src/util/html.util.d.ts +5 -22
  151. package/dist/src/util/html.util.js +8 -56
  152. package/dist/src/util/html.util.js.map +1 -1
  153. package/dist/src/widget/breadcrumb.widget.d.ts +2 -2
  154. package/dist/src/widget/breadcrumb.widget.js +2 -2
  155. package/dist/src/widget/breadcrumb.widget.js.map +1 -1
  156. package/dist/src/widget/page-title.widget.d.ts +1 -1
  157. package/dist/src/widget/page-title.widget.js +1 -1
  158. package/dist/src/widget/page-title.widget.js.map +1 -1
  159. package/package.json +8 -8
  160. package/runtime/abstract.runtime.ts +433 -17
  161. package/runtime/bun/fs/bun-fs.runtime.ts +15 -1
  162. package/runtime/bun/sqlite/bun-sqlite.runtime.ts +9 -0
  163. package/runtime/fetch.runtime.ts +3 -3
  164. package/runtime/sitemap.generator.ts +2 -2
  165. package/runtime/universal/fs/universal-fs.runtime.ts +15 -1
  166. package/server/build.util.ts +17 -43
  167. package/server/codegen.util.ts +1 -1
  168. package/server/emroute.server.ts +12 -426
  169. package/src/element/component.element.ts +14 -54
  170. package/src/element/markdown.element.ts +4 -3
  171. package/src/index.ts +22 -19
  172. package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +19 -20
  173. package/src/renderer/spa/mod.ts +22 -22
  174. package/src/util/html.util.ts +16 -61
  175. package/src/widget/breadcrumb.widget.ts +3 -3
  176. package/src/widget/page-title.widget.ts +1 -1
  177. package/dist/src/component/abstract.component.d.ts +0 -199
  178. package/dist/src/component/abstract.component.js +0 -84
  179. package/dist/src/component/abstract.component.js.map +0 -1
  180. package/dist/src/component/page.component.d.ts +0 -74
  181. package/dist/src/component/page.component.js +0 -107
  182. package/dist/src/component/page.component.js.map +0 -1
  183. package/dist/src/component/widget.component.d.ts +0 -47
  184. package/dist/src/component/widget.component.js +0 -69
  185. package/dist/src/component/widget.component.js.map +0 -1
  186. package/dist/src/renderer/ssr/html.renderer.js.map +0 -1
  187. package/dist/src/renderer/ssr/md.renderer.js.map +0 -1
  188. package/dist/src/renderer/ssr/ssr.renderer.js.map +0 -1
  189. package/dist/src/route/route-tree.util.js.map +0 -1
  190. package/dist/src/route/route.matcher.d.ts +0 -86
  191. package/dist/src/route/route.matcher.js +0 -214
  192. package/dist/src/route/route.matcher.js.map +0 -1
  193. package/dist/src/route/route.resolver.js.map +0 -1
  194. package/dist/src/route/route.trie.d.ts +0 -38
  195. package/dist/src/route/route.trie.js +0 -206
  196. package/dist/src/route/route.trie.js.map +0 -1
  197. package/dist/src/type/element.type.d.ts +0 -19
  198. package/dist/src/type/element.type.js +0 -9
  199. package/dist/src/type/element.type.js.map +0 -1
  200. package/dist/src/type/logger.type.d.ts +0 -17
  201. package/dist/src/type/logger.type.js +0 -9
  202. package/dist/src/type/logger.type.js.map +0 -1
  203. package/dist/src/type/markdown.type.d.ts +0 -20
  204. package/dist/src/type/markdown.type.js +0 -2
  205. package/dist/src/type/markdown.type.js.map +0 -1
  206. package/dist/src/type/route-tree.type.js.map +0 -1
  207. package/dist/src/type/route.type.d.ts +0 -94
  208. package/dist/src/type/route.type.js +0 -8
  209. package/dist/src/type/route.type.js.map +0 -1
  210. package/dist/src/type/widget.type.d.ts +0 -55
  211. package/dist/src/type/widget.type.js +0 -10
  212. package/dist/src/type/widget.type.js.map +0 -1
  213. package/dist/src/util/logger.util.d.ts +0 -26
  214. package/dist/src/util/logger.util.js +0 -80
  215. package/dist/src/util/logger.util.js.map +0 -1
  216. package/dist/src/util/md.util.js.map +0 -1
  217. package/dist/src/util/widget-resolve.util.d.ts +0 -52
  218. package/dist/src/util/widget-resolve.util.js.map +0 -1
  219. package/dist/src/widget/widget.parser.js.map +0 -1
  220. package/dist/src/widget/widget.registry.d.ts +0 -23
  221. package/dist/src/widget/widget.registry.js +0 -42
  222. package/dist/src/widget/widget.registry.js.map +0 -1
  223. package/runtime/bun/esbuild-runtime-loader.plugin.ts +0 -112
  224. package/server/esbuild-manifest.plugin.ts +0 -209
  225. package/server/server-api.type.ts +0 -101
  226. package/src/component/abstract.component.ts +0 -231
  227. package/src/component/widget.component.ts +0 -85
  228. package/src/route/route.core.ts +0 -371
  229. package/src/route/route.trie.ts +0 -265
  230. package/src/type/element.type.ts +0 -22
  231. package/src/type/logger.type.ts +0 -24
  232. package/src/type/markdown.type.ts +0 -21
  233. package/src/type/route-tree.type.ts +0 -51
  234. package/src/type/route.type.ts +0 -124
  235. package/src/type/widget.type.ts +0 -65
  236. package/src/util/logger.util.ts +0 -83
  237. package/src/widget/widget.registry.ts +0 -51
  238. /package/dist/{src/route → core/util}/route-tree.util.d.ts +0 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Emroute
3
+ *
4
+ * Framework entry point. Reads manifests from Runtime,
5
+ * builds Pipeline + Renderers, handles Request → Response.
6
+ */
7
+
8
+ import type { WidgetManifestEntry } from '../type/widget.type.ts';
9
+ import type { WidgetComponent } from '../component/widget.component.ts';
10
+ import type { Runtime } from '../runtime/abstract.runtime.ts';
11
+ import { Pipeline } from '../pipeline/pipeline.ts';
12
+ import { SsrHtmlRenderer } from '../renderer/html.renderer.ts';
13
+ import { SsrMdRenderer } from '../renderer/md.renderer.ts';
14
+ import { WidgetRegistry } from '../widget/widget.registry.ts';
15
+ import { escapeHtml } from '../util/html.util.ts';
16
+ import { rewriteMdLinks } from '../util/md.util.ts';
17
+ import type { RouteNode } from '../type/route-tree.type.ts';
18
+ import type { MarkdownRenderer } from '../type/markdown.type.ts';
19
+ import type { SpaMode } from '../type/widget.type.ts';
20
+ import type { ContextProvider } from '../type/component.type.ts';
21
+ import {
22
+ ROUTES_MANIFEST_PATH,
23
+ WIDGETS_MANIFEST_PATH,
24
+ } from '../runtime/abstract.runtime.ts';
25
+ export const DEFAULT_BASE_PATH = { html: '/html', md: '/md', app: '/app' };
26
+ export type BasePath = Record<keyof typeof DEFAULT_BASE_PATH, string>;
27
+
28
+ export class Emroute {
29
+ readonly htmlRenderer: SsrHtmlRenderer | null;
30
+ readonly mdRenderer: SsrMdRenderer | null;
31
+ readonly shell: string;
32
+
33
+ private constructor(
34
+ htmlRenderer: SsrHtmlRenderer | null,
35
+ mdRenderer: SsrMdRenderer | null,
36
+ shell: string,
37
+ private readonly runtime: Runtime,
38
+ private readonly htmlBase: string,
39
+ private readonly mdBase: string,
40
+ private readonly appBase: string,
41
+ private readonly spa: string,
42
+ private readonly title: string,
43
+ ) {
44
+ this.htmlRenderer = htmlRenderer;
45
+ this.mdRenderer = mdRenderer;
46
+ this.shell = shell;
47
+ }
48
+
49
+ static async create(
50
+ config: {
51
+ routeTree?: RouteNode;
52
+ widgets?: WidgetRegistry;
53
+ spa?: SpaMode;
54
+ basePath?: BasePath;
55
+ title?: string;
56
+ markdownRenderer?: MarkdownRenderer;
57
+ extendContext?: ContextProvider;
58
+ moduleLoaders?: Record<string, () => Promise<unknown>>;
59
+ },
60
+ runtime: Runtime,
61
+ ): Promise<Emroute> {
62
+ const { spa = 'root' } = config;
63
+ const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
64
+
65
+ // ── Verify route manifest exists ──────────────────────────────────
66
+
67
+ const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
68
+ if (manifestResponse.status === 404 && !config.routeTree) {
69
+ throw new Error(
70
+ `[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. ` +
71
+ 'Provide routeTree in config or ensure the runtime produces it.',
72
+ );
73
+ }
74
+
75
+ if (config.routeTree && manifestResponse.status === 404) {
76
+ await runtime.command(ROUTES_MANIFEST_PATH, {
77
+ body: JSON.stringify(config.routeTree),
78
+ });
79
+ }
80
+
81
+ // ── Pipeline ──────────────────────────────────────────────────────
82
+
83
+ const pipeline = new Pipeline({
84
+ runtime,
85
+ ...(config.extendContext ? { contextProvider: config.extendContext } : {}),
86
+ ...(config.moduleLoaders ? { moduleLoaders: config.moduleLoaders } : {}),
87
+ });
88
+
89
+ // ── Widgets ───────────────────────────────────────────────────────
90
+
91
+ let widgets: WidgetRegistry | undefined = config.widgets;
92
+
93
+ const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
94
+ if (widgetsResponse.status !== 404) {
95
+ const entries: WidgetManifestEntry[] = await widgetsResponse.json();
96
+ if (!config.widgets) {
97
+ widgets = await Emroute.importWidgets(entries, runtime);
98
+ }
99
+ }
100
+
101
+ // ── Renderers ─────────────────────────────────────────────────────
102
+
103
+ let ssrHtmlRenderer: SsrHtmlRenderer | null = null;
104
+ let ssrMdRenderer: SsrMdRenderer | null = null;
105
+
106
+ if (spa !== 'only') {
107
+ ssrHtmlRenderer = new SsrHtmlRenderer(pipeline, {
108
+ ...(config.markdownRenderer ? { markdownRenderer: config.markdownRenderer } : {}),
109
+ ...(widgets ? { widgets } : {}),
110
+ });
111
+
112
+ ssrMdRenderer = new SsrMdRenderer(pipeline, {
113
+ ...(widgets ? { widgets } : {}),
114
+ });
115
+ }
116
+
117
+ // ── HTML shell ────────────────────────────────────────────────────
118
+
119
+ const title = config.title ?? 'emroute';
120
+ const shellBase = (spa === 'root' || spa === 'only') ? appBase : htmlBase;
121
+ let shell = await Emroute.resolveShell(runtime, title, shellBase);
122
+
123
+ if ((await runtime.query('/main.css')).status !== 404) {
124
+ shell = shell.replace('</head>', ' <link rel="stylesheet" href="/main.css">\n</head>');
125
+ }
126
+
127
+ return new Emroute(
128
+ ssrHtmlRenderer,
129
+ ssrMdRenderer,
130
+ shell,
131
+ runtime,
132
+ htmlBase,
133
+ mdBase,
134
+ appBase,
135
+ spa,
136
+ title,
137
+ );
138
+ }
139
+
140
+ // ── handleRequest ─────────────────────────────────────────────────
141
+
142
+ async handleRequest(req: Request): Promise<Response | null> {
143
+ const url = new URL(req.url);
144
+ const pathname = url.pathname;
145
+
146
+ const mdPrefix = this.mdBase + '/';
147
+ const htmlPrefix = this.htmlBase + '/';
148
+ const appPrefix = this.appBase + '/';
149
+
150
+ // SSR Markdown: /md/*
151
+ if (
152
+ this.mdRenderer &&
153
+ (pathname.startsWith(mdPrefix) || pathname === this.mdBase)
154
+ ) {
155
+ const routePath = pathname === this.mdBase ? '/' : pathname.slice(this.mdBase.length);
156
+ if (routePath.length > 1 && routePath.endsWith('/')) {
157
+ const canonical = this.mdBase + routePath.slice(0, -1) + (url.search || '');
158
+ return Response.redirect(new URL(canonical, url.origin), 301);
159
+ }
160
+ try {
161
+ const routeUrl = new URL(routePath + url.search, url.origin);
162
+ const { content, status, redirect } = await this.mdRenderer.render(routeUrl, req.signal);
163
+ if (redirect) {
164
+ const target = redirect.startsWith('/') ? this.mdBase + redirect : redirect;
165
+ return Response.redirect(new URL(target, url.origin), status);
166
+ }
167
+ return new Response(rewriteMdLinks(content, this.mdBase, [this.mdBase, this.htmlBase]), {
168
+ status,
169
+ headers: { 'Content-Type': 'text/markdown; charset=utf-8; variant=CommonMark' },
170
+ });
171
+ } catch (e) {
172
+ console.error(`[emroute] Error rendering ${pathname}:`, e);
173
+ return new Response('Internal Server Error', { status: 500 });
174
+ }
175
+ }
176
+
177
+ // SSR HTML: /html/*
178
+ if (
179
+ this.htmlRenderer &&
180
+ (pathname.startsWith(htmlPrefix) || pathname === this.htmlBase)
181
+ ) {
182
+ const routePath = pathname === this.htmlBase ? '/' : pathname.slice(this.htmlBase.length);
183
+ if (routePath.length > 1 && routePath.endsWith('/')) {
184
+ const canonical = this.htmlBase + routePath.slice(0, -1) + (url.search || '');
185
+ return Response.redirect(new URL(canonical, url.origin), 301);
186
+ }
187
+ try {
188
+ const routeUrl = new URL(routePath + url.search, url.origin);
189
+ const result = await this.htmlRenderer.render(routeUrl, req.signal);
190
+ if (result.redirect) {
191
+ const target = result.redirect.startsWith('/') ? this.htmlBase + result.redirect : result.redirect;
192
+ return Response.redirect(new URL(target, url.origin), result.status);
193
+ }
194
+ const ssrTitle = result.title ?? this.title;
195
+ const html = Emroute.injectSsrContent(this.shell, result.content, ssrTitle, pathname);
196
+ return new Response(html, {
197
+ status: result.status,
198
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
199
+ });
200
+ } catch (e) {
201
+ console.error(`[emroute] Error rendering ${pathname}:`, e);
202
+ return new Response('Internal Server Error', { status: 500 });
203
+ }
204
+ }
205
+
206
+ // /app/*
207
+ if (pathname.startsWith(appPrefix) || pathname === this.appBase) {
208
+ return new Response(this.shell, {
209
+ status: 200,
210
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
211
+ });
212
+ }
213
+
214
+ // Unhandled SSR paths in 'only' mode — serve shell
215
+ if (
216
+ pathname.startsWith(htmlPrefix) || pathname === this.htmlBase ||
217
+ pathname.startsWith(mdPrefix) || pathname === this.mdBase
218
+ ) {
219
+ return new Response(this.shell, {
220
+ status: 200,
221
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
222
+ });
223
+ }
224
+
225
+ // Static files
226
+ const lastSegment = pathname.split('/').pop() ?? '';
227
+ if (lastSegment.includes('.')) {
228
+ const fileResponse = await this.runtime.handle(pathname);
229
+ if (fileResponse.status === 200) return fileResponse;
230
+ return null;
231
+ }
232
+
233
+ // Bare paths → redirect
234
+ const base = (this.spa === 'root' || this.spa === 'only') ? this.appBase : this.htmlBase;
235
+ const bare = pathname === '/' ? '' : pathname.slice(1).replace(/\/$/, '');
236
+ return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
237
+ }
238
+
239
+ // ── Private static helpers ────────────────────────────────────────
240
+
241
+ private static extractWidgetExport(
242
+ mod: Record<string, unknown>,
243
+ ): WidgetComponent | null {
244
+ for (const value of Object.values(mod)) {
245
+ if (!value) continue;
246
+ if (typeof value === 'object' && 'getData' in value) {
247
+ return value as WidgetComponent;
248
+ }
249
+ if (typeof value === 'function' && value.prototype?.getData) {
250
+ return new (value as new () => WidgetComponent)();
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+
256
+ private static async importWidgets(
257
+ entries: WidgetManifestEntry[],
258
+ runtime: Runtime,
259
+ ): Promise<WidgetRegistry> {
260
+ const registry = new WidgetRegistry();
261
+
262
+ for (const entry of entries) {
263
+ try {
264
+ const runtimePath = entry.modulePath.startsWith('/')
265
+ ? entry.modulePath
266
+ : `/${entry.modulePath}`;
267
+
268
+ const mod = await runtime.loadModule(runtimePath) as Record<string, unknown>;
269
+ const instance = Emroute.extractWidgetExport(mod);
270
+ if (!instance) continue;
271
+ registry.add(instance, runtimePath);
272
+ } catch (e) {
273
+ console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
274
+ }
275
+ }
276
+
277
+ return registry;
278
+ }
279
+
280
+ private static buildHtmlShell(title: string, htmlBase: string): string {
281
+ const baseTag = htmlBase ? `\n <base href="${escapeHtml(htmlBase)}/">` : '';
282
+ return `<!DOCTYPE html>
283
+ <html>
284
+ <head>${baseTag}
285
+ <meta charset="utf-8">
286
+ <meta name="viewport" content="width=device-width, initial-scale=1">
287
+ <title>${escapeHtml(title)}</title>
288
+ <style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
289
+ </head>
290
+ <body>
291
+ <router-slot></router-slot>
292
+ </body>
293
+ </html>`;
294
+ }
295
+
296
+ private static injectSsrContent(
297
+ html: string,
298
+ content: string,
299
+ title: string | undefined,
300
+ ssrRoute?: string,
301
+ ): string {
302
+ const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
303
+ if (!slotPattern.test(html)) return html;
304
+
305
+ const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : '';
306
+ html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
307
+
308
+ if (title) {
309
+ html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
310
+ }
311
+
312
+ return html;
313
+ }
314
+
315
+ private static async resolveShell(
316
+ runtime: Runtime,
317
+ title: string,
318
+ htmlBase: string,
319
+ ): Promise<string> {
320
+ const response = await runtime.query('/index.html');
321
+ if (response.status !== 404) return await response.text();
322
+ return Emroute.buildHtmlShell(title, htmlBase);
323
+ }
324
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Component Types
3
+ *
4
+ * Types for the component system. Separate from route types.
5
+ */
6
+
7
+ import type { RouteInfo } from './route.type.ts';
8
+
9
+ /** Shape of companion file contents (html, md, css). */
10
+ export type FileContents = { html?: string; md?: string; css?: string };
11
+
12
+ /** Context passed to components during rendering. */
13
+ export interface ComponentContext extends RouteInfo {
14
+ /** @deprecated Use context.url.pathname */
15
+ readonly pathname: string;
16
+ /** @deprecated Use context.url.searchParams */
17
+ readonly searchParams: URLSearchParams;
18
+ readonly files?: Readonly<FileContents>;
19
+ readonly signal?: AbortSignal;
20
+ readonly isLeaf?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Enriches the base ComponentContext with app-level services.
25
+ * Registered once at server creation; called for every context construction.
26
+ */
27
+ export type ContextProvider = (base: ComponentContext) => ComponentContext;
28
+
29
+ /** Render context determines how components are rendered. */
30
+ export type RenderContext = 'markdown' | 'html' | 'spa';
31
+
32
+ /** Component manifest entry for code generation. */
33
+ export interface ComponentManifestEntry {
34
+ name: string;
35
+ modulePath: string;
36
+ tagName: string;
37
+ type: 'page' | 'widget';
38
+ pattern?: string;
39
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Element Types
3
+ */
4
+
5
+ /** Custom element manifest entry for discovery and registration. */
6
+ export interface ElementManifestEntry {
7
+ name: string;
8
+ modulePath: string;
9
+ tagName: string;
10
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Logger Interface
3
+ *
4
+ * Minimal pluggable logger. Default: no-op (silent degradation).
5
+ * Pass via PipelineOptions to wire in.
6
+ */
7
+ export interface Logger {
8
+ error(msg: string, error?: Error): void;
9
+ warn(msg: string): void;
10
+ }
11
+
12
+ const noop = () => {};
13
+
14
+ /** Default no-op logger. */
15
+ export const defaultLogger: Logger = { error: noop, warn: noop };
16
+
17
+ /** @deprecated Pass `logger` in Emroute.create() config instead. This function is a no-op. */
18
+ export function setLogger(_logger: Logger): void {
19
+ console.warn('[emroute] setLogger() is deprecated. Pass `logger` in Emroute.create() config instead.');
20
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Markdown Renderer Interface
3
+ */
4
+
5
+ export interface MarkdownRenderer {
6
+ render(markdown: string): string;
7
+ init?(): Promise<void>;
8
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Route Tree
3
+ *
4
+ * Serializable tree structure that mirrors the filesystem layout.
5
+ *
6
+ * Each node corresponds to a URL segment. The tree is JSON-serializable
7
+ * (no Maps, no classes) so it can be written to disk, sent over the wire,
8
+ * or used directly as the in-memory trie for O(depth) route matching.
9
+ */
10
+
11
+ /** Files associated with a route (companion files discovered alongside the page). */
12
+ export interface RouteFiles {
13
+ ts?: string;
14
+ js?: string;
15
+ html?: string;
16
+ md?: string;
17
+ css?: string;
18
+ }
19
+
20
+ /** A single node in the route tree. */
21
+ export interface RouteNode {
22
+ files?: RouteFiles;
23
+ errorBoundary?: string;
24
+ redirect?: string;
25
+ children?: Record<string, RouteNode>;
26
+ dynamic?: { param: string; child: RouteNode };
27
+ wildcard?: { param: string; child: RouteNode };
28
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Route Types
3
+ *
4
+ * Pure routing types. No rendering, no navigation, no browser concerns.
5
+ */
6
+
7
+ export type { RouteNode, RouteFiles } from './route-tree.type.ts';
8
+
9
+ /** Parameters extracted from URL patterns. */
10
+ export type RouteParams = Readonly<Record<string, string>>;
11
+
12
+ /** Immutable route context built once per navigation, shared across the render pipeline. */
13
+ export interface RouteInfo {
14
+ readonly url: URL;
15
+ readonly params: RouteParams;
16
+ }
17
+
18
+ /** Supported file patterns in file-based routing. */
19
+ export type RouteFileType = 'page' | 'error' | 'redirect';
20
+
21
+ /** Redirect configuration. */
22
+ export interface RedirectConfig {
23
+ to: string;
24
+ status: 301 | 302;
25
+ }
26
+
27
+ /** Route configuration for a single matched route. */
28
+ export interface RouteConfig {
29
+ pattern: string;
30
+ type: RouteFileType;
31
+ modulePath: string;
32
+ files?: import('./route-tree.type.ts').RouteFiles;
33
+ parent?: string;
34
+ statusCode?: number;
35
+ }
36
+
37
+ /** Result of matching a URL against routes. */
38
+ export interface MatchedRoute {
39
+ readonly route: RouteConfig;
40
+ readonly params: RouteParams;
41
+ }
42
+
43
+ /** Error boundary configuration. */
44
+ export interface ErrorBoundary {
45
+ pattern: string;
46
+ modulePath: string;
47
+ }
48
+
49
+ /** Router state for history management. */
50
+ export interface RouterState {
51
+ pathname: string;
52
+ params: RouteParams;
53
+ scrollY?: number;
54
+ }
55
+
56
+ /** Navigation options. */
57
+ export interface NavigateOptions {
58
+ replace?: boolean;
59
+ state?: RouterState;
60
+ hash?: string;
61
+ }
62
+
63
+ /** Router event types. */
64
+ export type RouterEventType = 'navigate' | 'error' | 'load';
65
+
66
+ /** Router event payload. */
67
+ export interface RouterEvent {
68
+ type: RouterEventType;
69
+ pathname: string;
70
+ params: RouteParams;
71
+ error?: Error;
72
+ }
73
+
74
+ /** Router event listener. */
75
+ export type RouterEventListener = (event: RouterEvent) => void;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Widget Types
3
+ */
4
+
5
+ /** SPA rendering mode. */
6
+ export type SpaMode = 'none' | 'leaf' | 'root' | 'only';
7
+
8
+ /** Widget manifest entry for discovery and registration. */
9
+ export interface WidgetManifestEntry {
10
+ name: string;
11
+ modulePath: string;
12
+ tagName: string;
13
+ files?: { html?: string; md?: string; css?: string };
14
+ }
15
+
16
+ /** Full widgets manifest (array, sorted by name). */
17
+ export type WidgetsManifest = WidgetManifestEntry[];
18
+
19
+ /** Parsed widget block from markdown fenced code. */
20
+ export interface ParsedWidgetBlock {
21
+ fullMatch: string;
22
+ widgetName: string;
23
+ params: Record<string, unknown> | null;
24
+ parseError?: string;
25
+ startIndex: number;
26
+ endIndex: number;
27
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pure HTML utilities. No DOM, no browser APIs.
3
+ */
4
+
5
+ /** HTML attribute name marking a widget as server-rendered. */
6
+ export const SSR_ATTR = 'ssr';
7
+
8
+ /** HTML attribute name for lazy-loading widgets. */
9
+ export const LAZY_ATTR = 'lazy';
10
+
11
+ const BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
12
+
13
+ /** Throw if a redirect URL uses a dangerous protocol. */
14
+ export function assertSafeRedirect(url: string): void {
15
+ if (BLOCKED_PROTOCOLS.test(url.trim())) {
16
+ throw new Error(`Unsafe redirect URL blocked: ${url}`);
17
+ }
18
+ }
19
+
20
+ export function escapeHtml(text: string): string {
21
+ return text
22
+ .replaceAll('&', '&amp;')
23
+ .replaceAll('<', '&lt;')
24
+ .replaceAll('>', '&gt;')
25
+ .replaceAll('"', '&quot;')
26
+ .replaceAll("'", '&#39;')
27
+ .replaceAll('`', '&#96;');
28
+ }
29
+
30
+ export function unescapeHtml(text: string): string {
31
+ return text
32
+ .replaceAll('&#96;', '`')
33
+ .replaceAll('&#39;', "'")
34
+ .replaceAll('&quot;', '"')
35
+ .replaceAll('&gt;', '>')
36
+ .replaceAll('&lt;', '<')
37
+ .replaceAll('&amp;', '&');
38
+ }
39
+
40
+ export function scopeWidgetCss(css: string, widgetName: string): string {
41
+ return `@scope (widget-${widgetName}) {\n${css}\n}`;
42
+ }
43
+
44
+ /** Status code to message mapping. */
45
+ export const STATUS_MESSAGES: Record<number, string> = {
46
+ 401: 'Unauthorized',
47
+ 403: 'Forbidden',
48
+ 404: 'Not Found',
49
+ 500: 'Internal Server Error',
50
+ };
@@ -5,10 +5,8 @@
5
5
  * Skips fenced code blocks and links already under a known base path.
6
6
  */
7
7
 
8
- /** Rewrite internal absolute links in markdown to include the base path prefix. */
9
8
  export function rewriteMdLinks(markdown: string, base: string, skipPrefixes: string[]): string {
10
9
  const prefix = base + '/';
11
- // Negative lookahead: skip links already under a known base path
12
10
  const skip = skipPrefixes.map((p) => p.slice(1) + '/').join('|');
13
11
  const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, 'g');
14
12
  const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, 'g');
@@ -17,14 +15,14 @@ export function rewriteMdLinks(markdown: string, base: string, skipPrefixes: str
17
15
  let inCodeBlock = false;
18
16
 
19
17
  for (let i = 0; i < lines.length; i++) {
20
- if (lines[i].startsWith('```')) {
18
+ if (lines[i]!.startsWith('```')) {
21
19
  inCodeBlock = !inCodeBlock;
22
20
  continue;
23
21
  }
24
22
  if (inCodeBlock) continue;
25
23
 
26
- lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
27
- lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
24
+ lines[i] = lines[i]!.replaceAll(inlineRe, `](${prefix}`);
25
+ lines[i] = lines[i]!.replaceAll(refRe, `$1${prefix}`);
28
26
  }
29
27
 
30
28
  return lines.join('\n');
@@ -17,7 +17,6 @@ import type { RouteNode } from '../type/route-tree.type.ts';
17
17
  export function resolveTargetNode(node: RouteNode, name: string, isRoot: boolean): RouteNode {
18
18
  if (name === 'index') {
19
19
  if (isRoot) return node;
20
- // Non-root index → wildcard catch-all
21
20
  node.wildcard ??= { param: 'rest', child: {} };
22
21
  return node.wildcard.child;
23
22
  }
@@ -28,7 +27,6 @@ export function resolveTargetNode(node: RouteNode, name: string, isRoot: boolean
28
27
  return node.dynamic.child;
29
28
  }
30
29
 
31
- // Static segment
32
30
  node.children ??= {};
33
31
  node.children[name] ??= {};
34
32
  return node.children[name];