@emkodev/emroute 1.6.1 → 1.6.3

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 (129) hide show
  1. package/README.md +19 -17
  2. package/dist/runtime/abstract.runtime.d.ts +94 -0
  3. package/dist/runtime/abstract.runtime.js +339 -0
  4. package/dist/runtime/abstract.runtime.js.map +1 -0
  5. package/dist/runtime/bun/esbuild-runtime-loader.plugin.d.ts +25 -0
  6. package/dist/runtime/bun/esbuild-runtime-loader.plugin.js +72 -0
  7. package/dist/runtime/bun/esbuild-runtime-loader.plugin.js.map +1 -0
  8. package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +21 -0
  9. package/dist/runtime/bun/fs/bun-fs.runtime.js +205 -0
  10. package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -0
  11. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +27 -0
  12. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +234 -0
  13. package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -0
  14. package/dist/runtime/sitemap.generator.d.ts +58 -0
  15. package/dist/runtime/sitemap.generator.js +107 -0
  16. package/dist/runtime/sitemap.generator.js.map +1 -0
  17. package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +29 -0
  18. package/dist/runtime/universal/fs/universal-fs.runtime.js +213 -0
  19. package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -0
  20. package/dist/server/codegen.util.d.ts +16 -0
  21. package/dist/server/codegen.util.js +46 -0
  22. package/dist/server/codegen.util.js.map +1 -0
  23. package/dist/server/emroute.server.d.ts +37 -0
  24. package/dist/server/emroute.server.js +314 -0
  25. package/dist/server/emroute.server.js.map +1 -0
  26. package/dist/server/esbuild-manifest.plugin.d.ts +26 -0
  27. package/dist/server/esbuild-manifest.plugin.js +187 -0
  28. package/dist/server/esbuild-manifest.plugin.js.map +1 -0
  29. package/dist/server/scanner.util.d.ts +22 -0
  30. package/dist/server/scanner.util.js +194 -0
  31. package/dist/server/scanner.util.js.map +1 -0
  32. package/dist/server/server-api.type.d.ts +71 -0
  33. package/dist/server/server-api.type.js +9 -0
  34. package/dist/server/server-api.type.js.map +1 -0
  35. package/dist/src/component/abstract.component.d.ts +197 -0
  36. package/dist/src/component/abstract.component.js +84 -0
  37. package/dist/src/component/abstract.component.js.map +1 -0
  38. package/dist/src/component/page.component.d.ts +74 -0
  39. package/dist/src/component/page.component.js +107 -0
  40. package/dist/src/component/page.component.js.map +1 -0
  41. package/dist/src/component/widget.component.d.ts +47 -0
  42. package/dist/src/component/widget.component.js +69 -0
  43. package/dist/src/component/widget.component.js.map +1 -0
  44. package/dist/src/element/component.element.d.ts +79 -0
  45. package/dist/src/element/component.element.js +293 -0
  46. package/dist/src/element/component.element.js.map +1 -0
  47. package/dist/src/element/markdown.element.d.ts +36 -0
  48. package/dist/src/element/markdown.element.js +93 -0
  49. package/dist/src/element/markdown.element.js.map +1 -0
  50. package/dist/src/element/slot.element.d.ts +30 -0
  51. package/dist/src/element/slot.element.js +31 -0
  52. package/dist/src/element/slot.element.js.map +1 -0
  53. package/dist/src/index.d.ts +23 -0
  54. package/dist/src/index.js +24 -0
  55. package/dist/src/index.js.map +1 -0
  56. package/dist/src/overlay/mod.d.ts +9 -0
  57. package/dist/src/overlay/mod.js +9 -0
  58. package/dist/src/overlay/mod.js.map +1 -0
  59. package/dist/src/overlay/overlay.css.d.ts +8 -0
  60. package/dist/src/overlay/overlay.css.js +170 -0
  61. package/dist/src/overlay/overlay.css.js.map +1 -0
  62. package/dist/src/overlay/overlay.service.d.ts +14 -0
  63. package/dist/src/overlay/overlay.service.js +307 -0
  64. package/dist/src/overlay/overlay.service.js.map +1 -0
  65. package/dist/src/overlay/overlay.type.d.ts +33 -0
  66. package/dist/src/overlay/overlay.type.js +11 -0
  67. package/dist/src/overlay/overlay.type.js.map +1 -0
  68. package/dist/src/renderer/spa/base.renderer.d.ts +39 -0
  69. package/dist/src/renderer/spa/base.renderer.js +149 -0
  70. package/dist/src/renderer/spa/base.renderer.js.map +1 -0
  71. package/dist/src/renderer/spa/hash.renderer.d.ts +78 -0
  72. package/dist/src/renderer/spa/hash.renderer.js +162 -0
  73. package/dist/src/renderer/spa/hash.renderer.js.map +1 -0
  74. package/dist/src/renderer/spa/html.renderer.d.ts +81 -0
  75. package/dist/src/renderer/spa/html.renderer.js +304 -0
  76. package/dist/src/renderer/spa/html.renderer.js.map +1 -0
  77. package/dist/src/renderer/spa/mod.d.ts +30 -0
  78. package/dist/src/renderer/spa/mod.js +35 -0
  79. package/dist/src/renderer/spa/mod.js.map +1 -0
  80. package/dist/src/renderer/ssr/html.renderer.d.ts +49 -0
  81. package/dist/src/renderer/ssr/html.renderer.js +108 -0
  82. package/dist/src/renderer/ssr/html.renderer.js.map +1 -0
  83. package/dist/src/renderer/ssr/md.renderer.d.ts +40 -0
  84. package/dist/src/renderer/ssr/md.renderer.js +100 -0
  85. package/dist/src/renderer/ssr/md.renderer.js.map +1 -0
  86. package/dist/src/renderer/ssr/ssr.renderer.d.ts +74 -0
  87. package/dist/src/renderer/ssr/ssr.renderer.js +185 -0
  88. package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -0
  89. package/dist/src/route/route.core.d.ts +129 -0
  90. package/dist/src/route/route.core.js +255 -0
  91. package/dist/src/route/route.core.js.map +1 -0
  92. package/dist/src/route/route.matcher.d.ts +86 -0
  93. package/dist/src/route/route.matcher.js +214 -0
  94. package/dist/src/route/route.matcher.js.map +1 -0
  95. package/dist/src/type/logger.type.d.ts +17 -0
  96. package/dist/src/type/logger.type.js +9 -0
  97. package/dist/src/type/logger.type.js.map +1 -0
  98. package/dist/src/type/markdown.type.d.ts +20 -0
  99. package/dist/src/type/markdown.type.js +2 -0
  100. package/dist/src/type/markdown.type.js.map +1 -0
  101. package/dist/src/type/route.type.d.ts +112 -0
  102. package/dist/src/type/route.type.js +8 -0
  103. package/dist/src/type/route.type.js.map +1 -0
  104. package/dist/src/type/widget.type.d.ts +55 -0
  105. package/dist/src/type/widget.type.js +10 -0
  106. package/dist/src/type/widget.type.js.map +1 -0
  107. package/dist/src/util/html.util.d.ts +29 -0
  108. package/dist/src/util/html.util.js +158 -0
  109. package/dist/src/util/html.util.js.map +1 -0
  110. package/dist/src/util/logger.util.d.ts +26 -0
  111. package/dist/src/util/logger.util.js +80 -0
  112. package/dist/src/util/logger.util.js.map +1 -0
  113. package/dist/src/util/widget-resolve.util.d.ts +52 -0
  114. package/dist/src/util/widget-resolve.util.js +149 -0
  115. package/dist/src/util/widget-resolve.util.js.map +1 -0
  116. package/dist/src/widget/breadcrumb.widget.d.ts +48 -0
  117. package/dist/src/widget/breadcrumb.widget.js +72 -0
  118. package/dist/src/widget/breadcrumb.widget.js.map +1 -0
  119. package/dist/src/widget/page-title.widget.d.ts +33 -0
  120. package/dist/src/widget/page-title.widget.js +33 -0
  121. package/dist/src/widget/page-title.widget.js.map +1 -0
  122. package/dist/src/widget/widget.parser.d.ts +26 -0
  123. package/dist/src/widget/widget.parser.js +76 -0
  124. package/dist/src/widget/widget.parser.js.map +1 -0
  125. package/dist/src/widget/widget.registry.d.ts +23 -0
  126. package/dist/src/widget/widget.registry.js +42 -0
  127. package/dist/src/widget/widget.registry.js.map +1 -0
  128. package/package.json +72 -12
  129. package/runtime/universal/fs/universal-fs.runtime.ts +253 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Widget Registry
