@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
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, stat, readdir, mkdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, stat, readdir, mkdir, unlink } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import {
4
4
  CONTENT_TYPES,
@@ -8,6 +8,7 @@ import {
8
8
  Runtime,
9
9
  type RuntimeConfig,
10
10
  WIDGETS_MANIFEST_PATH,
11
+ ELEMENTS_MANIFEST_PATH,
11
12
  } from '../../abstract.runtime.ts';
12
13
 
13
14
  /**
@@ -37,6 +38,8 @@ export class UniversalFsRuntime extends Runtime {
37
38
  switch (method) {
38
39
  case 'PUT':
39
40
  return this.write(path, body);
41
+ case 'DELETE':
42
+ return this.delete(path);
40
43
  default:
41
44
  return this.read(path);
42
45
  }
@@ -107,6 +110,7 @@ export class UniversalFsRuntime extends Runtime {
107
110
  const pathname = path.slice(this.root.length);
108
111
  if (pathname === ROUTES_MANIFEST_PATH) return this.resolveRoutesManifest();
109
112
  if (pathname === WIDGETS_MANIFEST_PATH) return this.resolveWidgetsManifest();
113
+ if (pathname === ELEMENTS_MANIFEST_PATH) return this.resolveElementsManifest();
110
114
  return new Response('Not Found', { status: 404 });
111
115
  }
112
116
  return new Response(`Internal Error: ${error}`, { status: 500 });
@@ -136,6 +140,18 @@ export class UniversalFsRuntime extends Runtime {
136
140
  }
137
141
  }
138
142
 
143
+ private async delete(path: string): Promise<Response> {
144
+ try {
145
+ await unlink(path);
146
+ return new Response(null, { status: 204 });
147
+ } catch (error) {
148
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
149
+ return new Response('Not Found', { status: 404 });
150
+ }
151
+ return new Response(`Delete failed: ${error}`, { status: 500 });
152
+ }
153
+ }
154
+
139
155
  override loadModule(path: string): Promise<unknown> {
140
156
  return import(this.root + path);
141
157
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Build Utilities
3
3
  *
4
- * Standalone client bundling — extracted from Runtime so that build is a
4
+ * Standalone client build step — extracted from Runtime so that build is a
5
5
  * separate concern from storage. Call `buildClientBundles()` before
6
- * `createEmrouteServer()` to produce emroute.js + app.js.
6
+ * `Emroute.create()` to produce emroute.js + app.js.
7
7
  *
8
8
  * Route tree and widget manifest are fetched as JSON at boot time by
9
9
  * `bootEmrouteApp()` — no longer compiled into app.js.
@@ -20,12 +20,15 @@ import type { Runtime } from '../runtime/abstract.runtime.ts';
20
20
  import {
21
21
  ROUTES_MANIFEST_PATH,
22
22
  WIDGETS_MANIFEST_PATH,
23
- } from '../runtime/abstract.runtime.ts';
24
- import type { RouteNode } from '../src/type/route-tree.type.ts';
25
- import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
23
+ ELEMENTS_MANIFEST_PATH,
24
+ } from '../core/runtime/abstract.runtime.ts';
25
+ import type { RouteNode } from '../core/type/route-tree.type.ts';
26
+ import type { WidgetManifestEntry } from '../core/type/widget.type.ts';
27
+ import type { ElementManifestEntry } from '../core/type/element.type.ts';
26
28
  import { generateMainTs } from './codegen.util.ts';
27
- import type { SpaMode } from '../src/type/widget.type.ts';
29
+ import type { SpaMode } from '../core/type/widget.type.ts';
28
30
 
31
+ /** Package specifiers that map to emroute.js via import map. */
29
32
  export const EMROUTE_EXTERNALS = [
30
33
  '@emkodev/emroute/spa',
31
34
  '@emkodev/emroute/overlay',
@@ -34,15 +37,12 @@ export const EMROUTE_EXTERNALS = [
34
37
  '@emkodev/emroute/runtime/fetch',
35
38
  ] as const;
36
39
 
37
- /** esbuild namespace for virtual `emroute:routes` / `emroute:widgets` modules. */
38
- export const EMROUTE_VIRTUAL_NS = 'emroute';
39
-
40
40
  export interface BuildOptions {
41
41
  /** Runtime instance to read manifests and source files from. */
42
42
  runtime: Runtime;
43
- /** Filesystem root for esbuild resolution (e.g. process.cwd() or the app directory). */
43
+ /** Filesystem root for resolving the pre-built emroute.js bundle. */
44
44
  root: string;
45
- /** SPA mode — skips bundling when 'none'. */
45
+ /** SPA mode — skips build when 'none'. */
46
46
  spa: SpaMode;
47
47
  /** Consumer's SPA entry point (e.g. '/main.ts'). When absent, auto-generates one. */
48
48
  entryPoint?: string;
@@ -53,13 +53,13 @@ export interface BuildOptions {
53
53
  const DEFAULT_BUNDLE_PATHS = { emroute: '/emroute.js', app: '/app.js' };
54
54
 
55
55
  /**
56
- * Build client bundles and write them into the runtime.
56
+ * Build client assets and write them into the runtime.
57
57
  *
58
58
  * Produces:
59
59
  * - Merged .js modules — each .ts page/widget transpiled with companions inlined
60
60
  * - Updated manifests — route tree and widget manifest reference .js paths
61
- * - emroute.js — pre-built from dist/ (no esbuild needed for this)
62
- * - app.js — consumer entry point (esbuild only touches consumer code)
61
+ * - emroute.js — pre-built from dist/ (copied into runtime)
62
+ * - app.js — consumer entry point (transpiled from .ts, no bundler)
63
63
  * - index.html — shell with import map + script tags (if not already present)
64
64
  */
65
65
  export async function buildClientBundles(options: BuildOptions): Promise<void> {
@@ -71,17 +71,14 @@ export async function buildClientBundles(options: BuildOptions): Promise<void> {
71
71
  // Merge .ts modules → .js with inlined companions, update manifests
72
72
  await mergeModules(runtime);
73
73
 
74
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
- const esbuild = await loadEsbuild() as any;
76
-
77
74
  // Copy pre-built emroute.js from the package dist/
78
75
  const consumerRequire = createRequire(root + '/');
79
76
  const emrouteJsPath = resolvePrebuiltBundle(consumerRequire);
80
77
  const emrouteJs = await readFile(emrouteJsPath);
81
78
  await runtime.command(paths.emroute, { body: emrouteJs });
82
79
 
83
- // App bundle — consumer's main.ts bundled with esbuild.
84
- // Try filesystem first (where node_modules lives), then runtime, then generate.
80
+ // App entry point transpile consumer's main.ts (or generate a default one).
81
+ // Imports resolve via the import map in index.html no bundler needed.
85
82
  const ep = entryPoint ?? '/main.ts';
86
83
  let source: string | undefined;
87
84
  try {
@@ -92,19 +89,8 @@ export async function buildClientBundles(options: BuildOptions): Promise<void> {
92
89
  }
93
90
  source ??= generateMainTs(spa, '@emkodev/emroute');
94
91
 
95
- const result = await esbuild.build({
96
- bundle: true,
97
- write: false,
98
- format: 'esm' as const,
99
- platform: 'browser' as const,
100
- stdin: { contents: source, loader: 'ts', resolveDir: root },
101
- outfile: paths.app,
102
- external: [...EMROUTE_EXTERNALS],
103
- });
104
-
105
- for (const file of result.outputFiles) {
106
- await runtime.command(paths.app, { body: file.contents as unknown as BodyInit });
107
- }
92
+ const appJs = await runtime.transpile(source);
93
+ await runtime.command(paths.app, { body: appJs });
108
94
 
109
95
  // Copy main.css from disk into runtime if it exists (and runtime doesn't have it)
110
96
  if ((await runtime.query('/main.css')).status === 404) {
@@ -116,8 +102,6 @@ export async function buildClientBundles(options: BuildOptions): Promise<void> {
116
102
 
117
103
  // Write shell (index.html) if not already present
118
104
  await writeShell(runtime, paths);
119
-
120
- await esbuild.stop();
121
105
  }
122
106
 
123
107
  /**
@@ -146,10 +130,23 @@ async function writeShell(
146
130
  ): Promise<void> {
147
131
  if ((await runtime.query('/index.html')).status !== 404) return;
148
132
 
133
+ // Base emroute imports
149
134
  const imports: Record<string, string> = {};
150
135
  for (const pkg of EMROUTE_EXTERNALS) {
151
136
  imports[pkg] = paths.emroute;
152
137
  }
138
+
139
+ // Merge user-provided importmap.json (user entries win on conflict)
140
+ const mapResponse = await runtime.query('/importmap.json');
141
+ if (mapResponse.status !== 404) {
142
+ const userMap = await mapResponse.json() as { imports?: Record<string, string> };
143
+ if (userMap.imports) {
144
+ for (const [key, value] of Object.entries(userMap.imports)) {
145
+ imports[key] = value;
146
+ }
147
+ }
148
+ }
149
+
153
150
  const importMap = JSON.stringify({ imports }, null, 2);
154
151
 
155
152
  const html = `<!DOCTYPE html>
@@ -251,11 +248,10 @@ async function mergeModules(runtime: Runtime): Promise<void> {
251
248
  // Merge route modules
252
249
  async function walkRoutes(node: RouteNode): Promise<void> {
253
250
  if (node.files?.ts) {
254
- const companions = {
255
- html: node.files.html,
256
- md: node.files.md,
257
- css: node.files.css,
258
- };
251
+ const companions: { html?: string; md?: string; css?: string } = {};
252
+ if (node.files.html) companions.html = node.files.html;
253
+ if (node.files.md) companions.md = node.files.md;
254
+ if (node.files.css) companions.css = node.files.css;
259
255
  node.files.js = await transpileAndMerge(runtime, node.files.ts, companions);
260
256
  delete node.files.ts;
261
257
  delete node.files.html;
@@ -290,6 +286,19 @@ async function mergeModules(runtime: Runtime): Promise<void> {
290
286
  }
291
287
  }
292
288
 
289
+ // Read element manifest
290
+ const elementsResponse = await runtime.query(ELEMENTS_MANIFEST_PATH);
291
+ const elementEntries: ElementManifestEntry[] = elementsResponse.status !== 404
292
+ ? await elementsResponse.json()
293
+ : [];
294
+
295
+ // Merge element modules
296
+ for (const entry of elementEntries) {
297
+ if (entry.modulePath.endsWith('.ts')) {
298
+ entry.modulePath = await transpileAndMerge(runtime, entry.modulePath);
299
+ }
300
+ }
301
+
293
302
  // Write updated manifests back
294
303
  await runtime.command(ROUTES_MANIFEST_PATH, {
295
304
  body: JSON.stringify(routeTree),
@@ -297,12 +306,9 @@ async function mergeModules(runtime: Runtime): Promise<void> {
297
306
  await runtime.command(WIDGETS_MANIFEST_PATH, {
298
307
  body: JSON.stringify(widgetEntries),
299
308
  });
300
- }
301
-
302
- // ── esbuild loader ────────────────────────────────────────────────────
303
-
304
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
305
- async function loadEsbuild(): Promise<any> {
306
- const consumerRequire = createRequire(process.cwd() + '/');
307
- return consumerRequire('esbuild');
309
+ if (elementEntries.length > 0) {
310
+ await runtime.command(ELEMENTS_MANIFEST_PATH, {
311
+ body: JSON.stringify(elementEntries),
312
+ });
313
+ }
308
314
  }
@@ -6,7 +6,7 @@
6
6
  * fetching manifests, creating the runtime, and wiring navigation.
7
7
  */
8
8
 
9
- import type { SpaMode } from '../src/type/widget.type.ts';
9
+ import type { SpaMode } from '../core/type/widget.type.ts';
10
10
 
11
11
  /**
12
12
  * Generate a minimal main.ts entry point.
@@ -1,420 +1,20 @@
1
1
  /**
2
- * Emroute Server
3
- *
4
- * Runtime-agnostic server that handles SSR rendering, manifest resolution,
5
- * static file serving, and route matching. Works with any Runtime implementation.
6
- *
7
- * Usage (standalone):
8
- * ```ts
9
- * import { createEmrouteServer } from '@emkodev/emroute/server';
10
- * import { BunFsRuntime } from '@emkodev/emroute/runtime/bun/fs';
11
- *
12
- * const runtime = new BunFsRuntime('.', { routesDir: '/routes' });
13
- * const emroute = await createEmrouteServer({ spa: 'root' }, runtime);
14
- *
15
- * Bun.serve({ fetch: (req) => emroute.handleRequest(req) ?? new Response('Not Found', { status: 404 }) });
16
- * ```
17
- *
18
- * Usage (composable):
19
- * ```ts
20
- * const emroute = await createEmrouteServer(config, runtime);
21
- *
22
- * Bun.serve({ async fetch(req) {
23
- * if (isApiRoute(req)) return handleApi(req);
24
- * const response = await emroute.handleRequest(req);
25
- * if (response) return response;
26
- * return new Response('Not Found', { status: 404 });
27
- * }});
28
- * ```
2
+ * Emroute — re-exports from core/server.
29
3
  */
4
+ export { Emroute } from '../core/server/emroute.server.ts';
30
5
 
31
- import { DEFAULT_BASE_PATH } from '../src/route/route.core.ts';
32
- import { RouteTrie } from '../src/route/route.trie.ts';
33
- import { SsrHtmlRouter } from '../src/renderer/ssr/html.renderer.ts';
34
- import { SsrMdRouter } from '../src/renderer/ssr/md.renderer.ts';
35
- import type { RouteNode } from '../src/type/route-tree.type.ts';
36
- import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
37
- import { WidgetRegistry } from '../src/widget/widget.registry.ts';
38
- import type { WidgetComponent } from '../src/component/widget.component.ts';
39
- import { escapeHtml } from '../src/util/html.util.ts';
40
- import { rewriteMdLinks } from '../src/util/md.util.ts';
41
- import {
42
- ROUTES_MANIFEST_PATH,
43
- Runtime,
44
- WIDGETS_MANIFEST_PATH,
45
- } from '../runtime/abstract.runtime.ts';
46
- import type { EmrouteServer, EmrouteServerConfig } from './server-api.type.ts';
6
+ // ── Deprecated aliases ───────────────────────────────────────────────
47
7
 
48
- // ── Module loaders ─────────────────────────────────────────────────────
8
+ import { Emroute as _Emroute } from '../core/server/emroute.server.ts';
9
+ import type { Runtime } from '../core/runtime/abstract.runtime.ts';
49
10
 
50
- /**
51
- * Collect all .ts module paths from a RouteNode tree and create loaders.
52
- * Uses `runtime.loadModule()` — each runtime decides how to load modules
53
- * (filesystem import, SQLite transpile + blob URL, etc.).
54
- */
55
- function createModuleLoaders(
56
- tree: RouteNode,
57
- runtime: Runtime,
58
- ): Record<string, () => Promise<unknown>> {
59
- const paths = new Set<string>();
60
-
61
- function walk(node: RouteNode): void {
62
- const modulePath = node.files?.ts ?? node.files?.js;
63
- if (modulePath) paths.add(modulePath);
64
- if (node.redirect) paths.add(node.redirect);
65
- if (node.errorBoundary) paths.add(node.errorBoundary);
66
-
67
- if (node.children) {
68
- for (const child of Object.values(node.children)) walk(child);
69
- }
70
- if (node.dynamic) walk(node.dynamic.child);
71
- if (node.wildcard) walk(node.wildcard.child);
72
- }
73
-
74
- walk(tree);
75
-
76
- const loaders: Record<string, () => Promise<unknown>> = {};
77
- for (const path of paths) {
78
- loaders[path] = () => runtime.loadModule(path);
79
- }
80
- return loaders;
81
- }
82
-
83
- // ── Widget helpers ─────────────────────────────────────────────────────
84
-
85
- /** Find a WidgetComponent export from a module. */
86
- function extractWidgetExport(
87
- mod: Record<string, unknown>,
88
- ): WidgetComponent | null {
89
- for (const value of Object.values(mod)) {
90
- if (!value) continue;
91
- if (typeof value === 'object' && 'getData' in value) {
92
- return value as WidgetComponent;
93
- }
94
- if (typeof value === 'function' && value.prototype?.getData) {
95
- return new (value as new () => WidgetComponent)();
96
- }
97
- }
98
- return null;
99
- }
100
-
101
- /** Import widget modules for SSR via runtime.loadModule(). */
102
- async function importWidgets(
103
- entries: WidgetManifestEntry[],
104
- runtime: Runtime,
105
- manual?: WidgetRegistry,
106
- ): Promise<{
107
- registry: WidgetRegistry;
108
- widgetFiles: Record<string, { html?: string; md?: string; css?: string }>;
109
- }> {
110
- const registry = new WidgetRegistry();
111
- const widgetFiles: Record<string, { html?: string; md?: string; css?: string }> = {};
112
-
113
- for (const entry of entries) {
114
- try {
115
- const runtimePath = entry.modulePath.startsWith('/')
116
- ? entry.modulePath
117
- : `/${entry.modulePath}`;
118
-
119
- const mod = await runtime.loadModule(runtimePath) as Record<string, unknown>;
120
- const instance = extractWidgetExport(mod);
121
- if (!instance) continue;
122
- registry.add(instance);
123
-
124
- // Prefer inlined __files from merged module over manifest paths
125
- const inlined = mod.__files;
126
- if (inlined && typeof inlined === 'object') {
127
- widgetFiles[entry.name] = inlined as { html?: string; md?: string; css?: string };
128
- } else if (entry.files) {
129
- widgetFiles[entry.name] = entry.files;
130
- }
131
- } catch (e) {
132
- console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
133
- if (entry.files) widgetFiles[entry.name] = entry.files;
134
- }
135
- }
136
-
137
- if (manual) {
138
- for (const widget of manual) {
139
- registry.add(widget);
140
- }
141
- }
142
-
143
- return { registry, widgetFiles };
144
- }
145
-
146
- // ── HTML shell ─────────────────────────────────────────────────────────
147
-
148
- /** Build a default HTML shell. */
149
- function buildHtmlShell(title: string, htmlBase: string): string {
150
- const baseTag = htmlBase ? `\n <base href="${escapeHtml(htmlBase)}/">` : '';
151
- return `<!DOCTYPE html>
152
- <html>
153
- <head>${baseTag}
154
- <meta charset="utf-8">
155
- <meta name="viewport" content="width=device-width, initial-scale=1">
156
- <title>${escapeHtml(title)}</title>
157
- <style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
158
- </head>
159
- <body>
160
- <router-slot></router-slot>
161
- </body>
162
- </html>`;
163
- }
164
-
165
- /** Inject SSR-rendered content into an HTML shell. */
166
- function injectSsrContent(
167
- html: string,
168
- content: string,
169
- title: string | undefined,
170
- ssrRoute?: string,
171
- ): string {
172
- const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
173
- if (!slotPattern.test(html)) return html;
174
-
175
- const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : '';
176
- html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
177
-
178
- if (title) {
179
- html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
180
- }
181
-
182
- return html;
183
- }
184
-
185
- /** Read the HTML shell from runtime, with fallback to a default shell. */
186
- async function resolveShell(
187
- runtime: Runtime,
188
- title: string,
189
- htmlBase: string,
190
- ): Promise<string> {
191
- const response = await runtime.query('/index.html');
192
- if (response.status !== 404) return await response.text();
193
- return buildHtmlShell(title, htmlBase);
194
- }
195
-
196
- // ── More path helpers ─────────────────────────────────────────────────
11
+ /** @deprecated Use `Emroute` class directly. */
12
+ export type EmrouteServer = _Emroute;
197
13
 
198
- // ── createEmrouteServer ────────────────────────────────────────────────
199
-
200
- /**
201
- * Create an emroute server.
202
- *
203
- * All paths are Runtime-relative (starting with `/`). Runtime root = appRoot.
204
- */
205
- export async function createEmrouteServer(
206
- config: EmrouteServerConfig,
14
+ /** @deprecated Use `Emroute.create(config, runtime)`. */
15
+ export function createEmrouteServer(
16
+ config: Parameters<typeof _Emroute.create>[0],
207
17
  runtime: Runtime,
208
- ): Promise<EmrouteServer> {
209
- const {
210
- spa = 'root',
211
- } = config;
212
-
213
- const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
214
-
215
- // ── Route tree (read from runtime) ──────────────────────────────────
216
-
217
- let routeTree: RouteNode;
218
-
219
- if (config.routeTree) {
220
- routeTree = config.routeTree;
221
- } else {
222
- const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
223
- if (manifestResponse.status === 404) {
224
- throw new Error(
225
- `[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. ` +
226
- 'Provide routeTree in config or ensure the runtime produces it.',
227
- );
228
- }
229
- routeTree = await manifestResponse.json();
230
- }
231
-
232
- const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
233
- const resolver = new RouteTrie(routeTree);
234
-
235
- // ── Widgets (read from runtime) ────────────────────────────────────
236
-
237
- let widgets: WidgetRegistry | undefined = config.widgets;
238
- let widgetFiles: Record<string, { html?: string; md?: string; css?: string }> = {};
239
- let discoveredWidgetEntries: WidgetManifestEntry[] = [];
240
-
241
- const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
242
- if (widgetsResponse.status !== 404) {
243
- discoveredWidgetEntries = await widgetsResponse.json();
244
- if (config.widgets) {
245
- // Widgets pre-provided (e.g. browser bundle) — just collect file paths
246
- widgets = config.widgets;
247
- for (const entry of discoveredWidgetEntries) {
248
- if (entry.files) widgetFiles[entry.name] = entry.files;
249
- }
250
- } else {
251
- const imported = await importWidgets(discoveredWidgetEntries, runtime);
252
- widgets = imported.registry;
253
- widgetFiles = imported.widgetFiles;
254
- }
255
- }
256
-
257
- // ── SSR routers ──────────────────────────────────────────────────────
258
-
259
- let ssrHtmlRouter: SsrHtmlRouter | null = null;
260
- let ssrMdRouter: SsrMdRouter | null = null;
261
-
262
- function buildSsrRouters(): void {
263
- if (spa === 'only') {
264
- ssrHtmlRouter = null;
265
- ssrMdRouter = null;
266
- return;
267
- }
268
-
269
- ssrHtmlRouter = new SsrHtmlRouter(resolver, {
270
- fileReader: (path) => runtime.query(path, { as: 'text' }),
271
- moduleLoaders,
272
- markdownRenderer: config.markdownRenderer,
273
- extendContext: config.extendContext,
274
- widgets,
275
- widgetFiles,
276
- });
277
-
278
- ssrMdRouter = new SsrMdRouter(resolver, {
279
- fileReader: (path) => runtime.query(path, { as: 'text' }),
280
- moduleLoaders,
281
- extendContext: config.extendContext,
282
- widgets,
283
- widgetFiles,
284
- });
285
- }
286
-
287
- buildSsrRouters();
288
-
289
- // ── HTML shell ───────────────────────────────────────────────────────
290
-
291
- const title = config.title ?? 'emroute';
292
- let shell = await resolveShell(runtime, title, htmlBase);
293
-
294
- // Auto-discover main.css and inject <link> into <head>
295
- if ((await runtime.query('/main.css')).status !== 404) {
296
- shell = shell.replace('</head>', ' <link rel="stylesheet" href="/main.css">\n</head>');
297
- }
298
-
299
- // ── handleRequest ────────────────────────────────────────────────────
300
-
301
- async function handleRequest(req: Request): Promise<Response | null> {
302
- const url = new URL(req.url);
303
- const pathname = url.pathname;
304
-
305
- const mdPrefix = mdBase + '/';
306
- const htmlPrefix = htmlBase + '/';
307
- const appPrefix = appBase + '/';
308
-
309
- // SSR Markdown: /md/*
310
- if (
311
- ssrMdRouter &&
312
- (pathname.startsWith(mdPrefix) || pathname === mdBase)
313
- ) {
314
- // Normalize trailing slash: /md/about/ → 301 /md/about
315
- const routePath = pathname === mdBase ? '/' : pathname.slice(mdBase.length);
316
- if (routePath.length > 1 && routePath.endsWith('/')) {
317
- const canonical = mdBase + routePath.slice(0, -1) + (url.search || '');
318
- return Response.redirect(new URL(canonical, url.origin), 301);
319
- }
320
- try {
321
- const routeUrl = new URL(routePath + url.search, url.origin);
322
- const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
323
- if (redirect) {
324
- const target = redirect.startsWith('/') ? mdBase + redirect : redirect;
325
- return Response.redirect(new URL(target, url.origin), status);
326
- }
327
- return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
328
- status,
329
- headers: { 'Content-Type': 'text/markdown; charset=utf-8; variant=CommonMark' },
330
- });
331
- } catch (e) {
332
- console.error(`[emroute] Error rendering ${pathname}:`, e);
333
- return new Response('Internal Server Error', { status: 500 });
334
- }
335
- }
336
-
337
- // SSR HTML: /html/*
338
- if (
339
- ssrHtmlRouter &&
340
- (pathname.startsWith(htmlPrefix) || pathname === htmlBase)
341
- ) {
342
- // Normalize trailing slash: /html/about/ → 301 /html/about
343
- const routePath = pathname === htmlBase ? '/' : pathname.slice(htmlBase.length);
344
- if (routePath.length > 1 && routePath.endsWith('/')) {
345
- const canonical = htmlBase + routePath.slice(0, -1) + (url.search || '');
346
- return Response.redirect(new URL(canonical, url.origin), 301);
347
- }
348
- try {
349
- const routeUrl = new URL(routePath + url.search, url.origin);
350
- const result = await ssrHtmlRouter.render(routeUrl, req.signal);
351
- if (result.redirect) {
352
- const target = result.redirect.startsWith('/') ? htmlBase + result.redirect : result.redirect;
353
- return Response.redirect(new URL(target, url.origin), result.status);
354
- }
355
- const ssrTitle = result.title ?? title;
356
- const html = injectSsrContent(shell, result.content, ssrTitle, pathname);
357
- return new Response(html, {
358
- status: result.status,
359
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
360
- });
361
- } catch (e) {
362
- console.error(`[emroute] Error rendering ${pathname}:`, e);
363
- return new Response('Internal Server Error', { status: 500 });
364
- }
365
- }
366
-
367
- // /app/* — serve shell (browser JS takes over via createEmrouteApp)
368
- if (pathname.startsWith(appPrefix) || pathname === appBase) {
369
- return new Response(shell, {
370
- status: 200,
371
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
372
- });
373
- }
374
-
375
- // /html/* or /md/* that wasn't handled by SSR (e.g. 'only' mode) — serve shell
376
- if (
377
- pathname.startsWith(htmlPrefix) || pathname === htmlBase ||
378
- pathname.startsWith(mdPrefix) || pathname === mdBase
379
- ) {
380
- return new Response(shell, {
381
- status: 200,
382
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
383
- });
384
- }
385
-
386
- // Static files — only try runtime for paths with a file extension
387
- const lastSegment = pathname.split('/').pop() ?? '';
388
- if (lastSegment.includes('.')) {
389
- const fileResponse = await runtime.handle(pathname);
390
- if (fileResponse.status === 200) return fileResponse;
391
- return null;
392
- }
393
-
394
- // Bare paths — redirect to /app/* in root/only modes, /html/* otherwise.
395
- const base = (spa === 'root' || spa === 'only') ? appBase : htmlBase;
396
- const bare = pathname === '/' ? '' : pathname.slice(1).replace(/\/$/, '');
397
- return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
398
- }
399
-
400
- // ── Return ───────────────────────────────────────────────────────────
401
-
402
- return {
403
- handleRequest,
404
- get htmlRouter() {
405
- return ssrHtmlRouter;
406
- },
407
- get mdRouter() {
408
- return ssrMdRouter;
409
- },
410
- get routeTree() {
411
- return routeTree;
412
- },
413
- get widgetEntries() {
414
- return discoveredWidgetEntries;
415
- },
416
- get shell() {
417
- return shell;
418
- },
419
- };
18
+ ): Promise<_Emroute> {
19
+ return _Emroute.create(config, runtime);
420
20
  }