@emkodev/emroute 1.7.2 → 1.7.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.
- package/dist/emroute.js +135 -31
- package/dist/emroute.js.map +2 -2
- package/dist/runtime/abstract.runtime.d.ts +11 -0
- package/dist/runtime/abstract.runtime.js +53 -0
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.js +3 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +3 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.js +3 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
- package/dist/server/build.util.js +35 -6
- package/dist/server/build.util.js.map +1 -1
- package/dist/server/emroute.server.js +17 -7
- package/dist/server/emroute.server.js.map +1 -1
- package/dist/server/server-api.type.d.ts +3 -0
- package/dist/src/component/abstract.component.d.ts +1 -1
- package/dist/src/component/abstract.component.js.map +1 -1
- package/dist/src/element/component.element.js +17 -3
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/element/markdown.element.js +1 -1
- package/dist/src/element/markdown.element.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/renderer/spa/thin-client.js +28 -6
- package/dist/src/renderer/spa/thin-client.js.map +1 -1
- package/dist/src/renderer/ssr/html.renderer.js +1 -1
- package/dist/src/renderer/ssr/html.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/md.renderer.js +2 -2
- package/dist/src/renderer/ssr/md.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/ssr.renderer.js +6 -6
- package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -1
- package/dist/src/route/route.core.js +21 -7
- package/dist/src/route/route.core.js.map +1 -1
- package/dist/src/type/element.type.d.ts +19 -0
- package/dist/src/type/element.type.js +9 -0
- package/dist/src/type/element.type.js.map +1 -0
- package/dist/src/util/widget-resolve.util.js +1 -1
- package/dist/src/util/widget-resolve.util.js.map +1 -1
- package/dist/src/widget/widget.registry.js +1 -1
- package/dist/src/widget/widget.registry.js.map +1 -1
- package/package.json +1 -1
- package/runtime/abstract.runtime.ts +67 -0
- package/runtime/bun/fs/bun-fs.runtime.ts +2 -0
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +2 -0
- package/runtime/universal/fs/universal-fs.runtime.ts +2 -0
- package/server/build.util.ts +37 -5
- package/server/emroute.server.ts +20 -6
- package/server/server-api.type.ts +4 -0
- package/src/component/abstract.component.ts +1 -1
- package/src/element/component.element.ts +16 -4
- package/src/element/markdown.element.ts +1 -1
- package/src/index.ts +1 -0
- package/src/renderer/spa/thin-client.ts +30 -5
- package/src/renderer/ssr/html.renderer.ts +1 -1
- package/src/renderer/ssr/md.renderer.ts +5 -4
- package/src/renderer/ssr/ssr.renderer.ts +6 -6
- package/src/route/route.core.ts +17 -8
- package/src/type/element.type.ts +22 -0
- package/src/util/widget-resolve.util.ts +4 -4
- package/src/widget/widget.registry.ts +1 -1
|
@@ -1 +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;
|
|
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,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjD,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,6 +1,7 @@
|
|
|
1
1
|
import type { RouteNode, RouteFiles } from '../src/type/route-tree.type.ts';
|
|
2
2
|
import { resolveTargetNode } from '../src/route/route-tree.util.ts';
|
|
3
3
|
import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
4
|
+
import type { ElementManifestEntry } from '../src/type/element.type.ts';
|
|
4
5
|
|
|
5
6
|
export const CONTENT_TYPES: Map<string, string> = new Map<string, string>([
|
|
6
7
|
['.html', 'text/html; charset=utf-8'],
|
|
@@ -31,12 +32,15 @@ export type FetchReturn = ReturnType<typeof fetch>;
|
|
|
31
32
|
|
|
32
33
|
export const DEFAULT_ROUTES_DIR = '/routes';
|
|
33
34
|
export const DEFAULT_WIDGETS_DIR = '/widgets';
|
|
35
|
+
export const DEFAULT_ELEMENTS_DIR = '/elements';
|
|
34
36
|
export const ROUTES_MANIFEST_PATH = '/routes.manifest.json';
|
|
35
37
|
export const WIDGETS_MANIFEST_PATH = '/widgets.manifest.json';
|
|
38
|
+
export const ELEMENTS_MANIFEST_PATH = '/elements.manifest.json';
|
|
36
39
|
|
|
37
40
|
export interface RuntimeConfig {
|
|
38
41
|
routesDir?: string;
|
|
39
42
|
widgetsDir?: string;
|
|
43
|
+
elementsDir?: string;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -168,11 +172,13 @@ export abstract class Runtime {
|
|
|
168
172
|
|
|
169
173
|
private routesManifestCache: Response | null = null;
|
|
170
174
|
private widgetsManifestCache: Response | null = null;
|
|
175
|
+
private elementsManifestCache: Response | null = null;
|
|
171
176
|
|
|
172
177
|
/** Clear cached manifests so the next query triggers a fresh scan. */
|
|
173
178
|
invalidateManifests(): void {
|
|
174
179
|
this.routesManifestCache = null;
|
|
175
180
|
this.widgetsManifestCache = null;
|
|
181
|
+
this.elementsManifestCache = null;
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/**
|
|
@@ -215,6 +221,25 @@ export abstract class Runtime {
|
|
|
215
221
|
return this.widgetsManifestCache.clone();
|
|
216
222
|
}
|
|
217
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the elements manifest. Called when the concrete runtime returns
|
|
226
|
+
* 404 for ELEMENTS_MANIFEST_PATH. Scans `config.elementsDir` (or default).
|
|
227
|
+
*/
|
|
228
|
+
async resolveElementsManifest(): Promise<Response> {
|
|
229
|
+
if (this.elementsManifestCache) return this.elementsManifestCache.clone();
|
|
230
|
+
|
|
231
|
+
const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
|
|
232
|
+
|
|
233
|
+
const dirResponse = await this.query(elementsDir + '/');
|
|
234
|
+
if (dirResponse.status === 404) {
|
|
235
|
+
return new Response('Not Found', { status: 404 });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const entries = await this.scanElements(elementsDir, elementsDir.replace(/^\//, ''));
|
|
239
|
+
this.elementsManifestCache = Response.json(entries);
|
|
240
|
+
return this.elementsManifestCache.clone();
|
|
241
|
+
}
|
|
242
|
+
|
|
218
243
|
// ── Scanning ──────────────────────────────────────────────────────────
|
|
219
244
|
|
|
220
245
|
protected async *walkDirectory(dir: string): AsyncGenerator<string> {
|
|
@@ -344,4 +369,46 @@ export abstract class Runtime {
|
|
|
344
369
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
345
370
|
return entries;
|
|
346
371
|
}
|
|
372
|
+
|
|
373
|
+
protected async scanElements(
|
|
374
|
+
elementsDir: string,
|
|
375
|
+
pathPrefix?: string,
|
|
376
|
+
): Promise<ElementManifestEntry[]> {
|
|
377
|
+
const entries: ElementManifestEntry[] = [];
|
|
378
|
+
|
|
379
|
+
const trailingDir = elementsDir.endsWith('/') ? elementsDir : elementsDir + '/';
|
|
380
|
+
const response = await this.query(trailingDir);
|
|
381
|
+
const listing: string[] = await response.json();
|
|
382
|
+
|
|
383
|
+
for (const item of listing) {
|
|
384
|
+
if (!item.endsWith('/')) continue;
|
|
385
|
+
|
|
386
|
+
const name = item.slice(0, -1);
|
|
387
|
+
|
|
388
|
+
// Custom element names must contain a hyphen (web spec requirement)
|
|
389
|
+
if (!name.includes('-')) {
|
|
390
|
+
console.warn(`[emroute] Skipping element "${name}": custom element names must contain a hyphen (e.g. "my-element")`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Try .element.ts first, then .element.js
|
|
395
|
+
let moduleFile = `${name}.element.ts`;
|
|
396
|
+
let modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
397
|
+
if ((await this.query(modulePath)).status === 404) {
|
|
398
|
+
moduleFile = `${name}.element.js`;
|
|
399
|
+
modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
400
|
+
if ((await this.query(modulePath)).status === 404) continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const prefix = pathPrefix ? `${pathPrefix}/` : '';
|
|
404
|
+
entries.push({
|
|
405
|
+
name,
|
|
406
|
+
modulePath: `${prefix}${name}/${moduleFile}`,
|
|
407
|
+
tagName: name,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
412
|
+
return entries;
|
|
413
|
+
}
|
|
347
414
|
}
|
|
@@ -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
|
export class BunFsRuntime extends Runtime {
|
|
@@ -99,6 +100,7 @@ export class BunFsRuntime extends Runtime {
|
|
|
99
100
|
const pathname = path.slice(this.root.length);
|
|
100
101
|
if (pathname === ROUTES_MANIFEST_PATH) return this.resolveRoutesManifest();
|
|
101
102
|
if (pathname === WIDGETS_MANIFEST_PATH) return this.resolveWidgetsManifest();
|
|
103
|
+
if (pathname === ELEMENTS_MANIFEST_PATH) return this.resolveElementsManifest();
|
|
102
104
|
return new Response('Not Found', { status: 404 });
|
|
103
105
|
}
|
|
104
106
|
return new Response(`Internal Error: ${error}`, { status: 500 });
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Runtime,
|
|
8
8
|
type RuntimeConfig,
|
|
9
9
|
WIDGETS_MANIFEST_PATH,
|
|
10
|
+
ELEMENTS_MANIFEST_PATH,
|
|
10
11
|
} from '../../abstract.runtime.ts';
|
|
11
12
|
|
|
12
13
|
export class BunSqliteRuntime extends Runtime {
|
|
@@ -108,6 +109,7 @@ export class BunSqliteRuntime extends Runtime {
|
|
|
108
109
|
if (!row) {
|
|
109
110
|
if (path === ROUTES_MANIFEST_PATH) return this.resolveRoutesManifest();
|
|
110
111
|
if (path === WIDGETS_MANIFEST_PATH) return this.resolveWidgetsManifest();
|
|
112
|
+
if (path === ELEMENTS_MANIFEST_PATH) return this.resolveElementsManifest();
|
|
111
113
|
return new Response('Not Found', { status: 404 });
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -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
|
/**
|
|
@@ -107,6 +108,7 @@ export class UniversalFsRuntime extends Runtime {
|
|
|
107
108
|
const pathname = path.slice(this.root.length);
|
|
108
109
|
if (pathname === ROUTES_MANIFEST_PATH) return this.resolveRoutesManifest();
|
|
109
110
|
if (pathname === WIDGETS_MANIFEST_PATH) return this.resolveWidgetsManifest();
|
|
111
|
+
if (pathname === ELEMENTS_MANIFEST_PATH) return this.resolveElementsManifest();
|
|
110
112
|
return new Response('Not Found', { status: 404 });
|
|
111
113
|
}
|
|
112
114
|
return new Response(`Internal Error: ${error}`, { status: 500 });
|
package/server/build.util.ts
CHANGED
|
@@ -20,9 +20,11 @@ import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
|
20
20
|
import {
|
|
21
21
|
ROUTES_MANIFEST_PATH,
|
|
22
22
|
WIDGETS_MANIFEST_PATH,
|
|
23
|
+
ELEMENTS_MANIFEST_PATH,
|
|
23
24
|
} from '../runtime/abstract.runtime.ts';
|
|
24
25
|
import type { RouteNode } from '../src/type/route-tree.type.ts';
|
|
25
26
|
import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
27
|
+
import type { ElementManifestEntry } from '../src/type/element.type.ts';
|
|
26
28
|
import { generateMainTs } from './codegen.util.ts';
|
|
27
29
|
import type { SpaMode } from '../src/type/widget.type.ts';
|
|
28
30
|
|
|
@@ -146,10 +148,23 @@ async function writeShell(
|
|
|
146
148
|
): Promise<void> {
|
|
147
149
|
if ((await runtime.query('/index.html')).status !== 404) return;
|
|
148
150
|
|
|
151
|
+
// Base emroute imports
|
|
149
152
|
const imports: Record<string, string> = {};
|
|
150
153
|
for (const pkg of EMROUTE_EXTERNALS) {
|
|
151
154
|
imports[pkg] = paths.emroute;
|
|
152
155
|
}
|
|
156
|
+
|
|
157
|
+
// Merge user-provided importmap.json (user entries win on conflict)
|
|
158
|
+
const mapResponse = await runtime.query('/importmap.json');
|
|
159
|
+
if (mapResponse.status !== 404) {
|
|
160
|
+
const userMap = await mapResponse.json() as { imports?: Record<string, string> };
|
|
161
|
+
if (userMap.imports) {
|
|
162
|
+
for (const [key, value] of Object.entries(userMap.imports)) {
|
|
163
|
+
imports[key] = value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
153
168
|
const importMap = JSON.stringify({ imports }, null, 2);
|
|
154
169
|
|
|
155
170
|
const html = `<!DOCTYPE html>
|
|
@@ -251,11 +266,10 @@ async function mergeModules(runtime: Runtime): Promise<void> {
|
|
|
251
266
|
// Merge route modules
|
|
252
267
|
async function walkRoutes(node: RouteNode): Promise<void> {
|
|
253
268
|
if (node.files?.ts) {
|
|
254
|
-
const companions = {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
};
|
|
269
|
+
const companions: { html?: string; md?: string; css?: string } = {};
|
|
270
|
+
if (node.files.html) companions.html = node.files.html;
|
|
271
|
+
if (node.files.md) companions.md = node.files.md;
|
|
272
|
+
if (node.files.css) companions.css = node.files.css;
|
|
259
273
|
node.files.js = await transpileAndMerge(runtime, node.files.ts, companions);
|
|
260
274
|
delete node.files.ts;
|
|
261
275
|
delete node.files.html;
|
|
@@ -290,6 +304,19 @@ async function mergeModules(runtime: Runtime): Promise<void> {
|
|
|
290
304
|
}
|
|
291
305
|
}
|
|
292
306
|
|
|
307
|
+
// Read element manifest
|
|
308
|
+
const elementsResponse = await runtime.query(ELEMENTS_MANIFEST_PATH);
|
|
309
|
+
const elementEntries: ElementManifestEntry[] = elementsResponse.status !== 404
|
|
310
|
+
? await elementsResponse.json()
|
|
311
|
+
: [];
|
|
312
|
+
|
|
313
|
+
// Merge element modules
|
|
314
|
+
for (const entry of elementEntries) {
|
|
315
|
+
if (entry.modulePath.endsWith('.ts')) {
|
|
316
|
+
entry.modulePath = await transpileAndMerge(runtime, entry.modulePath);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
293
320
|
// Write updated manifests back
|
|
294
321
|
await runtime.command(ROUTES_MANIFEST_PATH, {
|
|
295
322
|
body: JSON.stringify(routeTree),
|
|
@@ -297,6 +324,11 @@ async function mergeModules(runtime: Runtime): Promise<void> {
|
|
|
297
324
|
await runtime.command(WIDGETS_MANIFEST_PATH, {
|
|
298
325
|
body: JSON.stringify(widgetEntries),
|
|
299
326
|
});
|
|
327
|
+
if (elementEntries.length > 0) {
|
|
328
|
+
await runtime.command(ELEMENTS_MANIFEST_PATH, {
|
|
329
|
+
body: JSON.stringify(elementEntries),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
300
332
|
}
|
|
301
333
|
|
|
302
334
|
// ── esbuild loader ────────────────────────────────────────────────────
|
package/server/emroute.server.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { SsrHtmlRouter } from '../src/renderer/ssr/html.renderer.ts';
|
|
|
34
34
|
import { SsrMdRouter } from '../src/renderer/ssr/md.renderer.ts';
|
|
35
35
|
import type { RouteNode } from '../src/type/route-tree.type.ts';
|
|
36
36
|
import type { WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
37
|
+
import type { ElementManifestEntry } from '../src/type/element.type.ts';
|
|
37
38
|
import { WidgetRegistry } from '../src/widget/widget.registry.ts';
|
|
38
39
|
import type { WidgetComponent } from '../src/component/widget.component.ts';
|
|
39
40
|
import { escapeHtml } from '../src/util/html.util.ts';
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
ROUTES_MANIFEST_PATH,
|
|
43
44
|
Runtime,
|
|
44
45
|
WIDGETS_MANIFEST_PATH,
|
|
46
|
+
ELEMENTS_MANIFEST_PATH,
|
|
45
47
|
} from '../runtime/abstract.runtime.ts';
|
|
46
48
|
import type { EmrouteServer, EmrouteServerConfig } from './server-api.type.ts';
|
|
47
49
|
|
|
@@ -254,6 +256,14 @@ export async function createEmrouteServer(
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
|
|
259
|
+
// ── Elements (read from runtime) ──────────────────────────────────
|
|
260
|
+
|
|
261
|
+
let discoveredElementEntries: ElementManifestEntry[] = [];
|
|
262
|
+
const elementsResponse = await runtime.query(ELEMENTS_MANIFEST_PATH);
|
|
263
|
+
if (elementsResponse.status !== 404) {
|
|
264
|
+
discoveredElementEntries = await elementsResponse.json();
|
|
265
|
+
}
|
|
266
|
+
|
|
257
267
|
// ── SSR routers ──────────────────────────────────────────────────────
|
|
258
268
|
|
|
259
269
|
let ssrHtmlRouter: SsrHtmlRouter | null = null;
|
|
@@ -269,17 +279,17 @@ export async function createEmrouteServer(
|
|
|
269
279
|
ssrHtmlRouter = new SsrHtmlRouter(resolver, {
|
|
270
280
|
fileReader: (path) => runtime.query(path, { as: 'text' }),
|
|
271
281
|
moduleLoaders,
|
|
272
|
-
markdownRenderer: config.markdownRenderer,
|
|
273
|
-
extendContext: config.extendContext,
|
|
274
|
-
widgets,
|
|
282
|
+
...(config.markdownRenderer ? { markdownRenderer: config.markdownRenderer } : {}),
|
|
283
|
+
...(config.extendContext ? { extendContext: config.extendContext } : {}),
|
|
284
|
+
...(widgets ? { widgets } : {}),
|
|
275
285
|
widgetFiles,
|
|
276
286
|
});
|
|
277
287
|
|
|
278
288
|
ssrMdRouter = new SsrMdRouter(resolver, {
|
|
279
289
|
fileReader: (path) => runtime.query(path, { as: 'text' }),
|
|
280
290
|
moduleLoaders,
|
|
281
|
-
extendContext: config.extendContext,
|
|
282
|
-
widgets,
|
|
291
|
+
...(config.extendContext ? { extendContext: config.extendContext } : {}),
|
|
292
|
+
...(widgets ? { widgets } : {}),
|
|
283
293
|
widgetFiles,
|
|
284
294
|
});
|
|
285
295
|
}
|
|
@@ -289,7 +299,8 @@ export async function createEmrouteServer(
|
|
|
289
299
|
// ── HTML shell ───────────────────────────────────────────────────────
|
|
290
300
|
|
|
291
301
|
const title = config.title ?? 'emroute';
|
|
292
|
-
|
|
302
|
+
const shellBase = (spa === 'root' || spa === 'only') ? appBase : htmlBase;
|
|
303
|
+
let shell = await resolveShell(runtime, title, shellBase);
|
|
293
304
|
|
|
294
305
|
// Auto-discover main.css and inject <link> into <head>
|
|
295
306
|
if ((await runtime.query('/main.css')).status !== 404) {
|
|
@@ -413,6 +424,9 @@ export async function createEmrouteServer(
|
|
|
413
424
|
get widgetEntries() {
|
|
414
425
|
return discoveredWidgetEntries;
|
|
415
426
|
},
|
|
427
|
+
get elementEntries() {
|
|
428
|
+
return discoveredElementEntries;
|
|
429
|
+
},
|
|
416
430
|
get shell() {
|
|
417
431
|
return shell;
|
|
418
432
|
},
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { RouteNode } from '../src/type/route-tree.type.ts';
|
|
10
10
|
import type { MarkdownRenderer } from '../src/type/markdown.type.ts';
|
|
11
11
|
import type { SpaMode, WidgetManifestEntry } from '../src/type/widget.type.ts';
|
|
12
|
+
import type { ElementManifestEntry } from '../src/type/element.type.ts';
|
|
12
13
|
import type { ContextProvider } from '../src/component/abstract.component.ts';
|
|
13
14
|
import type { BasePath } from '../src/route/route.core.ts';
|
|
14
15
|
import type { WidgetRegistry } from '../src/widget/widget.registry.ts';
|
|
@@ -92,6 +93,9 @@ export interface EmrouteServer {
|
|
|
92
93
|
/** Discovered widget entries. */
|
|
93
94
|
readonly widgetEntries: WidgetManifestEntry[];
|
|
94
95
|
|
|
96
|
+
/** Discovered element entries. */
|
|
97
|
+
readonly elementEntries: ElementManifestEntry[];
|
|
98
|
+
|
|
95
99
|
/** The resolved HTML shell. */
|
|
96
100
|
readonly shell: string;
|
|
97
101
|
}
|
|
@@ -105,7 +105,7 @@ export abstract class Component<
|
|
|
105
105
|
abstract readonly name: string;
|
|
106
106
|
|
|
107
107
|
/** Host element reference, set by ComponentElement in the browser. */
|
|
108
|
-
element?: HTMLElement;
|
|
108
|
+
element?: HTMLElement | undefined;
|
|
109
109
|
|
|
110
110
|
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
111
111
|
readonly files?: { html?: string; md?: string; css?: string };
|
|
@@ -18,6 +18,17 @@ import { HTMLElementBase, LAZY_ATTR, SSR_ATTR } from '../util/html.util.ts';
|
|
|
18
18
|
|
|
19
19
|
type ComponentState = 'idle' | 'loading' | 'ready' | 'error';
|
|
20
20
|
|
|
21
|
+
/** Strip keys with undefined values — returns the filtered object, or undefined if all values are undefined. */
|
|
22
|
+
function filterUndefined<T extends Record<string, unknown>>(obj: T): { [K in keyof T as T[K] extends undefined ? never : K]: NonNullable<T[K]> } | undefined {
|
|
23
|
+
const result: Record<string, unknown> = {};
|
|
24
|
+
let hasValue = false;
|
|
25
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
26
|
+
if (v !== undefined) { result[k] = v; hasValue = true; }
|
|
27
|
+
}
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
return hasValue ? result as any : undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
type WidgetFiles = { html?: string; md?: string; css?: string };
|
|
22
33
|
|
|
23
34
|
/**
|
|
@@ -42,7 +53,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
private component: Component<TParams, TData>;
|
|
45
|
-
private effectiveFiles?: WidgetFiles;
|
|
56
|
+
private effectiveFiles?: WidgetFiles | undefined;
|
|
46
57
|
private params: TParams | null = null;
|
|
47
58
|
private data: TData | null = null;
|
|
48
59
|
private context!: ComponentContext;
|
|
@@ -231,12 +242,13 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
231
242
|
if (signal.aborted) return;
|
|
232
243
|
|
|
233
244
|
const currentUrl = globalThis.location ? new URL(location.href) : new URL('http://localhost/');
|
|
245
|
+
const filteredFiles = filterUndefined(files);
|
|
234
246
|
const base: ComponentContext = {
|
|
235
247
|
url: currentUrl,
|
|
236
248
|
pathname: currentUrl.pathname,
|
|
237
249
|
searchParams: currentUrl.searchParams,
|
|
238
250
|
params: this.params ?? {},
|
|
239
|
-
|
|
251
|
+
...(filteredFiles ? { files: filteredFiles } : {}),
|
|
240
252
|
};
|
|
241
253
|
this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
|
|
242
254
|
|
|
@@ -350,7 +362,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
350
362
|
filePaths.css ? ComponentElement.loadFile(filePaths.css) : undefined,
|
|
351
363
|
]);
|
|
352
364
|
|
|
353
|
-
return { html, md, css };
|
|
365
|
+
return filterUndefined({ html, md, css }) ?? {};
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
private async loadData(): Promise<void> {
|
|
@@ -364,7 +376,7 @@ export class ComponentElement<TParams, TData> extends HTMLElementBase {
|
|
|
364
376
|
try {
|
|
365
377
|
const promise = this.component.getData({
|
|
366
378
|
params: this.params,
|
|
367
|
-
signal,
|
|
379
|
+
...(signal ? { signal } : {}),
|
|
368
380
|
context: this.context,
|
|
369
381
|
});
|
|
370
382
|
this.dataPromise = promise;
|
|
@@ -80,7 +80,7 @@ export class MarkdownElement extends HTMLElementBase {
|
|
|
80
80
|
const signal = this.abortController?.signal;
|
|
81
81
|
|
|
82
82
|
try {
|
|
83
|
-
const response = await fetch(src, { signal });
|
|
83
|
+
const response = await fetch(src, signal ? { signal } : {});
|
|
84
84
|
|
|
85
85
|
if (!response.ok) {
|
|
86
86
|
throw new Error(`Failed to fetch ${src}: ${response.status}`);
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export type {
|
|
|
40
40
|
WidgetsManifest,
|
|
41
41
|
} from './type/widget.type.ts';
|
|
42
42
|
|
|
43
|
+
export type { ElementManifestEntry } from './type/element.type.ts';
|
|
43
44
|
export type { MarkdownRenderer } from './type/markdown.type.ts';
|
|
44
45
|
export { type Logger, setLogger } from './type/logger.type.ts';
|
|
45
46
|
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import type { EmrouteServer } from '../../../server/server-api.type.ts';
|
|
12
12
|
import { createEmrouteServer } from '../../../server/emroute.server.ts';
|
|
13
13
|
import { FetchRuntime } from '../../../runtime/fetch.runtime.ts';
|
|
14
|
-
import { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH } from '../../../runtime/abstract.runtime.ts';
|
|
14
|
+
import { ROUTES_MANIFEST_PATH, WIDGETS_MANIFEST_PATH, ELEMENTS_MANIFEST_PATH } from '../../../runtime/abstract.runtime.ts';
|
|
15
15
|
import type { RouteNode } from '../../type/route-tree.type.ts';
|
|
16
16
|
import type { NavigateOptions } from '../../type/route.type.ts';
|
|
17
17
|
import type { WidgetManifestEntry } from '../../type/widget.type.ts';
|
|
18
|
+
import type { ElementManifestEntry } from '../../type/element.type.ts';
|
|
18
19
|
import { assertSafeRedirect, type BasePath, DEFAULT_BASE_PATH } from '../../route/route.core.ts';
|
|
19
20
|
import { escapeHtml } from '../../util/html.util.ts';
|
|
20
21
|
import { ComponentElement } from '../../element/component.element.ts';
|
|
@@ -207,8 +208,14 @@ export async function bootEmrouteApp(options?: BootOptions): Promise<EmrouteApp>
|
|
|
207
208
|
? await widgetsResponse.json()
|
|
208
209
|
: [];
|
|
209
210
|
|
|
210
|
-
//
|
|
211
|
-
const
|
|
211
|
+
// Fetch element manifest (optional — app may have no custom elements)
|
|
212
|
+
const elementsResponse = await runtime.handle(ELEMENTS_MANIFEST_PATH);
|
|
213
|
+
const elementEntries: ElementManifestEntry[] = elementsResponse.ok
|
|
214
|
+
? await elementsResponse.json()
|
|
215
|
+
: [];
|
|
216
|
+
|
|
217
|
+
// Build lazy module loaders for all route + widget + element modules
|
|
218
|
+
const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, elementEntries, runtime);
|
|
212
219
|
|
|
213
220
|
// Register widgets eagerly (tag defined immediately, module loads on connectedCallback)
|
|
214
221
|
const widgets = new WidgetRegistry();
|
|
@@ -216,24 +223,41 @@ export async function bootEmrouteApp(options?: BootOptions): Promise<EmrouteApp>
|
|
|
216
223
|
ComponentElement.registerLazy(entry.name, entry.files, moduleLoaders[entry.modulePath]);
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
// Register custom elements — import all modules, define when loaded
|
|
227
|
+
for (const entry of elementEntries) {
|
|
228
|
+
const loader = moduleLoaders[entry.modulePath];
|
|
229
|
+
if (loader) {
|
|
230
|
+
loader().then((mod) => {
|
|
231
|
+
const cls = (mod as Record<string, unknown>).default;
|
|
232
|
+
if (typeof cls === 'function' && !customElements.get(entry.tagName)) {
|
|
233
|
+
customElements.define(entry.tagName, cls as CustomElementConstructor);
|
|
234
|
+
}
|
|
235
|
+
}).catch((e) => {
|
|
236
|
+
console.error(`[emroute] Failed to load element ${entry.tagName}:`, e);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
// Create the server (reuses the same createEmrouteServer as SSR)
|
|
242
|
+
const mdRenderer = MarkdownElement.getConfiguredRenderer();
|
|
220
243
|
const server = await createEmrouteServer({
|
|
221
244
|
routeTree,
|
|
222
245
|
widgets,
|
|
223
246
|
moduleLoaders,
|
|
224
|
-
markdownRenderer:
|
|
247
|
+
...(mdRenderer ? { markdownRenderer: mdRenderer } : {}),
|
|
225
248
|
}, runtime);
|
|
226
249
|
|
|
227
250
|
return createEmrouteApp(server, options);
|
|
228
251
|
}
|
|
229
252
|
|
|
230
253
|
/**
|
|
231
|
-
* Walk the route tree and
|
|
254
|
+
* Walk the route tree, widget entries, and element entries to build a map of
|
|
232
255
|
* `path → () => runtime.loadModule(path)` lazy loaders.
|
|
233
256
|
*/
|
|
234
257
|
function buildLazyLoaders(
|
|
235
258
|
tree: RouteNode,
|
|
236
259
|
widgetEntries: WidgetManifestEntry[],
|
|
260
|
+
elementEntries: ElementManifestEntry[],
|
|
237
261
|
runtime: FetchRuntime,
|
|
238
262
|
): Record<string, () => Promise<unknown>> {
|
|
239
263
|
const paths = new Set<string>();
|
|
@@ -252,6 +276,7 @@ function buildLazyLoaders(
|
|
|
252
276
|
|
|
253
277
|
walk(tree);
|
|
254
278
|
for (const entry of widgetEntries) paths.add(entry.modulePath);
|
|
279
|
+
for (const entry of elementEntries) paths.add(entry.modulePath);
|
|
255
280
|
|
|
256
281
|
const loaders: Record<string, () => Promise<unknown>> = {};
|
|
257
282
|
for (const path of paths) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { RouteConfig, RouteInfo } from '../../type/route.type.ts';
|
|
9
|
+
import type { ComponentContext } from '../../component/abstract.component.ts';
|
|
9
10
|
import type { RouteResolver } from '../../route/route.resolver.ts';
|
|
10
11
|
import type { PageComponent } from '../../component/page.component.ts';
|
|
11
12
|
import { DEFAULT_ROOT_ROUTE } from '../../route/route.core.ts';
|
|
@@ -68,7 +69,7 @@ export class SsrMdRouter extends SsrRenderer {
|
|
|
68
69
|
content = await this.resolveWidgets(content, routeInfo);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
return { content, title };
|
|
72
|
+
return { content, ...(title != null ? { title } : {}) };
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
protected override renderContent(
|
|
@@ -118,13 +119,13 @@ export class SsrMdRouter extends SsrRenderer {
|
|
|
118
119
|
files = await this.core.loadWidgetFiles(filePaths);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
const baseContext = {
|
|
122
|
+
const baseContext: ComponentContext = {
|
|
122
123
|
...routeInfo,
|
|
123
124
|
pathname: routeInfo.url.pathname,
|
|
124
125
|
searchParams: routeInfo.url.searchParams,
|
|
125
|
-
files,
|
|
126
|
+
...(files ? { files } : {}),
|
|
126
127
|
};
|
|
127
|
-
const context = this.core.contextProvider
|
|
128
|
+
const context: ComponentContext = this.core.contextProvider
|
|
128
129
|
? this.core.contextProvider(baseContext)
|
|
129
130
|
: baseContext;
|
|
130
131
|
const data = await widget.getData({ params: block.params, context });
|
|
@@ -60,7 +60,7 @@ export abstract class SsrRenderer {
|
|
|
60
60
|
try {
|
|
61
61
|
const ri: RouteInfo = { url, params: {} };
|
|
62
62
|
const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
|
|
63
|
-
return { content: this.stripSlots(result.content), status: 404, title: result.title };
|
|
63
|
+
return { content: this.stripSlots(result.content), status: 404, ...(result.title != null ? { title: result.title } : {}) };
|
|
64
64
|
} catch (e) {
|
|
65
65
|
logger.error(
|
|
66
66
|
`[${this.label}] Failed to render 404 status page for ${url.pathname}`,
|
|
@@ -89,7 +89,7 @@ export abstract class SsrRenderer {
|
|
|
89
89
|
|
|
90
90
|
try {
|
|
91
91
|
const { content, title } = await this.renderPage(routeInfo, matched, signal);
|
|
92
|
-
return { content, status: 200, title };
|
|
92
|
+
return { content, status: 200, ...(title != null ? { title } : {}) };
|
|
93
93
|
} catch (error) {
|
|
94
94
|
if (error instanceof Response) {
|
|
95
95
|
const statusPage = this.core.getStatusPage(error.status);
|
|
@@ -100,7 +100,7 @@ export abstract class SsrRenderer {
|
|
|
100
100
|
return {
|
|
101
101
|
content: this.stripSlots(result.content),
|
|
102
102
|
status: error.status,
|
|
103
|
-
title: result.title,
|
|
103
|
+
...(result.title != null ? { title: result.title } : {}),
|
|
104
104
|
};
|
|
105
105
|
} catch (e) {
|
|
106
106
|
logger.error(
|
|
@@ -196,7 +196,7 @@ export abstract class SsrRenderer {
|
|
|
196
196
|
|
|
197
197
|
result = this.stripSlots(result);
|
|
198
198
|
|
|
199
|
-
return { content: result, title: pageTitle };
|
|
199
|
+
return { content: result, ...(pageTitle != null ? { title: pageTitle } : {}) };
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
protected abstract renderRouteContent(
|
|
@@ -221,11 +221,11 @@ export abstract class SsrRenderer {
|
|
|
221
221
|
: defaultPageComponent;
|
|
222
222
|
|
|
223
223
|
const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
|
|
224
|
-
const data = await component.getData({ params: routeInfo.params, signal, context });
|
|
224
|
+
const data = await component.getData({ params: routeInfo.params, ...(signal ? { signal } : {}), context });
|
|
225
225
|
const content = this.renderContent(component, { data, params: routeInfo.params, context });
|
|
226
226
|
const title = component.getTitle({ data, params: routeInfo.params, context });
|
|
227
227
|
|
|
228
|
-
return { content, title };
|
|
228
|
+
return { content, ...(title != null ? { title } : {}) };
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
/** Render a component to the output format (HTML or Markdown). */
|