3
+ *
4
+ * Canonical registry where all widgets live. Used by all renderers:
5
+ * - SSR HTML: resolves widgets by name, calls getData() + renderHTML()
6
+ * - SSR Markdown: resolves widgets by name, calls getData() + renderMarkdown()
7
+ * - SPA: registers custom elements for each widget
8
+ *
9
+ * Pages are NOT in this registry — they live in the routes manifest.
10
+ */
11
+ export class WidgetRegistry {
12
+ widgets = new Map();
13
+ /** Register a widget by its name. */
14
+ add(widget) {
15
+ this.widgets.set(widget.name, widget);
16
+ }
17
+ /** Look up a widget by name. */
18
+ get(name) {
19
+ return this.widgets.get(name);
20
+ }
21
+ /** Iterate all registered widgets. */
22
+ [Symbol.iterator]() {
23
+ return this.widgets.values();
24
+ }
25
+ /** Emit a WidgetsManifest from registered widgets. */
26
+ toManifest() {
27
+ const widgets = [];
28
+ const moduleLoaders = {};
29
+ for (const [name, widget] of this.widgets) {
30
+ const entry = {
31
+ name,
32
+ modulePath: name,
33
+ tagName: `widget-${name}`,
34
+ files: widget.files,
35
+ };
36
+ widgets.push(entry);
37
+ moduleLoaders[name] = () => Promise.resolve({ default: widget.constructor });
38
+ }
39
+ return { widgets, moduleLoaders };
40
+ }
41
+ }
42
+ //# sourceMappingURL=widget.registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"widget.registry.js","sourceRoot":"","sources":["../../../src/widget/widget.registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,OAAO,cAAc;IACjB,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IAErD,qCAAqC;IACrC,GAAG,CAAC,MAAuB;QACzB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,gCAAgC;IAChC,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,sCAAsC;IACtC,CAAC,MAAM,CAAC,QAAQ,CAAC;QACf,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;IAC/B,CAAC;IAED,sDAAsD;IACtD,UAAU;QACR,MAAM,OAAO,GAA0B,EAAE,CAAC;QAC1C,MAAM,aAAa,GAA2C,EAAE,CAAC;QAEjE,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC1C,MAAM,KAAK,GAAwB;gBACjC,IAAI;gBACJ,UAAU,EAAE,IAAI;gBAChB,OAAO,EAAE,UAAU,IAAI,EAAE;gBACzB,KAAK,EAAE,MAAM,CAAC,KAAK;aACpB,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpB,aAAa,CAAC,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;QAC/E,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;IACpC,CAAC;CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@emkodev/emroute",
3
- "version": "1.6.1",
4
- "description": "File-based router with triple rendering (SPA, SSR HTML, SSR Markdown). Zero dependencies.",
3
+ "version": "1.6.3",
4
+ "description": "File-based (but storage-agnostic) router with triple rendering (SPA, SSR HTML, SSR Markdown). Zero dependencies.",
5
5
  "license": "BSD-3-Clause",
