@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.
- package/README.md +1 -1
- package/core/component/abstract.component.ts +74 -0
- package/{src → core}/component/page.component.ts +3 -61
- package/core/component/widget.component.ts +54 -0
- package/core/pipeline/pipeline.ts +224 -0
- package/{src/renderer/ssr → core/renderer}/html.renderer.ts +26 -47
- package/{src/renderer/ssr → core/renderer}/md.renderer.ts +22 -41
- package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +44 -58
- package/{src/route → core/router}/route.resolver.ts +1 -10
- package/core/router/route.trie.ts +175 -0
- package/core/runtime/abstract.runtime.ts +47 -0
- package/core/server/emroute.server.ts +324 -0
- package/core/type/component.type.ts +39 -0
- package/core/type/element.type.ts +10 -0
- package/core/type/logger.type.ts +20 -0
- package/core/type/markdown.type.ts +8 -0
- package/core/type/route-tree.type.ts +28 -0
- package/core/type/route.type.ts +75 -0
- package/core/type/widget.type.ts +27 -0
- package/core/util/html.util.ts +50 -0
- package/{src → core}/util/md.util.ts +3 -5
- package/{src/route → core/util}/route-tree.util.ts +0 -2
- package/{src → core}/util/widget-resolve.util.ts +15 -46
- package/{src → core}/widget/widget.parser.ts +2 -23
- package/core/widget/widget.registry.ts +36 -0
- package/dist/core/component/abstract.component.d.ts +48 -0
- package/dist/core/component/abstract.component.js +42 -0
- package/dist/core/component/abstract.component.js.map +1 -0
- package/dist/core/component/page.component.d.ts +23 -0
- package/dist/core/component/page.component.js +49 -0
- package/dist/core/component/page.component.js.map +1 -0
- package/dist/core/component/widget.component.d.ts +17 -0
- package/dist/core/component/widget.component.js +37 -0
- package/dist/core/component/widget.component.js.map +1 -0
- package/dist/core/pipeline/pipeline.d.ts +61 -0
- package/dist/core/pipeline/pipeline.js +189 -0
- package/dist/core/pipeline/pipeline.js.map +1 -0
- package/dist/{src/renderer/ssr → core/renderer}/html.renderer.d.ts +8 -24
- package/dist/{src/renderer/ssr → core/renderer}/html.renderer.js +20 -35
- package/dist/core/renderer/html.renderer.js.map +1 -0
- package/dist/{src/renderer/ssr → core/renderer}/md.renderer.d.ts +6 -21
- package/dist/{src/renderer/ssr → core/renderer}/md.renderer.js +16 -32
- package/dist/core/renderer/md.renderer.js.map +1 -0
- package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -27
- package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +33 -37
- package/dist/core/renderer/ssr.renderer.js.map +1 -0
- package/dist/{src/route → core/router}/route.resolver.d.ts +1 -8
- package/dist/{src/route → core/router}/route.resolver.js +0 -1
- package/dist/core/router/route.resolver.js.map +1 -0
- package/dist/core/router/route.trie.d.ts +32 -0
- package/dist/core/router/route.trie.js +152 -0
- package/dist/core/router/route.trie.js.map +1 -0
- package/dist/core/runtime/abstract.runtime.d.ts +32 -0
- package/dist/core/runtime/abstract.runtime.js +26 -0
- package/dist/core/runtime/abstract.runtime.js.map +1 -0
- package/dist/core/server/emroute.server.d.ts +48 -0
- package/dist/core/server/emroute.server.js +239 -0
- package/dist/core/server/emroute.server.js.map +1 -0
- package/dist/core/server/server.type.d.ts +45 -0
- package/dist/core/server/server.type.js +11 -0
- package/dist/core/server/server.type.js.map +1 -0
- package/dist/core/type/component.type.d.ts +37 -0
- package/dist/core/type/component.type.js +7 -0
- package/dist/core/type/component.type.js.map +1 -0
- package/dist/core/type/element.type.d.ts +9 -0
- package/dist/core/type/element.type.js +5 -0
- package/dist/core/type/element.type.js.map +1 -0
- package/dist/core/type/logger.type.d.ts +14 -0
- package/dist/core/type/logger.type.js +8 -0
- package/dist/core/type/logger.type.js.map +1 -0
- package/dist/core/type/markdown.type.d.ts +7 -0
- package/dist/core/type/markdown.type.js +5 -0
- package/dist/core/type/markdown.type.js.map +1 -0
- package/dist/{src → core}/type/route-tree.type.d.ts +0 -12
- package/dist/{src → core}/type/route-tree.type.js +0 -1
- package/dist/core/type/route-tree.type.js.map +1 -0
- package/dist/core/type/route.type.d.ts +62 -0
- package/dist/core/type/route.type.js +7 -0
- package/dist/core/type/route.type.js.map +1 -0
- package/dist/core/type/widget.type.d.ts +27 -0
- package/dist/core/type/widget.type.js +5 -0
- package/dist/core/type/widget.type.js.map +1 -0
- package/dist/core/util/html.util.d.ts +14 -0
- package/dist/core/util/html.util.js +43 -0
- package/dist/core/util/html.util.js.map +1 -0
- package/dist/{src → core}/util/md.util.d.ts +0 -1
- package/dist/{src → core}/util/md.util.js +0 -2
- package/dist/core/util/md.util.js.map +1 -0
- package/dist/{src/route → core/util}/route-tree.util.js +0 -2
- package/dist/core/util/route-tree.util.js.map +1 -0
- package/dist/core/util/widget-resolve.util.d.ts +28 -0
- package/dist/{src → core}/util/widget-resolve.util.js +12 -42
- package/dist/core/util/widget-resolve.util.js.map +1 -0
- package/dist/{src → core}/widget/widget.parser.d.ts +0 -13
- package/dist/{src → core}/widget/widget.parser.js +1 -22
- package/dist/core/widget/widget.parser.js.map +1 -0
- package/dist/core/widget/widget.registry.d.ts +14 -0
- package/dist/core/widget/widget.registry.js +26 -0
- package/dist/core/widget/widget.registry.js.map +1 -0
- package/dist/emroute.js +1092 -1220
- package/dist/emroute.js.map +36 -5
- package/dist/runtime/abstract.runtime.d.ts +41 -7
- package/dist/runtime/abstract.runtime.js +404 -9
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +1 -0
- package/dist/runtime/bun/fs/bun-fs.runtime.js +15 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +2 -0
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +8 -0
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/fetch.runtime.d.ts +3 -3
- package/dist/runtime/fetch.runtime.js +3 -3
- package/dist/runtime/sitemap.generator.d.ts +1 -1
- package/dist/runtime/sitemap.generator.js +1 -1
- package/dist/runtime/sitemap.generator.js.map +1 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +1 -0
- package/dist/runtime/universal/fs/universal-fs.runtime.js +15 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
- package/dist/server/build.util.d.ts +9 -10
- package/dist/server/build.util.js +11 -31
- package/dist/server/build.util.js.map +1 -1
- package/dist/server/codegen.util.d.ts +1 -1
- package/dist/server/emroute.server.d.ts +8 -35
- package/dist/server/emroute.server.js +7 -351
- package/dist/server/emroute.server.js.map +1 -1
- package/dist/server/esbuild-manifest.plugin.js +1 -1
- package/dist/server/esbuild-manifest.plugin.js.map +1 -1
- package/dist/server/server-api.type.d.ts +3 -71
- package/dist/server/server-api.type.js +1 -8
- package/dist/server/server-api.type.js.map +1 -1
- package/dist/src/element/component.element.d.ts +6 -14
- package/dist/src/element/component.element.js +13 -40
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/element/markdown.element.d.ts +2 -2
- package/dist/src/element/markdown.element.js +3 -2
- package/dist/src/element/markdown.element.js.map +1 -1
- package/dist/src/index.d.ts +15 -14
- package/dist/src/index.js +8 -8
- package/dist/src/index.js.map +1 -1
- package/dist/src/renderer/spa/emroute.app.d.ts +50 -0
- package/dist/src/renderer/spa/emroute.app.js +246 -0
- package/dist/src/renderer/spa/emroute.app.js.map +1 -0
- package/dist/src/renderer/spa/mod.d.ts +17 -16
- package/dist/src/renderer/spa/mod.js +9 -9
- package/dist/src/renderer/spa/mod.js.map +1 -1
- package/dist/src/renderer/spa/thin-client.d.ts +3 -3
- package/dist/src/renderer/spa/thin-client.js +7 -7
- package/dist/src/renderer/spa/thin-client.js.map +1 -1
- package/dist/src/route/route.core.d.ts +3 -3
- package/dist/src/util/html.util.d.ts +5 -22
- package/dist/src/util/html.util.js +8 -56
- package/dist/src/util/html.util.js.map +1 -1
- package/dist/src/widget/breadcrumb.widget.d.ts +2 -2
- package/dist/src/widget/breadcrumb.widget.js +2 -2
- package/dist/src/widget/breadcrumb.widget.js.map +1 -1
- package/dist/src/widget/page-title.widget.d.ts +1 -1
- package/dist/src/widget/page-title.widget.js +1 -1
- package/dist/src/widget/page-title.widget.js.map +1 -1
- package/package.json +8 -8
- package/runtime/abstract.runtime.ts +433 -17
- package/runtime/bun/fs/bun-fs.runtime.ts +15 -1
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +9 -0
- package/runtime/fetch.runtime.ts +3 -3
- package/runtime/sitemap.generator.ts +2 -2
- package/runtime/universal/fs/universal-fs.runtime.ts +15 -1
- package/server/build.util.ts +17 -43
- package/server/codegen.util.ts +1 -1
- package/server/emroute.server.ts +12 -426
- package/src/element/component.element.ts +14 -54
- package/src/element/markdown.element.ts +4 -3
- package/src/index.ts +22 -19
- package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +19 -20
- package/src/renderer/spa/mod.ts +22 -22
- package/src/util/html.util.ts +16 -61
- package/src/widget/breadcrumb.widget.ts +3 -3
- package/src/widget/page-title.widget.ts +1 -1
- package/dist/src/component/abstract.component.d.ts +0 -199
- package/dist/src/component/abstract.component.js +0 -84
- package/dist/src/component/abstract.component.js.map +0 -1
- package/dist/src/component/page.component.d.ts +0 -74
- package/dist/src/component/page.component.js +0 -107
- package/dist/src/component/page.component.js.map +0 -1
- package/dist/src/component/widget.component.d.ts +0 -47
- package/dist/src/component/widget.component.js +0 -69
- package/dist/src/component/widget.component.js.map +0 -1
- package/dist/src/renderer/ssr/html.renderer.js.map +0 -1
- package/dist/src/renderer/ssr/md.renderer.js.map +0 -1
- package/dist/src/renderer/ssr/ssr.renderer.js.map +0 -1
- package/dist/src/route/route-tree.util.js.map +0 -1
- package/dist/src/route/route.matcher.d.ts +0 -86
- package/dist/src/route/route.matcher.js +0 -214
- package/dist/src/route/route.matcher.js.map +0 -1
- package/dist/src/route/route.resolver.js.map +0 -1
- package/dist/src/route/route.trie.d.ts +0 -38
- package/dist/src/route/route.trie.js +0 -206
- package/dist/src/route/route.trie.js.map +0 -1
- package/dist/src/type/element.type.d.ts +0 -19
- package/dist/src/type/element.type.js +0 -9
- package/dist/src/type/element.type.js.map +0 -1
- package/dist/src/type/logger.type.d.ts +0 -17
- package/dist/src/type/logger.type.js +0 -9
- package/dist/src/type/logger.type.js.map +0 -1
- package/dist/src/type/markdown.type.d.ts +0 -20
- package/dist/src/type/markdown.type.js +0 -2
- package/dist/src/type/markdown.type.js.map +0 -1
- package/dist/src/type/route-tree.type.js.map +0 -1
- package/dist/src/type/route.type.d.ts +0 -94
- package/dist/src/type/route.type.js +0 -8
- package/dist/src/type/route.type.js.map +0 -1
- package/dist/src/type/widget.type.d.ts +0 -55
- package/dist/src/type/widget.type.js +0 -10
- package/dist/src/type/widget.type.js.map +0 -1
- package/dist/src/util/logger.util.d.ts +0 -26
- package/dist/src/util/logger.util.js +0 -80
- package/dist/src/util/logger.util.js.map +0 -1
- package/dist/src/util/md.util.js.map +0 -1
- package/dist/src/util/widget-resolve.util.d.ts +0 -52
- package/dist/src/util/widget-resolve.util.js.map +0 -1
- package/dist/src/widget/widget.parser.js.map +0 -1
- package/dist/src/widget/widget.registry.d.ts +0 -23
- package/dist/src/widget/widget.registry.js +0 -42
- package/dist/src/widget/widget.registry.js.map +0 -1
- package/runtime/bun/esbuild-runtime-loader.plugin.ts +0 -112
- package/server/esbuild-manifest.plugin.ts +0 -209
- package/server/server-api.type.ts +0 -101
- package/src/component/abstract.component.ts +0 -231
- package/src/component/widget.component.ts +0 -85
- package/src/route/route.core.ts +0 -371
- package/src/route/route.trie.ts +0 -265
- package/src/type/element.type.ts +0 -22
- package/src/type/logger.type.ts +0 -24
- package/src/type/markdown.type.ts +0 -21
- package/src/type/route-tree.type.ts +0 -51
- package/src/type/route.type.ts +0 -124
- package/src/type/widget.type.ts +0 -65
- package/src/util/logger.util.ts +0 -83
- package/src/widget/widget.registry.ts +0 -51
- /package/dist/{src/route → core/util}/route-tree.util.d.ts +0 -0
package/README.md
CHANGED
|
@@ -145,7 +145,7 @@ Pick your runtime: [Bun](doc/01a-setup-bun.md) | [Node](doc/01b-setup-node.md) |
|
|
|
145
145
|
- [Routing](doc/04-routing.md) — dynamic segments, catch-all, redirects
|
|
146
146
|
- [Nesting](doc/05-nesting.md) — layouts, slots, passthrough pages, tips and tricks
|
|
147
147
|
- [Widgets](doc/06-widgets.md) — interactive islands with data lifecycle
|
|
148
|
-
- [Server](doc/07-server.md) — `
|
|
148
|
+
- [Server](doc/07-server.md) — `Emroute.create`, composition, static files
|
|
149
149
|
- [Markdown renderers](doc/08-markdown-renderer.md) — pluggable parser interface and setup
|
|
150
150
|
- [Runtime](doc/09-runtime.md) — abstract runtime, UniversalFsRuntime, BunFsRuntime, BunSqliteRuntime
|
|
151
151
|
- [SPA modes](doc/10-spa-mode.md) — none, leaf, root, only
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Base Class
|
|
3
|
+
*
|
|
4
|
+
* Everything is a Component: pages and widgets.
|
|
5
|
+
* Components render differently based on context:
|
|
6
|
+
* - /md/* → Markdown (LLMs, text clients)
|
|
7
|
+
* - /html/* → Pre-rendered HTML (SSR)
|
|
8
|
+
* - SPA → Hydrated custom elements
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ComponentContext } from '../type/component.type.ts';
|
|
12
|
+
import { escapeHtml } from '../util/html.util.ts';
|
|
13
|
+
|
|
14
|
+
export abstract class Component<
|
|
15
|
+
TParams = unknown,
|
|
16
|
+
TData = unknown,
|
|
17
|
+
TContext extends ComponentContext = ComponentContext,
|
|
18
|
+
> {
|
|
19
|
+
declare readonly DataArgs: {
|
|
20
|
+
params: TParams;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
context: TContext;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
declare readonly RenderArgs: {
|
|
26
|
+
data: TData | null;
|
|
27
|
+
params: TParams;
|
|
28
|
+
context: TContext;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
abstract readonly name: string;
|
|
32
|
+
|
|
33
|
+
/** Host element reference, set by ComponentElement in the browser. */
|
|
34
|
+
element?: HTMLElement | undefined;
|
|
35
|
+
|
|
36
|
+
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
37
|
+
readonly files?: { html?: string; md?: string; css?: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* When true, SSR serializes the getData() result into the element's
|
|
41
|
+
* light DOM so the client can access it immediately in hydrate()
|
|
42
|
+
* without re-fetching.
|
|
43
|
+
*/
|
|
44
|
+
readonly exposeSsrData?: boolean;
|
|
45
|
+
|
|
46
|
+
abstract getData(args: this['DataArgs']): Promise<TData | null>;
|
|
47
|
+
abstract renderMarkdown(args: this['RenderArgs']): string;
|
|
48
|
+
|
|
49
|
+
renderHTML(args: this['RenderArgs']): string {
|
|
50
|
+
if (args.data === null) {
|
|
51
|
+
return `<div data-component="${this.name}">Loading...</div>`;
|
|
52
|
+
}
|
|
53
|
+
const markdown = this.renderMarkdown({
|
|
54
|
+
data: args.data,
|
|
55
|
+
params: args.params,
|
|
56
|
+
context: args.context,
|
|
57
|
+
});
|
|
58
|
+
return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
hydrate?(args: this['RenderArgs']): void;
|
|
62
|
+
destroy?(): void;
|
|
63
|
+
validateParams?(params: TParams): string | undefined;
|
|
64
|
+
|
|
65
|
+
renderError(args: { error: unknown; params: TParams }): string {
|
|
66
|
+
const msg = args.error instanceof Error ? args.error.message : String(args.error);
|
|
67
|
+
return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
renderMarkdownError(error: unknown): string {
|
|
71
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
72
|
+
return `> **Error** (\`${this.name}\`): ${msg}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Params come from URL, context carries file content.
|
|
5
5
|
*
|
|
6
6
|
* Default implementations follow the fallback table:
|
|
7
7
|
* - renderHTML: html file → md via <mark-down> → <router-slot /> (non-leaf only)
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - getData: no-op (returns null)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { Component
|
|
12
|
+
import { Component } from './abstract.component.ts';
|
|
13
|
+
import type { ComponentContext } from '../type/component.type.ts';
|
|
13
14
|
import { escapeHtml } from '../util/html.util.ts';
|
|
14
15
|
|
|
15
16
|
export class PageComponent<
|
|
@@ -18,42 +19,14 @@ export class PageComponent<
|
|
|
18
19
|
TContext extends ComponentContext = ComponentContext,
|
|
19
20
|
> extends Component<TParams, TData, TContext> {
|
|
20
21
|
override readonly name: string = 'page';
|
|
21
|
-
|
|
22
|
-
/** Route pattern this page handles (optional — set by subclasses) */
|
|
23
22
|
readonly pattern?: string;
|
|
24
23
|
|
|
25
|
-
/**
|
|
26
|
-
* Fetch or compute page data. Override in subclasses.
|
|
27
|
-
* Default: returns null (no data needed).
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```ts
|
|
31
|
-
* override getData({ params, context }: this['DataArgs']) {
|
|
32
|
-
* return fetch(`/api/${params.id}`, { signal: context?.signal });
|
|
33
|
-
* }
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
24
|
override getData(
|
|
37
25
|
_args: this['DataArgs'],
|
|
38
26
|
): Promise<TData | null> {
|
|
39
27
|
return Promise.resolve(null);
|
|
40
28
|
}
|
|
41
29
|
|
|
42
|
-
/**
|
|
43
|
-
* Render page as HTML.
|
|
44
|
-
*
|
|
45
|
-
* Fallback chain:
|
|
46
|
-
* 1. html file content from context
|
|
47
|
-
* 2. md file content wrapped in `<mark-down>`
|
|
48
|
-
* 3. `<router-slot />` (bare slot for child routes)
|
|
49
|
-
*
|
|
50
|
-
* @example
|
|
51
|
-
* ```ts
|
|
52
|
-
* override renderHTML({ data, params, context }: this['RenderArgs']) {
|
|
53
|
-
* return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
|
|
54
|
-
* }
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
30
|
override renderHTML(
|
|
58
31
|
args: this['RenderArgs'],
|
|
59
32
|
): string {
|
|
@@ -72,12 +45,6 @@ export class PageComponent<
|
|
|
72
45
|
}
|
|
73
46
|
|
|
74
47
|
if (files?.md) {
|
|
75
|
-
// HOTFIX: skip external <router-slot> when markdown already defines one
|
|
76
|
-
// via ```router-slot fenced block. Without this, SPA mode produces two
|
|
77
|
-
// slots with the same pattern — one inside <mark-down> (from the fenced
|
|
78
|
-
// block) and one outside (from this suffix). SSR strips empty duplicates
|
|
79
|
-
// via stripSlots, but SPA has no equivalent cleanup.
|
|
80
|
-
// See: issues/pending/spa-duplicate-router-slot.issue.md
|
|
81
48
|
const hasSlot = files.md.includes('```router-slot');
|
|
82
49
|
const slot = args.context.isLeaf || hasSlot ? '' : '\n<router-slot></router-slot>';
|
|
83
50
|
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
|
|
@@ -86,20 +53,6 @@ export class PageComponent<
|
|
|
86
53
|
return args.context.isLeaf ? '' : '<router-slot></router-slot>';
|
|
87
54
|
}
|
|
88
55
|
|
|
89
|
-
/**
|
|
90
|
-
* Render page as Markdown.
|
|
91
|
-
*
|
|
92
|
-
* Fallback chain:
|
|
93
|
-
* 1. md file content from context
|
|
94
|
-
* 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* ```ts
|
|
98
|
-
* override renderMarkdown({ data, params, context }: this['RenderArgs']) {
|
|
99
|
-
* return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
|
|
100
|
-
* }
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
56
|
override renderMarkdown(
|
|
104
57
|
args: this['RenderArgs'],
|
|
105
58
|
): string {
|
|
@@ -112,17 +65,6 @@ export class PageComponent<
|
|
|
112
65
|
return args.context.isLeaf ? '' : '```router-slot\n```';
|
|
113
66
|
}
|
|
114
67
|
|
|
115
|
-
/**
|
|
116
|
-
* Page title. Override in subclasses.
|
|
117
|
-
* Default: undefined (no title).
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* ```ts
|
|
121
|
-
* override getTitle({ data, params }: this['RenderArgs']) {
|
|
122
|
-
* return `Project ${params.id}`;
|
|
123
|
-
* }
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
68
|
getTitle(
|
|
127
69
|
_args: this['RenderArgs'],
|
|
128
70
|
): string | undefined {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget Component
|
|
3
|
+
*
|
|
4
|
+
* Embeddable unit within page content. Everything reusable that is not
|
|
5
|
+
* a page is a Widget. Widgets render across all contexts (HTML, Markdown, SPA)
|
|
6
|
+
* and are resolved by name via WidgetRegistry.
|
|
7
|
+
*
|
|
8
|
+
* Default rendering fallback chains (parallel to PageComponent):
|
|
9
|
+
* - renderHTML: html file → md file in <mark-down> → base Component default
|
|
10
|
+
* - renderMarkdown: md file → ''
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Component } from './abstract.component.ts';
|
|
14
|
+
import type { ComponentContext } from '../type/component.type.ts';
|
|
15
|
+
import { escapeHtml, scopeWidgetCss } from '../util/html.util.ts';
|
|
16
|
+
|
|
17
|
+
export abstract class WidgetComponent<
|
|
18
|
+
TParams = unknown,
|
|
19
|
+
TData = unknown,
|
|
20
|
+
TContext extends ComponentContext = ComponentContext,
|
|
21
|
+
> extends Component<TParams, TData, TContext> {
|
|
22
|
+
override renderHTML(
|
|
23
|
+
args: this['RenderArgs'],
|
|
24
|
+
): string {
|
|
25
|
+
const files = args.context.files;
|
|
26
|
+
const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>\n` : '';
|
|
27
|
+
|
|
28
|
+
if (files?.html) {
|
|
29
|
+
return style + files.html;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (files?.md) {
|
|
33
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (style) {
|
|
37
|
+
return style + super.renderHTML(args);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return super.renderHTML(args);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override renderMarkdown(
|
|
44
|
+
args: this['RenderArgs'],
|
|
45
|
+
): string {
|
|
46
|
+
const files = args.context.files;
|
|
47
|
+
|
|
48
|
+
if (files?.md) {
|
|
49
|
+
return files.md;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Orchestration layer between Router, Runtime, and Component.
|
|
5
|
+
*
|
|
6
|
+
* Owns:
|
|
7
|
+
* - Route matching (reads manifest from Runtime, walks RouteNode tree)
|
|
8
|
+
* - Module loading (delegates to Runtime)
|
|
9
|
+
* - Companion file reading (delegates to Runtime)
|
|
10
|
+
* - ComponentContext building
|
|
11
|
+
* - Route hierarchy construction
|
|
12
|
+
*
|
|
13
|
+
* Does NOT own:
|
|
14
|
+
* - Rendering (that's Renderer)
|
|
15
|
+
* - HTTP routing / base paths (that's Server)
|
|
16
|
+
* - Storage I/O (that's Runtime)
|
|
17
|
+
* - Navigation events (that's the browser adapter)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { RouteTrie } from '../router/route.trie.ts';
|
|
21
|
+
import type { RouteNode } from '../type/route-tree.type.ts';
|
|
22
|
+
import type { RouteConfig, MatchedRoute, RouteInfo } from '../type/route.type.ts';
|
|
23
|
+
import type { ComponentContext, ContextProvider, FileContents } from '../type/component.type.ts';
|
|
24
|
+
import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
25
|
+
import { ROUTES_MANIFEST_PATH } from '../runtime/abstract.runtime.ts';
|
|
26
|
+
import { type Logger, defaultLogger } from '../type/logger.type.ts';
|
|
27
|
+
|
|
28
|
+
/** Default root route — renders a slot for child routes. */
|
|
29
|
+
export const DEFAULT_ROOT_ROUTE: RouteConfig = {
|
|
30
|
+
pattern: '/',
|
|
31
|
+
type: 'page',
|
|
32
|
+
modulePath: '__default_root__',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Synthesize a RouteConfig from matched route data. */
|
|
36
|
+
function toRouteConfig(node: RouteNode, pattern: string): RouteConfig {
|
|
37
|
+
return {
|
|
38
|
+
pattern,
|
|
39
|
+
type: node.redirect ? 'redirect' : 'page',
|
|
40
|
+
modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? '',
|
|
41
|
+
...(node.files ? { files: node.files } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pipeline configuration. */
|
|
46
|
+
export interface PipelineOptions {
|
|
47
|
+
runtime: Runtime;
|
|
48
|
+
contextProvider?: ContextProvider;
|
|
49
|
+
/** Pre-bundled module loaders (browser boot passes these). */
|
|
50
|
+
moduleLoaders?: Record<string, () => Promise<unknown>>;
|
|
51
|
+
logger?: Logger;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class Pipeline {
|
|
55
|
+
private readonly runtime: Runtime;
|
|
56
|
+
readonly contextProvider: ContextProvider | undefined;
|
|
57
|
+
readonly logger: Logger;
|
|
58
|
+
private readonly moduleLoaders: Record<string, () => Promise<unknown>>;
|
|
59
|
+
|
|
60
|
+
constructor(options: PipelineOptions) {
|
|
61
|
+
this.runtime = options.runtime;
|
|
62
|
+
this.contextProvider = options.contextProvider;
|
|
63
|
+
this.logger = options.logger ?? defaultLogger;
|
|
64
|
+
this.moduleLoaders = options.moduleLoaders ?? {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Route resolver (from Runtime) ───────────────────────────────────
|
|
68
|
+
|
|
69
|
+
private async getResolver(): Promise<RouteTrie> {
|
|
70
|
+
const response = await this.runtime.query(ROUTES_MANIFEST_PATH);
|
|
71
|
+
const tree: RouteNode = response.status === 404 ? {} : await response.json();
|
|
72
|
+
return new RouteTrie(tree);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Matching ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async match(url: URL): Promise<MatchedRoute | undefined> {
|
|
78
|
+
const resolver = await this.getResolver();
|
|
79
|
+
const resolved = resolver.match(url.pathname);
|
|
80
|
+
if (resolved) {
|
|
81
|
+
return { route: toRouteConfig(resolved.node, resolved.pattern), params: resolved.params };
|
|
82
|
+
}
|
|
83
|
+
if (url.pathname === '/' || url.pathname === '') {
|
|
84
|
+
return { route: DEFAULT_ROOT_ROUTE, params: {} };
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async findRoute(pattern: string): Promise<RouteConfig | undefined> {
|
|
90
|
+
const resolver = await this.getResolver();
|
|
91
|
+
const node = resolver.findRoute(pattern);
|
|
92
|
+
if (!node) return undefined;
|
|
93
|
+
return toRouteConfig(node, pattern);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async findErrorBoundary(pathname: string): Promise<{ pattern: string; modulePath: string } | undefined> {
|
|
97
|
+
const resolver = await this.getResolver();
|
|
98
|
+
const modulePath = resolver.findErrorBoundary(pathname);
|
|
99
|
+
if (!modulePath) return undefined;
|
|
100
|
+
return { pattern: pathname, modulePath };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getStatusPage(status: number): Promise<RouteConfig | undefined> {
|
|
104
|
+
const resolver = await this.getResolver();
|
|
105
|
+
const node = resolver.findRoute(`/${status}`);
|
|
106
|
+
if (!node) return undefined;
|
|
107
|
+
return toRouteConfig(node, `/${status}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async getErrorHandler(): Promise<RouteConfig | undefined> {
|
|
111
|
+
const resolver = await this.getResolver();
|
|
112
|
+
const modulePath = resolver.findErrorBoundary('/');
|
|
113
|
+
if (!modulePath) return undefined;
|
|
114
|
+
return { pattern: '/', type: 'error', modulePath };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Route hierarchy ────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
buildRouteHierarchy(pattern: string): string[] {
|
|
120
|
+
if (pattern === '/') return ['/'];
|
|
121
|
+
const segments = pattern.split('/').filter(Boolean);
|
|
122
|
+
const hierarchy: string[] = ['/'];
|
|
123
|
+
let current = '';
|
|
124
|
+
for (const segment of segments) {
|
|
125
|
+
current += '/' + segment;
|
|
126
|
+
hierarchy.push(current);
|
|
127
|
+
}
|
|
128
|
+
return hierarchy;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Module loading ─────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
async loadModule<T>(modulePath: string): Promise<T> {
|
|
134
|
+
const loader = this.moduleLoaders[modulePath];
|
|
135
|
+
if (loader) {
|
|
136
|
+
return await loader() as T;
|
|
137
|
+
}
|
|
138
|
+
const abs = modulePath.startsWith('/') ? modulePath : '/' + modulePath;
|
|
139
|
+
return await this.runtime.loadModule(abs) as T;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get inlined `__files` from a loaded module (merged module pattern).
|
|
144
|
+
*/
|
|
145
|
+
getModuleFiles(mod: unknown): FileContents | undefined {
|
|
146
|
+
if (!mod || typeof mod !== 'object') return undefined;
|
|
147
|
+
const files = (mod as Record<string, unknown>).__files;
|
|
148
|
+
if (!files || typeof files !== 'object') return undefined;
|
|
149
|
+
return files as FileContents;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── File loading ───────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
async loadFiles(
|
|
155
|
+
files: { html?: string; md?: string; css?: string },
|
|
156
|
+
): Promise<FileContents> {
|
|
157
|
+
const load = async (path: string): Promise<string | undefined> => {
|
|
158
|
+
const abs = path.startsWith('/') ? path : '/' + path;
|
|
159
|
+
try {
|
|
160
|
+
return await this.runtime.query(abs, { as: 'text' });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.warn(
|
|
163
|
+
`[Pipeline] Failed to load file ${path}:`,
|
|
164
|
+
e instanceof Error ? e.message : e,
|
|
165
|
+
);
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const [html, md, css] = await Promise.all([
|
|
171
|
+
files.html ? load(files.html) : undefined,
|
|
172
|
+
files.md ? load(files.md) : undefined,
|
|
173
|
+
files.css ? load(files.css) : undefined,
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
const result: FileContents = {};
|
|
177
|
+
if (html !== undefined) result.html = html;
|
|
178
|
+
if (md !== undefined) result.md = md;
|
|
179
|
+
if (css !== undefined) result.css = css;
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Context building ───────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
toRouteInfo(matched: MatchedRoute, url: URL): RouteInfo {
|
|
186
|
+
return { url, params: matched.params };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async buildContext(
|
|
190
|
+
routeInfo: RouteInfo,
|
|
191
|
+
route: RouteConfig,
|
|
192
|
+
signal?: AbortSignal,
|
|
193
|
+
isLeaf?: boolean,
|
|
194
|
+
loadedModule?: unknown,
|
|
195
|
+
): Promise<ComponentContext> {
|
|
196
|
+
const rf = route.files;
|
|
197
|
+
|
|
198
|
+
// Try inlined __files from merged module first
|
|
199
|
+
const inlined = loadedModule ? this.getModuleFiles(loadedModule) : undefined;
|
|
200
|
+
|
|
201
|
+
let files: FileContents;
|
|
202
|
+
if (inlined) {
|
|
203
|
+
files = inlined;
|
|
204
|
+
} else if (rf) {
|
|
205
|
+
const filePaths: { html?: string; md?: string; css?: string } = {};
|
|
206
|
+
if (rf.html) filePaths.html = rf.html;
|
|
207
|
+
if (rf.md) filePaths.md = rf.md;
|
|
208
|
+
if (rf.css) filePaths.css = rf.css;
|
|
209
|
+
files = await this.loadFiles(filePaths);
|
|
210
|
+
} else {
|
|
211
|
+
files = {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const base: ComponentContext = {
|
|
215
|
+
...routeInfo,
|
|
216
|
+
pathname: routeInfo.url.pathname,
|
|
217
|
+
searchParams: routeInfo.url.searchParams,
|
|
218
|
+
files,
|
|
219
|
+
...(signal ? { signal } : {}),
|
|
220
|
+
...(isLeaf !== undefined ? { isLeaf } : {}),
|
|
221
|
+
};
|
|
222
|
+
return this.contextProvider ? this.contextProvider(base) : base;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -6,31 +6,27 @@
|
|
|
6
6
|
* Expands <mark-down> tags server-side when a markdown renderer is provided.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { RouteConfig, RouteInfo } from '
|
|
10
|
-
import type {
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
13
|
-
import { DEFAULT_ROOT_ROUTE } from '
|
|
14
|
-
import { escapeHtml, STATUS_MESSAGES, unescapeHtml } from '
|
|
15
|
-
import { resolveWidgetTags } from '
|
|
9
|
+
import type { RouteConfig, RouteInfo } from '../type/route.type.ts';
|
|
10
|
+
import type { MarkdownRenderer } from '../type/markdown.type.ts';
|
|
11
|
+
import type { PageComponent } from '../component/page.component.ts';
|
|
12
|
+
import type { Pipeline } from '../pipeline/pipeline.ts';
|
|
13
|
+
import { DEFAULT_ROOT_ROUTE } from '../pipeline/pipeline.ts';
|
|
14
|
+
import { escapeHtml, STATUS_MESSAGES, unescapeHtml } from '../util/html.util.ts';
|
|
15
|
+
import { resolveWidgetTags } from '../util/widget-resolve.util.ts';
|
|
16
16
|
import { SsrRenderer, type SsrRendererOptions } from './ssr.renderer.ts';
|
|
17
17
|
|
|
18
|
-
/** Options for SSR HTML
|
|
19
|
-
export interface
|
|
20
|
-
/** Markdown renderer for server-side <mark-down> expansion */
|
|
18
|
+
/** Options for SSR HTML Renderer. */
|
|
19
|
+
export interface SsrHtmlRendererOptions extends SsrRendererOptions {
|
|
21
20
|
markdownRenderer?: MarkdownRenderer;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
* SSR HTML Router for server-side rendering.
|
|
26
|
-
*/
|
|
27
|
-
export class SsrHtmlRouter extends SsrRenderer {
|
|
23
|
+
export class SsrHtmlRenderer extends SsrRenderer {
|
|
28
24
|
protected override readonly label = 'SSR HTML';
|
|
29
25
|
private markdownRenderer: MarkdownRenderer | null;
|
|
30
26
|
private markdownReady: Promise<void> | null = null;
|
|
31
27
|
|
|
32
|
-
constructor(
|
|
33
|
-
super(
|
|
28
|
+
constructor(pipeline: Pipeline, options: SsrHtmlRendererOptions = {}) {
|
|
29
|
+
super(pipeline, options);
|
|
34
30
|
this.markdownRenderer = options.markdownRenderer ?? null;
|
|
35
31
|
|
|
36
32
|
if (this.markdownRenderer?.init) {
|
|
@@ -50,9 +46,6 @@ export class SsrHtmlRouter extends SsrRenderer {
|
|
|
50
46
|
return result.replace(/<router-slot[^>]*><\/router-slot>/g, '');
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
/**
|
|
54
|
-
* Render a single route's content.
|
|
55
|
-
*/
|
|
56
49
|
protected override async renderRouteContent(
|
|
57
50
|
routeInfo: RouteInfo,
|
|
58
51
|
route: RouteConfig,
|
|
@@ -66,28 +59,32 @@ export class SsrHtmlRouter extends SsrRenderer {
|
|
|
66
59
|
const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
|
|
67
60
|
let content = rawContent;
|
|
68
61
|
|
|
69
|
-
// Expand <mark-down> tags server-side
|
|
70
62
|
content = await this.expandMarkdown(content);
|
|
71
63
|
|
|
72
|
-
// Attribute bare <router-slot> tags with this route's pattern
|
|
73
|
-
// resolution so widget-internal slots inside <template> are not affected)
|
|
64
|
+
// Attribute bare <router-slot> tags with this route's pattern
|
|
74
65
|
content = this.attributeSlots(content, route.pattern);
|
|
75
66
|
|
|
76
|
-
// Resolve <widget-*> tags
|
|
67
|
+
// Resolve <widget-*> tags
|
|
77
68
|
if (this.widgets) {
|
|
78
69
|
content = await resolveWidgetTags(
|
|
79
70
|
content,
|
|
80
71
|
this.widgets,
|
|
81
72
|
routeInfo,
|
|
82
|
-
(name
|
|
83
|
-
const
|
|
84
|
-
|
|
73
|
+
async (name) => {
|
|
74
|
+
const modulePath = this.widgets!.getModulePath(name);
|
|
75
|
+
if (modulePath) {
|
|
76
|
+
const mod = await this.pipeline.loadModule(modulePath);
|
|
77
|
+
const inlined = this.pipeline.getModuleFiles(mod);
|
|
78
|
+
if (inlined) return inlined;
|
|
79
|
+
}
|
|
80
|
+
return {};
|
|
85
81
|
},
|
|
86
|
-
this.
|
|
82
|
+
this.pipeline.contextProvider,
|
|
83
|
+
this.logger,
|
|
87
84
|
);
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
return { content, ...(title
|
|
87
|
+
return { content, ...(title !== undefined ? { title } : {}) };
|
|
91
88
|
}
|
|
92
89
|
|
|
93
90
|
protected override renderContent(
|
|
@@ -117,7 +114,6 @@ export class SsrHtmlRouter extends SsrRenderer {
|
|
|
117
114
|
`;
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
/** Add pattern attribute to bare <router-slot> tags. */
|
|
121
117
|
private attributeSlots(content: string, routePattern: string): string {
|
|
122
118
|
return content.replace(
|
|
123
119
|
/<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g,
|
|
@@ -125,10 +121,6 @@ export class SsrHtmlRouter extends SsrRenderer {
|
|
|
125
121
|
);
|
|
126
122
|
}
|
|
127
123
|
|
|
128
|
-
/**
|
|
129
|
-
* Expand <mark-down> tags by rendering markdown to HTML server-side.
|
|
130
|
-
* Leaves content unchanged if no markdown renderer is configured.
|
|
131
|
-
*/
|
|
132
124
|
private async expandMarkdown(content: string): Promise<string> {
|
|
133
125
|
if (!this.markdownRenderer) return content;
|
|
134
126
|
if (!content.includes('<mark-down>')) return content;
|
|
@@ -138,24 +130,11 @@ export class SsrHtmlRouter extends SsrRenderer {
|
|
|
138
130
|
}
|
|
139
131
|
|
|
140
132
|
const renderer = this.markdownRenderer;
|
|
141
|
-
|
|
142
|
-
// Match <mark-down>escaped content</mark-down>
|
|
143
133
|
const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
|
|
144
134
|
|
|
145
135
|
return content.replace(pattern, (_match, escaped: string) => {
|
|
146
136
|
const markdown = unescapeHtml(escaped);
|
|
147
|
-
|
|
148
|
-
return rendered;
|
|
137
|
+
return renderer.render(markdown);
|
|
149
138
|
});
|
|
150
139
|
}
|
|
151
140
|
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Create SSR HTML router.
|
|
155
|
-
*/
|
|
156
|
-
export function createSsrHtmlRouter(
|
|
157
|
-
resolver: RouteResolver,
|
|
158
|
-
options?: SsrHtmlRouterOptions,
|
|
159
|
-
): SsrHtmlRouter {
|
|
160
|
-
return new SsrHtmlRouter(resolver, options);
|
|
161
|
-
}
|