@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
@@ -1,7 +1,7 @@
1
- import type { RouteNode, RouteFiles } from '../src/type/route-tree.type.ts';
2
- import { resolveTargetNode } from '../src/route/route-tree.util.ts';
3
- import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
4
- import type { ElementManifestEntry } from '../src/type/element.type.ts';
1
+ import type { RouteNode, RouteFiles } from '../core/type/route-tree.type.ts';
2
+ import { resolveTargetNode } from '../core/util/route-tree.util.ts';
3
+ import type { WidgetManifestEntry } from '../core/type/widget.type.ts';
4
+ import type { ElementManifestEntry } from '../core/type/element.type.ts';
5
5
 
6
6
  export const CONTENT_TYPES: Map<string, string> = new Map<string, string>([
7
7
  ['.html', 'text/html; charset=utf-8'],
@@ -33,9 +33,12 @@ export type FetchReturn = ReturnType<typeof fetch>;
33
33
  export const DEFAULT_ROUTES_DIR = '/routes';
34
34
  export const DEFAULT_WIDGETS_DIR = '/widgets';
35
35
  export const DEFAULT_ELEMENTS_DIR = '/elements';
36
- export const ROUTES_MANIFEST_PATH = '/routes.manifest.json';
37
- export const WIDGETS_MANIFEST_PATH = '/widgets.manifest.json';
38
- export const ELEMENTS_MANIFEST_PATH = '/elements.manifest.json';
36
+ import {
37
+ ROUTES_MANIFEST_PATH,
38
+ WIDGETS_MANIFEST_PATH,
39
+ ELEMENTS_MANIFEST_PATH,
40
+ } from '../core/runtime/abstract.runtime.ts';
41
+ export { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH, ELEMENTS_MANIFEST_PATH };
39
42
 
40
43
  export interface RuntimeConfig {
41
44
  routesDir?: string;
@@ -76,16 +79,47 @@ export abstract class Runtime {
76
79
  options?: FetchParams[1],
77
80
  ): FetchReturn;
78
81
 
79
- /** Write. Defaults to PUT; pass `{ method: "DELETE" }` etc. to override. */
82
+ /** Write or delete. Defaults to PUT; pass `{ method: "DELETE" }` to remove. */
80
83
  command(resource: FetchParams[0], options?: FetchParams[1]): FetchReturn {
81
84
  const path = typeof resource === 'string'
82
85
  ? resource
83
86
  : new URL(resource instanceof Request ? resource.url : resource.toString()).pathname;
84
- const result = this.handle(resource, { method: 'PUT', ...options });
87
+ const method = options?.method ?? 'PUT';
88
+ const isDelete = method === 'DELETE';
89
+ const result = this.handle(resource, { method, ...options });
85
90
  const routesDir = this.config.routesDir ?? DEFAULT_ROUTES_DIR;
91
+ const widgetsDir = this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR;
92
+ const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
86
93
  if (path.startsWith(routesDir + '/')) {
87
94
  return result.then(async (res) => {
88
- await this.mergeRouteIntoManifest(path, routesDir);
95
+ if (isDelete) {
96
+ await this.pruneRouteFromManifest(path, routesDir);
97
+ } else {
98
+ await this.mergeRouteIntoManifest(path, routesDir);
99
+ await this.retranspileIfNeeded(path, routesDir, 'route');
100
+ }
101
+ return res;
102
+ });
103
+ }
104
+ if (path.startsWith(widgetsDir + '/')) {
105
+ return result.then(async (res) => {
106
+ if (isDelete) {
107
+ await this.pruneWidgetFromManifest(path, widgetsDir);
108
+ } else {
109
+ await this.mergeWidgetIntoManifest(path, widgetsDir);
110
+ await this.retranspileIfNeeded(path, widgetsDir, 'widget');
111
+ }
112
+ return res;
113
+ });
114
+ }
115
+ if (path.startsWith(elementsDir + '/')) {
116
+ return result.then(async (res) => {
117
+ if (isDelete) {
118
+ await this.pruneElementFromManifest(path, elementsDir);
119
+ } else {
120
+ await this.mergeElementIntoManifest(path, elementsDir);
121
+ await this.retranspileIfNeeded(path, elementsDir, 'element');
122
+ }
89
123
  return res;
90
124
  });
91
125
  }
@@ -103,13 +137,15 @@ export abstract class Runtime {
103
137
  ): Promise<void> {
104
138
  const relativePath = filePath.slice(routesDir.length + 1);
105
139
  const parts = relativePath.split('/');
106
- const filename = parts[parts.length - 1];
140
+ const filename = parts[parts.length - 1]!;
107
141
  const dirSegments = parts.slice(0, -1);
108
142
 
109
143
  const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
110
144
  if (!match) return;
111
145
 
112
- const [, name, kind, ext] = match;
146
+ const name = match[1]!;
147
+ const kind = match[2]!;
148
+ const ext = match[3]!;
113
149
 
114
150
  // Read current manifest (or start fresh)
115
151
  const response = await this.handle(ROUTES_MANIFEST_PATH);
@@ -127,7 +163,7 @@ export abstract class Runtime {
127
163
  } else {
128
164
  node.children ??= {};
129
165
  node.children[dir] ??= {};
130
- node = node.children[dir];
166
+ node = node.children[dir]!;
131
167
  }
132
168
  }
133
169
 
@@ -152,6 +188,277 @@ export abstract class Runtime {
152
188
  });
153
189
  }
154
190
 
191
+ /**
192
+ * Remove a route entry from the stored manifest when a file is deleted.
193
+ * Walks the tree to find the node, clears the relevant field, then
194
+ * prunes empty ancestor nodes.
195
+ */
196
+ private async pruneRouteFromManifest(
197
+ filePath: string,
198
+ routesDir: string,
199
+ ): Promise<void> {
200
+ const relativePath = filePath.slice(routesDir.length + 1);
201
+ const parts = relativePath.split('/');
202
+ const filename = parts[parts.length - 1]!;
203
+ const dirSegments = parts.slice(0, -1);
204
+
205
+ const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
206
+ if (!match) return;
207
+
208
+ const name = match[1]!;
209
+ const kind = match[2]!;
210
+ const ext = match[3]! as keyof RouteFiles;
211
+
212
+ const response = await this.handle(ROUTES_MANIFEST_PATH);
213
+ if (response.status === 404) return;
214
+ const tree: RouteNode = await response.json();
215
+
216
+ // Walk to the parent node, tracking path for pruning
217
+ const ancestors: { node: RouteNode; key: string; via: 'children' | 'dynamic' }[] = [];
218
+ let node = tree;
219
+ for (const dir of dirSegments) {
220
+ if (dir.startsWith('[') && dir.endsWith(']')) {
221
+ if (!node.dynamic) return;
222
+ ancestors.push({ node, key: dir, via: 'dynamic' });
223
+ node = node.dynamic.child;
224
+ } else {
225
+ if (!node.children?.[dir]) return;
226
+ ancestors.push({ node, key: dir, via: 'children' });
227
+ node = node.children[dir]!;
228
+ }
229
+ }
230
+
231
+ // Clear the field
232
+ if (kind === 'error') {
233
+ if (node.errorBoundary === filePath) delete node.errorBoundary;
234
+ } else {
235
+ const isRoot = dirSegments.length === 0;
236
+ const target = this.findTargetNode(node, name, isRoot);
237
+ if (!target) return;
238
+
239
+ if (kind === 'redirect') {
240
+ if (target.redirect === filePath) delete target.redirect;
241
+ } else {
242
+ if (target.files?.[ext] === filePath) {
243
+ delete target.files[ext];
244
+ if (Object.keys(target.files).length === 0) delete target.files;
245
+ }
246
+ }
247
+
248
+ // If target is a child node and now empty, remove it
249
+ if (target !== node && this.isEmptyNode(target)) {
250
+ if (name === 'index' && !isRoot) {
251
+ delete node.wildcard;
252
+ } else if (name.startsWith('[') && name.endsWith(']')) {
253
+ delete node.dynamic;
254
+ } else if (node.children) {
255
+ delete node.children[name];
256
+ if (Object.keys(node.children).length === 0) delete node.children;
257
+ }
258
+ }
259
+ }
260
+
261
+ // Prune empty ancestors bottom-up
262
+ for (let i = ancestors.length - 1; i >= 0; i--) {
263
+ const { node: parent, key, via } = ancestors[i]!;
264
+ const child = via === 'dynamic' ? parent.dynamic?.child : parent.children?.[key];
265
+ if (child && this.isEmptyNode(child)) {
266
+ if (via === 'dynamic') {
267
+ delete parent.dynamic;
268
+ } else if (parent.children) {
269
+ delete parent.children[key];
270
+ if (Object.keys(parent.children).length === 0) delete parent.children;
271
+ }
272
+ }
273
+ }
274
+
275
+ this.routesManifestCache = null;
276
+ await this.handle(ROUTES_MANIFEST_PATH, {
277
+ method: 'PUT',
278
+ body: JSON.stringify(tree),
279
+ });
280
+ }
281
+
282
+ /** Find a target node without creating it (read-only counterpart to resolveTargetNode). */
283
+ private findTargetNode(node: RouteNode, name: string, isRoot: boolean): RouteNode | null {
284
+ if (name === 'index') {
285
+ return isRoot ? node : (node.wildcard?.child ?? null);
286
+ }
287
+ if (name.startsWith('[') && name.endsWith(']')) {
288
+ return node.dynamic?.child ?? null;
289
+ }
290
+ return node.children?.[name] ?? null;
291
+ }
292
+
293
+ private isEmptyNode(node: RouteNode): boolean {
294
+ return (
295
+ !node.files &&
296
+ !node.errorBoundary &&
297
+ !node.redirect &&
298
+ !node.children &&
299
+ !node.dynamic &&
300
+ !node.wildcard
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Remove a widget entry from the stored manifest when a file is deleted.
306
+ */
307
+ private async pruneWidgetFromManifest(
308
+ filePath: string,
309
+ widgetsDir: string,
310
+ ): Promise<void> {
311
+ const relativePath = filePath.slice(widgetsDir.length + 1);
312
+ const parts = relativePath.split('/');
313
+ if (parts.length !== 2) return;
314
+
315
+ const [dirName, filename] = parts as [string, string];
316
+ const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
317
+ if (!match) return;
318
+
319
+ const name = match[1]!;
320
+ const ext = match[2]!;
321
+ if (name !== dirName) return;
322
+
323
+ const response = await this.handle(WIDGETS_MANIFEST_PATH);
324
+ if (response.status === 404) return;
325
+ const entries: WidgetManifestEntry[] = await response.json();
326
+
327
+ if (ext === 'ts' || ext === 'js') {
328
+ // Module deleted → remove entire entry
329
+ const idx = entries.findIndex((e) => e.name === name);
330
+ if (idx === -1) return;
331
+ entries.splice(idx, 1);
332
+ } else {
333
+ // Companion deleted → remove from files
334
+ const entry = entries.find((e) => e.name === name);
335
+ if (!entry?.files) return;
336
+ delete (entry.files as Record<string, string>)[ext];
337
+ if (Object.keys(entry.files).length === 0) delete entry.files;
338
+ }
339
+
340
+ this.widgetsManifestCache = null;
341
+ await this.handle(WIDGETS_MANIFEST_PATH, {
342
+ method: 'PUT',
343
+ body: JSON.stringify(entries),
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Remove an element entry from the stored manifest when a file is deleted.
349
+ */
350
+ private async pruneElementFromManifest(
351
+ filePath: string,
352
+ elementsDir: string,
353
+ ): Promise<void> {
354
+ const relativePath = filePath.slice(elementsDir.length + 1);
355
+ const parts = relativePath.split('/');
356
+ if (parts.length !== 2) return;
357
+
358
+ const [dirName, filename] = parts as [string, string];
359
+ const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
360
+ if (!match) return;
361
+
362
+ const name = match[1]!;
363
+ if (name !== dirName) return;
364
+
365
+ const response = await this.handle(ELEMENTS_MANIFEST_PATH);
366
+ if (response.status === 404) return;
367
+ const entries: ElementManifestEntry[] = await response.json();
368
+
369
+ const idx = entries.findIndex((e) => e.name === name);
370
+ if (idx === -1) return;
371
+ entries.splice(idx, 1);
372
+
373
+ this.elementsManifestCache = null;
374
+ await this.handle(ELEMENTS_MANIFEST_PATH, {
375
+ method: 'PUT',
376
+ body: JSON.stringify(entries),
377
+ });
378
+ }
379
+
380
+ /**
381
+ * After a source or companion file is written, check if a built `.js`
382
+ * artifact exists for that module. If so, re-transpile the `.ts` source
383
+ * with companions inlined and write the `.js` back.
384
+ *
385
+ * Best-effort: silently skips if `transpile()` is not implemented.
386
+ */
387
+ private async retranspileIfNeeded(
388
+ filePath: string,
389
+ dir: string,
390
+ kind: 'route' | 'widget' | 'element',
391
+ ): Promise<void> {
392
+ // Only act on source/companion files, not the .js output itself
393
+ if (filePath.endsWith('.js')) return;
394
+
395
+ const relativePath = filePath.slice(dir.length + 1);
396
+ const parts = relativePath.split('/');
397
+ const filename = parts[parts.length - 1]!;
398
+
399
+ // Determine the module base name and the .js output path
400
+ let jsPath: string;
401
+ if (kind === 'route') {
402
+ const match = filename.match(/^(.+?)\.(page)\.(ts|html|md|css)$/);
403
+ if (!match) return;
404
+ const name = match[1]!;
405
+ jsPath = `${dir}/${parts.slice(0, -1).join('/')}${parts.length > 1 ? '/' : ''}${name}.page.js`;
406
+ } else if (kind === 'widget') {
407
+ const match = filename.match(/^(.+?)\.(widget)\.(ts|html|md|css)$/);
408
+ if (!match) return;
409
+ const name = match[1]!;
410
+ jsPath = `${dir}/${name}/${name}.widget.js`;
411
+ } else {
412
+ const match = filename.match(/^(.+?)\.(element)\.ts$/);
413
+ if (!match) return;
414
+ const name = match[1]!;
415
+ jsPath = `${dir}/${name}/${name}.element.js`;
416
+ }
417
+
418
+ // Check if the .js artifact exists
419
+ const jsResponse = await this.handle(jsPath);
420
+ if (jsResponse.status === 404) return;
421
+
422
+ // Read the .ts source
423
+ const tsPath = jsPath.replace(/\.js$/, '.ts');
424
+ let tsSource: string;
425
+ try {
426
+ tsSource = await this.query(tsPath, { as: 'text' });
427
+ } catch {
428
+ return; // .ts doesn't exist (maybe .js was hand-written)
429
+ }
430
+
431
+ // Collect companion files and inline them
432
+ const companionExts = kind === 'element' ? [] : ['html', 'md', 'css'];
433
+ const files: Record<string, string> = {};
434
+ for (const ext of companionExts) {
435
+ const companionPath = tsPath.replace(/\.ts$/, `.${ext}`);
436
+ try {
437
+ files[ext] = await this.query(companionPath, { as: 'text' });
438
+ } catch {
439
+ // companion doesn't exist — skip
440
+ }
441
+ }
442
+
443
+ // Transpile
444
+ let jsCode: string;
445
+ try {
446
+ jsCode = await this.transpile(tsSource);
447
+ } catch {
448
+ return; // transpile not implemented — skip silently
449
+ }
450
+
451
+ // Append __files export if there are companions
452
+ if (Object.keys(files).length > 0) {
453
+ const entries = Object.entries(files)
454
+ .map(([k, v]) => `${k}: \`${v.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$')}\``)
455
+ .join(', ');
456
+ jsCode += `\nexport const __files = { ${entries} };\n`;
457
+ }
458
+
459
+ await this.handle(jsPath, { method: 'PUT', body: jsCode });
460
+ }
461
+
155
462
  /**
156
463
  * Dynamically import a module from this runtime's storage.
157
464
  * Used by the server for SSR imports of `.page.ts` and `.widget.ts` files.
@@ -168,6 +475,113 @@ export abstract class Runtime {
168
475
  throw new Error(`transpile not implemented for ${this.constructor.name}`);
169
476
  }
170
477
 
478
+ /**
479
+ * Parse a widget file path and merge it into the stored manifest.
480
+ * Reads the current manifest, upserts the entry, writes it back.
481
+ */
482
+ private async mergeWidgetIntoManifest(
483
+ filePath: string,
484
+ widgetsDir: string,
485
+ ): Promise<void> {
486
+ const relativePath = filePath.slice(widgetsDir.length + 1);
487
+ const parts = relativePath.split('/');
488
+ if (parts.length !== 2) return; // must be widgets/{name}/{file}
489
+
490
+ const [dirName, filename] = parts as [string, string];
491
+
492
+ // Only act on .widget.{ts,js,html,md,css} files
493
+ const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
494
+ if (!match) return;
495
+
496
+ const name = match[1]!;
497
+ const ext = match[2]!;
498
+ if (name !== dirName) return; // filename must match directory
499
+
500
+ const response = await this.handle(WIDGETS_MANIFEST_PATH);
501
+ const entries: WidgetManifestEntry[] = response.status === 404
502
+ ? []
503
+ : await response.json();
504
+
505
+ const prefix = widgetsDir.replace(/^\//, '');
506
+
507
+ if (ext === 'ts' || ext === 'js') {
508
+ // Module file — upsert the entry
509
+ let entry = entries.find((e) => e.name === name);
510
+ if (!entry) {
511
+ entry = {
512
+ name,
513
+ modulePath: `${prefix}/${name}/${filename}`,
514
+ tagName: `widget-${name}`,
515
+ };
516
+ entries.push(entry);
517
+ entries.sort((a, b) => a.name.localeCompare(b.name));
518
+ } else {
519
+ entry.modulePath = `${prefix}/${name}/${filename}`;
520
+ }
521
+ } else {
522
+ // Companion file — update files on existing entry
523
+ const entry = entries.find((e) => e.name === name);
524
+ if (!entry) return; // no module yet, companion alone is not enough
525
+ entry.files ??= {};
526
+ (entry.files as Record<string, string>)[ext] = `${prefix}/${name}/${filename}`;
527
+ }
528
+
529
+ this.widgetsManifestCache = null;
530
+ await this.handle(WIDGETS_MANIFEST_PATH, {
531
+ method: 'PUT',
532
+ body: JSON.stringify(entries),
533
+ });
534
+ }
535
+
536
+ /**
537
+ * Parse an element file path and merge it into the stored manifest.
538
+ * Reads the current manifest, upserts the entry, writes it back.
539
+ */
540
+ private async mergeElementIntoManifest(
541
+ filePath: string,
542
+ elementsDir: string,
543
+ ): Promise<void> {
544
+ const relativePath = filePath.slice(elementsDir.length + 1);
545
+ const parts = relativePath.split('/');
546
+ if (parts.length !== 2) return;
547
+
548
+ const [dirName, filename] = parts as [string, string];
549
+
550
+ const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
551
+ if (!match) return;
552
+
553
+ const name = match[1]!;
554
+ if (name !== dirName) return;
555
+
556
+ // Custom element names must contain a hyphen
557
+ if (!name.includes('-')) return;
558
+
559
+ const response = await this.handle(ELEMENTS_MANIFEST_PATH);
560
+ const entries: ElementManifestEntry[] = response.status === 404
561
+ ? []
562
+ : await response.json();
563
+
564
+ const prefix = elementsDir.replace(/^\//, '');
565
+ let entry = entries.find((e) => e.name === name);
566
+ if (!entry) {
567
+ entry = {
568
+ name,
569
+ modulePath: `${prefix}/${name}/${filename}`,
570
+ tagName: name,
571
+ };
572
+ entries.push(entry);
573
+ entries.sort((a, b) => a.name.localeCompare(b.name));
574
+ } else {
575
+ entry.modulePath = `${prefix}/${name}/${filename}`;
576
+ }
577
+
578
+ this.elementsManifestCache = null;
579
+ await this.handle(ELEMENTS_MANIFEST_PATH, {
580
+ method: 'PUT',
581
+ body: JSON.stringify(entries),
582
+ });
583
+ }
584
+
171
585
  // ── Manifest resolution ─────────────────────────────────────────────
172
586
 
173
587
  private routesManifestCache: Response | null = null;
@@ -272,14 +686,16 @@ export abstract class Runtime {
272
686
  for (const filePath of allFiles) {
273
687
  const relativePath = filePath.replace(`${routesDir}/`, '');
274
688
  const parts = relativePath.split('/');
275
- const filename = parts[parts.length - 1];
689
+ const filename = parts[parts.length - 1]!;
276
690
  const dirSegments = parts.slice(0, -1);
277
691
 
278
692
  // Parse filename: name.kind.ext (e.g. "about.page.ts", "[id].page.html", "index.error.ts")
279
693
  const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
280
694
  if (!match) continue;
281
695
 
282
- const [, name, kind, ext] = match;
696
+ const name = match[1]!;
697
+ const kind = match[2]!;
698
+ const ext = match[3]! as keyof RouteFiles;
283
699
 
284
700
  // Walk directory segments to reach the parent node
285
701
  let node = root;
@@ -291,7 +707,7 @@ export abstract class Runtime {
291
707
  } else {
292
708
  node.children ??= {};
293
709
  node.children[dir] ??= {};
294
- node = node.children[dir];
710
+ node = node.children[dir]!;
295
711
  }
296
712
  }
297
713
 
@@ -305,7 +721,7 @@ export abstract class Runtime {
305
721
  }
306
722
 
307
723
  // For page and redirect files, the name determines the final node
308
- const target = resolveTargetNode(node, name, dirSegments.length === 0);
724
+ const target = resolveTargetNode(node, name!, dirSegments.length === 0);
309
725
 
310
726
  if (kind === 'redirect') {
311
727
  target.redirect = filePath;
@@ -1,4 +1,4 @@
1
- import { stat, readdir, mkdir } from 'node:fs/promises';
1
+ import { stat, readdir, mkdir, unlink } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import {
4
4
  CONTENT_TYPES,
@@ -30,6 +30,8 @@ export class BunFsRuntime extends Runtime {
30
30
  switch (method) {
31
31
  case 'PUT':
32
32
  return this.write(path, body);
33
+ case 'DELETE':
34
+ return this.delete(path);
33
35
  default:
34
36
  return this.read(path);
35
37
  }
@@ -130,6 +132,18 @@ export class BunFsRuntime extends Runtime {
130
132
  }
131
133
  }
132
134
 
135
+ private async delete(path: string): Promise<Response> {
136
+ try {
137
+ await unlink(path);
138
+ return new Response(null, { status: 204 });
139
+ } catch (error) {
140
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
141
+ return new Response('Not Found', { status: 404 });
142
+ }
143
+ return new Response(`Delete failed: ${error}`, { status: 500 });
144
+ }
145
+ }
146
+
133
147
  override loadModule(path: string): Promise<unknown> {
134
148
  return import(this.root + path);
135
149
  }
@@ -14,6 +14,7 @@ export class BunSqliteRuntime extends Runtime {
14
14
  private readonly db: Database;
15
15
  private readonly stmtGet: ReturnType<Database['prepare']>;
16
16
  private readonly stmtSet: ReturnType<Database['prepare']>;
17
+ private readonly stmtDel: ReturnType<Database['prepare']>;
17
18
  private readonly stmtList: ReturnType<Database['prepare']>;
18
19
  private readonly stmtHas: ReturnType<Database['prepare']>;
19
20
 
@@ -29,6 +30,7 @@ export class BunSqliteRuntime extends Runtime {
29
30
  `);
30
31
  this.stmtGet = this.db.prepare('SELECT data, mtime FROM files WHERE path = ?');
31
32
  this.stmtSet = this.db.prepare('INSERT OR REPLACE INTO files (path, data, mtime) VALUES (?, ?, ?)');
33
+ this.stmtDel = this.db.prepare('DELETE FROM files WHERE path = ?');
32
34
  this.stmtList = this.db.prepare("SELECT DISTINCT path FROM files WHERE path LIKE ? || '%'");
33
35
  this.stmtHas = this.db.prepare("SELECT 1 FROM files WHERE path LIKE ? || '%' LIMIT 1");
34
36
  }
@@ -42,6 +44,8 @@ export class BunSqliteRuntime extends Runtime {
42
44
  switch (method) {
43
45
  case 'PUT':
44
46
  return this.write(pathname, body);
47
+ case 'DELETE':
48
+ return this.delete(pathname);
45
49
  default:
46
50
  return this.read(pathname);
47
51
  }
@@ -132,6 +136,11 @@ export class BunSqliteRuntime extends Runtime {
132
136
  return new Response(null, { status: 204 });
133
137
  }
134
138
 
139
+ private async delete(path: string): Promise<Response> {
140
+ this.stmtDel.run(path);
141
+ return new Response(null, { status: 204 });
142
+ }
143
+
135
144
  private listChildren(prefix: string): string[] {
136
145
  const rows = this.stmtList.all(prefix) as { path: string }[];
137
146
  const entries = new Set<string>();
@@ -2,9 +2,9 @@
2
2
  * Fetch Runtime
3
3
  *
4
4
  * Browser-compatible Runtime that delegates all reads to a remote server
5
- * via `fetch()`. Used by the thin client in `root` mode — same
6
- * `createEmrouteServer` runs in the browser, but the Runtime fetches
7
- * files from the real server instead of reading from disk.
5
+ * via `fetch()`. Used by EmrouteApp in `root` mode — same Emroute
6
+ * instance runs in the browser, but the Runtime fetches files from the
7
+ * real server instead of reading from disk.
8
8
  *
9
9
  * No bundling, no transpiling, no filesystem access.
10
10
  * No directory scanning — the remote server already has manifests.
@@ -17,8 +17,8 @@
17
17
  * @see https://www.sitemaps.org/protocol.html
18
18
  */
19
19
 
20
- import { escapeHtml } from '../src/util/html.util.ts';
21
- import type { RouteNode } from '../src/type/route-tree.type.ts';
20
+ import { escapeHtml } from '../core/util/html.util.ts';
21
+ import type { RouteNode } from '../core/type/route-tree.type.ts';
22
22
 
23
23
  /** Valid changefreq values per sitemaps.org protocol. */
24
24
  export type Changefreq =
@@ -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,
@@ -38,6 +38,8 @@ export class UniversalFsRuntime extends Runtime {
38
38
  switch (method) {
39
39
  case 'PUT':
40
40
  return this.write(path, body);
41
+ case 'DELETE':
42
+ return this.delete(path);
41
43
  default:
42
44
  return this.read(path);
43
45
  }
@@ -138,6 +140,18 @@ export class UniversalFsRuntime extends Runtime {
138
140
  }
139
141
  }
140
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
+
141
155
  override loadModule(path: string): Promise<unknown> {
142
156
  return import(this.root + path);
143
157
  }