6
6
  "author": "emko.dev",
7
7
  "type": "module",
@@ -13,6 +13,8 @@
13
13
  "file-based",
14
14
  "framework",
15
15
  "bun",
16
+ "deno",
17
+ "node",
16
18
  "typescript",
17
19
  "markdown"
18
20
  ],
@@ -24,20 +26,75 @@
24
26
  "src/",
25
27
  "server/*.ts",
26
28
  "runtime/",
29
+ "dist/",
27
30
  "LICENSE",
28
31
  "README.md"
29
32
  ],
30
33
  "exports": {
31
- ".": "./src/index.ts",
32
- "./spa": "./src/renderer/spa/mod.ts",
33
- "./overlay": "./src/overlay/mod.ts",
34
- "./ssr/html": "./src/renderer/ssr/html.renderer.ts",
35
- "./ssr/md": "./src/renderer/ssr/md.renderer.ts",
36
- "./server": "./server/emroute.server.ts",
37
- "./runtime": "./runtime/abstract.runtime.ts",
38
- "./runtime/sitemap": "./runtime/sitemap.generator.ts",
39
- "./runtime/bun/fs": "./runtime/bun/fs/bun-fs.runtime.ts",
40
- "./runtime/bun/sqlite": "./runtime/bun/sqlite/bun-sqlite.runtime.ts"
34
+ ".": {
35
+ "bun": "./src/index.ts",
36
+ "deno": "./src/index.ts",
37
+ "types": "./dist/src/index.d.ts",
38
+ "default": "./dist/src/index.js"
39
+ },
40
+ "./spa": {
41
+ "bun": "./src/renderer/spa/mod.ts",
42
+ "deno": "./src/renderer/spa/mod.ts",
43
+ "types": "./dist/src/renderer/spa/mod.d.ts",
44
+ "default": "./dist/src/renderer/spa/mod.js"
45
+ },
46
+ "./overlay": {
47
+ "bun": "./src/overlay/mod.ts",
48
+ "deno": "./src/overlay/mod.ts",
49
+ "types": "./dist/src/overlay/mod.d.ts",
50
+ "default": "./dist/src/overlay/mod.js"
51
+ },
52
+ "./ssr/html": {
53
+ "bun": "./src/renderer/ssr/html.renderer.ts",
54
+ "deno": "./src/renderer/ssr/html.renderer.ts",
55
+ "types": "./dist/src/renderer/ssr/html.renderer.d.ts",
56
+ "default": "./dist/src/renderer/ssr/html.renderer.js"
57
+ },
58
+ "./ssr/md": {
59
+ "bun": "./src/renderer/ssr/md.renderer.ts",
60
+ "deno": "./src/renderer/ssr/md.renderer.ts",
61
+ "types": "./dist/src/renderer/ssr/md.renderer.d.ts",
62
+ "default": "./dist/src/renderer/ssr/md.renderer.js"
63
+ },
64
+ "./server": {
65
+ "bun": "./server/emroute.server.ts",
66
+ "deno": "./server/emroute.server.ts",
67
+ "types": "./dist/server/emroute.server.d.ts",
68
+ "default": "./dist/server/emroute.server.js"
69
+ },
70
+ "./runtime": {
71
+ "bun": "./runtime/abstract.runtime.ts",
72
+ "deno": "./runtime/abstract.runtime.ts",
73
+ "types": "./dist/runtime/abstract.runtime.d.ts",
74
+ "default": "./dist/runtime/abstract.runtime.js"
75
+ },
76
+ "./runtime/sitemap": {
77
+ "bun": "./runtime/sitemap.generator.ts",
78
+ "deno": "./runtime/sitemap.generator.ts",
79
+ "types": "./dist/runtime/sitemap.generator.d.ts",
80
+ "default": "./dist/runtime/sitemap.generator.js"
81
+ },
82
+ "./runtime/bun/fs": {
83
+ "bun": "./runtime/bun/fs/bun-fs.runtime.ts",
84
+ "types": "./dist/runtime/bun/fs/bun-fs.runtime.d.ts",
85
+ "default": "./dist/runtime/bun/fs/bun-fs.runtime.js"
86
+ },
87
+ "./runtime/bun/sqlite": {
88
+ "bun": "./runtime/bun/sqlite/bun-sqlite.runtime.ts",
89
+ "types": "./dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts",
90
+ "default": "./dist/runtime/bun/sqlite/bun-sqlite.runtime.js"
91
+ },
92
+ "./runtime/universal/fs": {
93
+ "bun": "./runtime/universal/fs/universal-fs.runtime.ts",
94
+ "deno": "./runtime/universal/fs/universal-fs.runtime.ts",
95
+ "types": "./dist/runtime/universal/fs/universal-fs.runtime.d.ts",
96
+ "default": "./dist/runtime/universal/fs/universal-fs.runtime.js"
97
+ }
41
98
  },
42
99
  "devDependencies": {
43
100
  "@eslint/js": "^10.0.1",
@@ -45,9 +102,12 @@
45
102
  "esbuild": "^0.27.3",
46
103
  "eslint": "^10.0.2",
47
104
  "playwright": "^1.58.2",
105
+ "typescript": "^5.9.3",
48
106
  "typescript-eslint": "^8.56.1"
49
107
  },
50
108
  "scripts": {
109
+ "build": "tsc -p tsconfig.build.json",
110
+ "prepublishOnly": "bun run build",
51
111
  "check": "bun x tsc --noEmit",
52
112
  "test": "bun test test/unit",
53
113
  "test:unit": "bun test test/unit",
@@ -0,0 +1,253 @@
1
+ import { readFile, writeFile, stat, readdir, mkdir } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { resolve } from 'node:path';
4
+ import {
5
+ CONTENT_TYPES,
6
+ DEFAULT_ROUTES_DIR,
7
+ DEFAULT_WIDGETS_DIR,
8
+ EMROUTE_EXTERNALS,
9
+ type FetchParams,
10
+ type FetchReturn,
11
+ ROUTES_MANIFEST_PATH,
12
+ Runtime,
13
+ type RuntimeConfig,
14
+ WIDGETS_MANIFEST_PATH,
15
+ } from '../../abstract.runtime.ts';
16
+ import { createManifestPlugin } from '../../../server/esbuild-manifest.plugin.ts';
17
+ import { createRuntimeLoaderPlugin } from '../../bun/esbuild-runtime-loader.plugin.ts';
18
+ import { generateMainTs } from '../../../server/codegen.util.ts';
19
+
20
+ /**
21
+ * Filesystem runtime using only `node:` APIs and esbuild. Works on Node,
22
+ * Deno, and Bun without platform-specific dependencies.
23
+ *
24
+ * Not recommended for production — `node:fs/promises` has higher overhead
25
+ * than platform-native alternatives (e.g. `Bun.file()`). Use
26
+ * {@link BunFsRuntime} on Bun for better I/O performance.
27
+ */
28
+ export class UniversalFsRuntime extends Runtime {
29
+ private readonly root: string;
30
+
31
+ constructor(root: string, config: RuntimeConfig = {}) {
32
+ if (config.entryPoint && !config.bundlePaths) {
33
+ config.bundlePaths = { emroute: '/emroute.js', app: '/app.js' };
34
+ }
35
+ super(config);
36
+ const abs = resolve(root);
37
+ this.root = abs.endsWith('/') ? abs.slice(0, -1) : abs;
38
+ }
39
+
40
+ handle(
41
+ resource: FetchParams[0],
42
+ init?: FetchParams[1],
43
+ ): FetchReturn {
44
+ const [pathname, method, body] = this.parse(resource, init);
45
+ const path = `${this.root}${pathname}`;
46
+
47
+ switch (method) {
48
+ case 'PUT':
49
+ return this.write(path, body);
50
+ default:
51
+ return this.read(path);
52
+ }
53
+ }
54
+
55
+ query(
56
+ resource: FetchParams[0],
57
+ options: FetchParams[1] & { as: 'text' },
58
+ ): Promise<string>;
59
+ query(
60
+ resource: FetchParams[0],
61
+ options?: FetchParams[1],
62
+ ): FetchReturn;
63
+ query(
64
+ resource: FetchParams[0],
65
+ options?: FetchParams[1] & { as?: 'text' },
66
+ ): Promise<Response | string> {
67
+ if (options?.as === 'text') {
68
+ const pathname = this.parsePath(resource);
69
+ return readFile(`${this.root}${pathname}`, 'utf-8');
70
+ }
71
+ return this.handle(resource, options);
72
+ }
73
+
74
+ private parsePath(resource: FetchParams[0]): string {
75
+ if (typeof resource === 'string') return decodeURIComponent(resource);
76
+ if (resource instanceof URL) return decodeURIComponent(resource.pathname);
77
+ return decodeURIComponent(new URL(resource.url).pathname);
78
+ }
79
+
80
+ private parse(
81
+ resource: FetchParams[0],
82
+ init?: RequestInit,
83
+ ): [string, string, BodyInit | null] {
84
+ const pathname = this.parsePath(resource);
85
+ if (typeof resource === 'string' || resource instanceof URL) {
86
+ return [pathname, init?.method ?? 'GET', init?.body ?? null];
87
+ }
88
+ return [
89
+ pathname,
90
+ init?.method ?? resource.method,
91
+ init?.body ?? resource.body,
92
+ ];
93
+ }
94
+
95
+ private async read(path: string): Promise<Response> {
96
+ try {
97
+ const info = await stat(path);
98
+
99
+ if (info.isDirectory()) {
100
+ return this.list(path);
101
+ }
102
+
103
+ const content = new Uint8Array(await readFile(path));
104
+ const ext = path.slice(path.lastIndexOf('.')).toLowerCase();
105
+ const headers: HeadersInit = {
106
+ 'Content-Type': CONTENT_TYPES.get(ext) ?? 'application/octet-stream',
107
+ 'Content-Length': content.byteLength.toString(),
108
+ };
109
+
110
+ if (info.mtime) {
111
+ headers['Last-Modified'] = info.mtime.toUTCString();
112
+ }
113
+
114
+ return new Response(content, { status: 200, headers });
115
+ } catch (error) {
116
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
117
+ const pathname = path.slice(this.root.length);
118
+ if (pathname === ROUTES_MANIFEST_PATH) return this.resolveRoutesManifest();
119
+ if (pathname === WIDGETS_MANIFEST_PATH) return this.resolveWidgetsManifest();
120
+ return new Response('Not Found', { status: 404 });
121
+ }
122
+ return new Response(`Internal Error: ${error}`, { status: 500 });
123
+ }
124
+ }
125
+
126
+ private async list(path: string): Promise<Response> {
127
+ const entries: string[] = [];
128
+ const dirents = await readdir(path, { withFileTypes: true });
129
+ for (const entry of dirents) {
130
+ entries.push(entry.name + (entry.isDirectory() ? '/' : ''));
131
+ }
132
+ return Response.json(entries);
133
+ }
134
+
135
+ private async write(path: string, body: BodyInit | null): Promise<Response> {
136
+ try {
137
+ const content = body
138
+ ? new Uint8Array(await new Response(body).arrayBuffer())
139
+ : new Uint8Array();
140
+ const dir = path.slice(0, path.lastIndexOf('/'));
141
+ if (dir) await mkdir(dir, { recursive: true });
142
+ await writeFile(path, content);
143
+ return new Response(null, { status: 204 });
144
+ } catch (error) {
145
+ return new Response(`Write failed: ${error}`, { status: 500 });
146
+ }
147
+ }
148
+
149
+ override loadModule(path: string): Promise<unknown> {
150
+ return import(this.root + path);
151
+ }
152
+
153
+ // ── Bundling ─────────────────────────────────────────────────────────
154
+
155
+ override async bundle(): Promise<void> {
156
+ if (this.config.spa === 'none') return;
157
+ const paths = this.config.bundlePaths;
158
+ if (!paths) return;
159
+
160
+ const esbuild = await UniversalFsRuntime.esbuild();
161
+ const builds: Promise<{ outputFiles: { path: string; contents: Uint8Array }[] }>[] = [];
162
+ const shared = { bundle: true, write: false, format: 'esm' as const, platform: 'browser' as const };
163
+ const runtimeLoader = createRuntimeLoaderPlugin({ runtime: this, root: this.root });
164
+
165
+ // Emroute SPA bundle — resolve from consumer's node_modules (no runtime loader needed)
166
+ const consumerRequire = createRequire(this.root + '/');
167
+ const spaEntry = consumerRequire.resolve('@emkodev/emroute/spa');
168
+ builds.push(esbuild.build({
169
+ ...shared,
170
+ entryPoints: [spaEntry],
171
+ outfile: `${this.root}${paths.emroute}`,
172
+ }));
173
+
174
+ // App bundle — generate main.ts if absent, virtual plugin resolves manifests
175
+ if (this.config.entryPoint) {
176
+ if ((await this.query(this.config.entryPoint)).status === 404) {
177
+ const hasRoutes = (await this.query((this.config.routesDir ?? DEFAULT_ROUTES_DIR) + '/')).status !== 404;
178
+ const hasWidgets = (await this.query((this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR) + '/')).status !== 404;
179
+ const code = generateMainTs('root', hasRoutes, hasWidgets, '@emkodev/emroute');
180
+ await this.command(this.config.entryPoint, { body: code });
181
+ }
182
+ const manifestPlugin = createManifestPlugin({
183
+ runtime: this,
184
+ basePath: '/html',
185
+ resolveDir: this.root,
186
+ });
187
+ builds.push(esbuild.build({
188
+ ...shared,
189
+ entryPoints: [`${this.root}${this.config.entryPoint}`],
190
+ outfile: `${this.root}${paths.app}`,
191
+ external: [...EMROUTE_EXTERNALS],
192
+ plugins: [manifestPlugin, runtimeLoader],
193
+ }));
194
+ }
195
+
196
+ // Widgets bundle
197
+ if (paths.widgets) {
198
+ const widgetsTsPath = paths.widgets.replace('.js', '.ts');
199
+ if ((await this.query(widgetsTsPath)).status !== 404) {
200
+ builds.push(esbuild.build({
201
+ ...shared,
202
+ entryPoints: [`${this.root}${widgetsTsPath}`],
203
+ outfile: `${this.root}${paths.widgets}`,
204
+ external: [...EMROUTE_EXTERNALS],
205
+ plugins: [runtimeLoader],
206
+ }));
207
+ }
208
+ }
209
+
210
+ const results = await Promise.all(builds);
211
+
212
+ // Write all output files through the runtime
213
+ for (const result of results) {
214
+ for (const file of result.outputFiles) {
215
+ const runtimePath = file.path.startsWith(this.root)
216
+ ? file.path.slice(this.root.length)
217
+ : '/' + file.path;
218
+ await this.command(runtimePath, { body: file.contents as unknown as BodyInit });
219
+ }
220
+ }
221
+
222
+ await this.writeShell(paths);
223
+
224
+ await esbuild.stop();
225
+ UniversalFsRuntime._esbuild = null;
226
+ }
227
+
228
+ // ── Transpile / esbuild ───────────────────────────────────────────────
229
+
230
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
231
+ private static _esbuild: any = null;
232
+
233
+ private static async esbuild() {
234
+ if (!UniversalFsRuntime._esbuild) {
235
+ const consumerRequire = createRequire(process.cwd() + '/');
236
+ UniversalFsRuntime._esbuild = consumerRequire('esbuild');
237
+ }
238
+ return UniversalFsRuntime._esbuild;
239
+ }
240
+
241
+ static override async transpile(source: string): Promise<string> {
242
+ const esbuild = await UniversalFsRuntime.esbuild();
243
+ const result = await esbuild.transform(source, { loader: 'ts' });
244
+ return result.code;
245
+ }
246
+
247
+ static override async stopBundler(): Promise<void> {
248
+ if (UniversalFsRuntime._esbuild) {
249
+ await UniversalFsRuntime._esbuild.stop();
250
+ UniversalFsRuntime._esbuild = null;
251
+ }
252
+ }
253
+ }