@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
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emroute
|
|
3
|
+
*
|
|
4
|
+
* Framework entry point. Reads manifests from Runtime,
|
|
5
|
+
* builds Pipeline + Renderers, handles Request → Response.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { WidgetManifestEntry } from '../type/widget.type.ts';
|
|
9
|
+
import type { WidgetComponent } from '../component/widget.component.ts';
|
|
10
|
+
import type { Runtime } from '../runtime/abstract.runtime.ts';
|
|
11
|
+
import { Pipeline } from '../pipeline/pipeline.ts';
|
|
12
|
+
import { SsrHtmlRenderer } from '../renderer/html.renderer.ts';
|
|
13
|
+
import { SsrMdRenderer } from '../renderer/md.renderer.ts';
|
|
14
|
+
import { WidgetRegistry } from '../widget/widget.registry.ts';
|
|
15
|
+
import { escapeHtml } from '../util/html.util.ts';
|
|
16
|
+
import { rewriteMdLinks } from '../util/md.util.ts';
|
|
17
|
+
import type { RouteNode } from '../type/route-tree.type.ts';
|
|
18
|
+
import type { MarkdownRenderer } from '../type/markdown.type.ts';
|
|
19
|
+
import type { SpaMode } from '../type/widget.type.ts';
|
|
20
|
+
import type { ContextProvider } from '../type/component.type.ts';
|
|
21
|
+
import {
|
|
22
|
+
ROUTES_MANIFEST_PATH,
|
|
23
|
+
WIDGETS_MANIFEST_PATH,
|
|
24
|
+
} from '../runtime/abstract.runtime.ts';
|
|
25
|
+
export const DEFAULT_BASE_PATH = { html: '/html', md: '/md', app: '/app' };
|
|
26
|
+
export type BasePath = Record<keyof typeof DEFAULT_BASE_PATH, string>;
|
|
27
|
+
|
|
28
|
+
export class Emroute {
|
|
29
|
+
readonly htmlRenderer: SsrHtmlRenderer | null;
|
|
30
|
+
readonly mdRenderer: SsrMdRenderer | null;
|
|
31
|
+
readonly shell: string;
|
|
32
|
+
|
|
33
|
+
private constructor(
|
|
34
|
+
htmlRenderer: SsrHtmlRenderer | null,
|
|
35
|
+
mdRenderer: SsrMdRenderer | null,
|
|
36
|
+
shell: string,
|
|
37
|
+
private readonly runtime: Runtime,
|
|
38
|
+
private readonly htmlBase: string,
|
|
39
|
+
private readonly mdBase: string,
|
|
40
|
+
private readonly appBase: string,
|
|
41
|
+
private readonly spa: string,
|
|
42
|
+
private readonly title: string,
|
|
43
|
+
) {
|
|
44
|
+
this.htmlRenderer = htmlRenderer;
|
|
45
|
+
this.mdRenderer = mdRenderer;
|
|
46
|
+
this.shell = shell;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static async create(
|
|
50
|
+
config: {
|
|
51
|
+
routeTree?: RouteNode;
|
|
52
|
+
widgets?: WidgetRegistry;
|
|
53
|
+
spa?: SpaMode;
|
|
54
|
+
basePath?: BasePath;
|
|
55
|
+
title?: string;
|
|
56
|
+
markdownRenderer?: MarkdownRenderer;
|
|
57
|
+
extendContext?: ContextProvider;
|
|
58
|
+
moduleLoaders?: Record<string, () => Promise<unknown>>;
|
|
59
|
+
},
|
|
60
|
+
runtime: Runtime,
|
|
61
|
+
): Promise<Emroute> {
|
|
62
|
+
const { spa = 'root' } = config;
|
|
63
|
+
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
64
|
+
|
|
65
|
+
// ── Verify route manifest exists ──────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
|
|
68
|
+
if (manifestResponse.status === 404 && !config.routeTree) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. ` +
|
|
71
|
+
'Provide routeTree in config or ensure the runtime produces it.',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (config.routeTree && manifestResponse.status === 404) {
|
|
76
|
+
await runtime.command(ROUTES_MANIFEST_PATH, {
|
|
77
|
+
body: JSON.stringify(config.routeTree),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Pipeline ──────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const pipeline = new Pipeline({
|
|
84
|
+
runtime,
|
|
85
|
+
...(config.extendContext ? { contextProvider: config.extendContext } : {}),
|
|
86
|
+
...(config.moduleLoaders ? { moduleLoaders: config.moduleLoaders } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── Widgets ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
let widgets: WidgetRegistry | undefined = config.widgets;
|
|
92
|
+
|
|
93
|
+
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
94
|
+
if (widgetsResponse.status !== 404) {
|
|
95
|
+
const entries: WidgetManifestEntry[] = await widgetsResponse.json();
|
|
96
|
+
if (!config.widgets) {
|
|
97
|
+
widgets = await Emroute.importWidgets(entries, runtime);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Renderers ─────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
let ssrHtmlRenderer: SsrHtmlRenderer | null = null;
|
|
104
|
+
let ssrMdRenderer: SsrMdRenderer | null = null;
|
|
105
|
+
|
|
106
|
+
if (spa !== 'only') {
|
|
107
|
+
ssrHtmlRenderer = new SsrHtmlRenderer(pipeline, {
|
|
108
|
+
...(config.markdownRenderer ? { markdownRenderer: config.markdownRenderer } : {}),
|
|
109
|
+
...(widgets ? { widgets } : {}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
ssrMdRenderer = new SsrMdRenderer(pipeline, {
|
|
113
|
+
...(widgets ? { widgets } : {}),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── HTML shell ────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const title = config.title ?? 'emroute';
|
|
120
|
+
const shellBase = (spa === 'root' || spa === 'only') ? appBase : htmlBase;
|
|
121
|
+
let shell = await Emroute.resolveShell(runtime, title, shellBase);
|
|
122
|
+
|
|
123
|
+
if ((await runtime.query('/main.css')).status !== 404) {
|
|
124
|
+
shell = shell.replace('</head>', ' <link rel="stylesheet" href="/main.css">\n</head>');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return new Emroute(
|
|
128
|
+
ssrHtmlRenderer,
|
|
129
|
+
ssrMdRenderer,
|
|
130
|
+
shell,
|
|
131
|
+
runtime,
|
|
132
|
+
htmlBase,
|
|
133
|
+
mdBase,
|
|
134
|
+
appBase,
|
|
135
|
+
spa,
|
|
136
|
+
title,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── handleRequest ─────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
async handleRequest(req: Request): Promise<Response | null> {
|
|
143
|
+
const url = new URL(req.url);
|
|
144
|
+
const pathname = url.pathname;
|
|
145
|
+
|
|
146
|
+
const mdPrefix = this.mdBase + '/';
|
|
147
|
+
const htmlPrefix = this.htmlBase + '/';
|
|
148
|
+
const appPrefix = this.appBase + '/';
|
|
149
|
+
|
|
150
|
+
// SSR Markdown: /md/*
|
|
151
|
+
if (
|
|
152
|
+
this.mdRenderer &&
|
|
153
|
+
(pathname.startsWith(mdPrefix) || pathname === this.mdBase)
|
|
154
|
+
) {
|
|
155
|
+
const routePath = pathname === this.mdBase ? '/' : pathname.slice(this.mdBase.length);
|
|
156
|
+
if (routePath.length > 1 && routePath.endsWith('/')) {
|
|
157
|
+
const canonical = this.mdBase + routePath.slice(0, -1) + (url.search || '');
|
|
158
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
162
|
+
const { content, status, redirect } = await this.mdRenderer.render(routeUrl, req.signal);
|
|
163
|
+
if (redirect) {
|
|
164
|
+
const target = redirect.startsWith('/') ? this.mdBase + redirect : redirect;
|
|
165
|
+
return Response.redirect(new URL(target, url.origin), status);
|
|
166
|
+
}
|
|
167
|
+
return new Response(rewriteMdLinks(content, this.mdBase, [this.mdBase, this.htmlBase]), {
|
|
168
|
+
status,
|
|
169
|
+
headers: { 'Content-Type': 'text/markdown; charset=utf-8; variant=CommonMark' },
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
173
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// SSR HTML: /html/*
|
|
178
|
+
if (
|
|
179
|
+
this.htmlRenderer &&
|
|
180
|
+
(pathname.startsWith(htmlPrefix) || pathname === this.htmlBase)
|
|
181
|
+
) {
|
|
182
|
+
const routePath = pathname === this.htmlBase ? '/' : pathname.slice(this.htmlBase.length);
|
|
183
|
+
if (routePath.length > 1 && routePath.endsWith('/')) {
|
|
184
|
+
const canonical = this.htmlBase + routePath.slice(0, -1) + (url.search || '');
|
|
185
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
189
|
+
const result = await this.htmlRenderer.render(routeUrl, req.signal);
|
|
190
|
+
if (result.redirect) {
|
|
191
|
+
const target = result.redirect.startsWith('/') ? this.htmlBase + result.redirect : result.redirect;
|
|
192
|
+
return Response.redirect(new URL(target, url.origin), result.status);
|
|
193
|
+
}
|
|
194
|
+
const ssrTitle = result.title ?? this.title;
|
|
195
|
+
const html = Emroute.injectSsrContent(this.shell, result.content, ssrTitle, pathname);
|
|
196
|
+
return new Response(html, {
|
|
197
|
+
status: result.status,
|
|
198
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
199
|
+
});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
202
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// /app/*
|
|
207
|
+
if (pathname.startsWith(appPrefix) || pathname === this.appBase) {
|
|
208
|
+
return new Response(this.shell, {
|
|
209
|
+
status: 200,
|
|
210
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Unhandled SSR paths in 'only' mode — serve shell
|
|
215
|
+
if (
|
|
216
|
+
pathname.startsWith(htmlPrefix) || pathname === this.htmlBase ||
|
|
217
|
+
pathname.startsWith(mdPrefix) || pathname === this.mdBase
|
|
218
|
+
) {
|
|
219
|
+
return new Response(this.shell, {
|
|
220
|
+
status: 200,
|
|
221
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Static files
|
|
226
|
+
const lastSegment = pathname.split('/').pop() ?? '';
|
|
227
|
+
if (lastSegment.includes('.')) {
|
|
228
|
+
const fileResponse = await this.runtime.handle(pathname);
|
|
229
|
+
if (fileResponse.status === 200) return fileResponse;
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Bare paths → redirect
|
|
234
|
+
const base = (this.spa === 'root' || this.spa === 'only') ? this.appBase : this.htmlBase;
|
|
235
|
+
const bare = pathname === '/' ? '' : pathname.slice(1).replace(/\/$/, '');
|
|
236
|
+
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Private static helpers ────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
private static extractWidgetExport(
|
|
242
|
+
mod: Record<string, unknown>,
|
|
243
|
+
): WidgetComponent | null {
|
|
244
|
+
for (const value of Object.values(mod)) {
|
|
245
|
+
if (!value) continue;
|
|
246
|
+
if (typeof value === 'object' && 'getData' in value) {
|
|
247
|
+
return value as WidgetComponent;
|
|
248
|
+
}
|
|
249
|
+
if (typeof value === 'function' && value.prototype?.getData) {
|
|
250
|
+
return new (value as new () => WidgetComponent)();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private static async importWidgets(
|
|
257
|
+
entries: WidgetManifestEntry[],
|
|
258
|
+
runtime: Runtime,
|
|
259
|
+
): Promise<WidgetRegistry> {
|
|
260
|
+
const registry = new WidgetRegistry();
|
|
261
|
+
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
try {
|
|
264
|
+
const runtimePath = entry.modulePath.startsWith('/')
|
|
265
|
+
? entry.modulePath
|
|
266
|
+
: `/${entry.modulePath}`;
|
|
267
|
+
|
|
268
|
+
const mod = await runtime.loadModule(runtimePath) as Record<string, unknown>;
|
|
269
|
+
const instance = Emroute.extractWidgetExport(mod);
|
|
270
|
+
if (!instance) continue;
|
|
271
|
+
registry.add(instance, runtimePath);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return registry;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private static buildHtmlShell(title: string, htmlBase: string): string {
|
|
281
|
+
const baseTag = htmlBase ? `\n <base href="${escapeHtml(htmlBase)}/">` : '';
|
|
282
|
+
return `<!DOCTYPE html>
|
|
283
|
+
<html>
|
|
284
|
+
<head>${baseTag}
|
|
285
|
+
<meta charset="utf-8">
|
|
286
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
287
|
+
<title>${escapeHtml(title)}</title>
|
|
288
|
+
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
289
|
+
</head>
|
|
290
|
+
<body>
|
|
291
|
+
<router-slot></router-slot>
|
|
292
|
+
</body>
|
|
293
|
+
</html>`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private static injectSsrContent(
|
|
297
|
+
html: string,
|
|
298
|
+
content: string,
|
|
299
|
+
title: string | undefined,
|
|
300
|
+
ssrRoute?: string,
|
|
301
|
+
): string {
|
|
302
|
+
const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
|
|
303
|
+
if (!slotPattern.test(html)) return html;
|
|
304
|
+
|
|
305
|
+
const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : '';
|
|
306
|
+
html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
|
|
307
|
+
|
|
308
|
+
if (title) {
|
|
309
|
+
html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return html;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private static async resolveShell(
|
|
316
|
+
runtime: Runtime,
|
|
317
|
+
title: string,
|
|
318
|
+
htmlBase: string,
|
|
319
|
+
): Promise<string> {
|
|
320
|
+
const response = await runtime.query('/index.html');
|
|
321
|
+
if (response.status !== 404) return await response.text();
|
|
322
|
+
return Emroute.buildHtmlShell(title, htmlBase);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Types
|
|
3
|
+
*
|
|
4
|
+
* Types for the component system. Separate from route types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RouteInfo } from './route.type.ts';
|
|
8
|
+
|
|
9
|
+
/** Shape of companion file contents (html, md, css). */
|
|
10
|
+
export type FileContents = { html?: string; md?: string; css?: string };
|
|
11
|
+
|
|
12
|
+
/** Context passed to components during rendering. */
|
|
13
|
+
export interface ComponentContext extends RouteInfo {
|
|
14
|
+
/** @deprecated Use context.url.pathname */
|
|
15
|
+
readonly pathname: string;
|
|
16
|
+
/** @deprecated Use context.url.searchParams */
|
|
17
|
+
readonly searchParams: URLSearchParams;
|
|
18
|
+
readonly files?: Readonly<FileContents>;
|
|
19
|
+
readonly signal?: AbortSignal;
|
|
20
|
+
readonly isLeaf?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Enriches the base ComponentContext with app-level services.
|
|
25
|
+
* Registered once at server creation; called for every context construction.
|
|
26
|
+
*/
|
|
27
|
+
export type ContextProvider = (base: ComponentContext) => ComponentContext;
|
|
28
|
+
|
|
29
|
+
/** Render context determines how components are rendered. */
|
|
30
|
+
export type RenderContext = 'markdown' | 'html' | 'spa';
|
|
31
|
+
|
|
32
|
+
/** Component manifest entry for code generation. */
|
|
33
|
+
export interface ComponentManifestEntry {
|
|
34
|
+
name: string;
|
|
35
|
+
modulePath: string;
|
|
36
|
+
tagName: string;
|
|
37
|
+
type: 'page' | 'widget';
|
|
38
|
+
pattern?: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Interface
|
|
3
|
+
*
|
|
4
|
+
* Minimal pluggable logger. Default: no-op (silent degradation).
|
|
5
|
+
* Pass via PipelineOptions to wire in.
|
|
6
|
+
*/
|
|
7
|
+
export interface Logger {
|
|
8
|
+
error(msg: string, error?: Error): void;
|
|
9
|
+
warn(msg: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const noop = () => {};
|
|
13
|
+
|
|
14
|
+
/** Default no-op logger. */
|
|
15
|
+
export const defaultLogger: Logger = { error: noop, warn: noop };
|
|
16
|
+
|
|
17
|
+
/** @deprecated Pass `logger` in Emroute.create() config instead. This function is a no-op. */
|
|
18
|
+
export function setLogger(_logger: Logger): void {
|
|
19
|
+
console.warn('[emroute] setLogger() is deprecated. Pass `logger` in Emroute.create() config instead.');
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Tree
|
|
3
|
+
*
|
|
4
|
+
* Serializable tree structure that mirrors the filesystem layout.
|
|
5
|
+
*
|
|
6
|
+
* Each node corresponds to a URL segment. The tree is JSON-serializable
|
|
7
|
+
* (no Maps, no classes) so it can be written to disk, sent over the wire,
|
|
8
|
+
* or used directly as the in-memory trie for O(depth) route matching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Files associated with a route (companion files discovered alongside the page). */
|
|
12
|
+
export interface RouteFiles {
|
|
13
|
+
ts?: string;
|
|
14
|
+
js?: string;
|
|
15
|
+
html?: string;
|
|
16
|
+
md?: string;
|
|
17
|
+
css?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A single node in the route tree. */
|
|
21
|
+
export interface RouteNode {
|
|
22
|
+
files?: RouteFiles;
|
|
23
|
+
errorBoundary?: string;
|
|
24
|
+
redirect?: string;
|
|
25
|
+
children?: Record<string, RouteNode>;
|
|
26
|
+
dynamic?: { param: string; child: RouteNode };
|
|
27
|
+
wildcard?: { param: string; child: RouteNode };
|
|
28
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Types
|
|
3
|
+
*
|
|
4
|
+
* Pure routing types. No rendering, no navigation, no browser concerns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { RouteNode, RouteFiles } from './route-tree.type.ts';
|
|
8
|
+
|
|
9
|
+
/** Parameters extracted from URL patterns. */
|
|
10
|
+
export type RouteParams = Readonly<Record<string, string>>;
|
|
11
|
+
|
|
12
|
+
/** Immutable route context built once per navigation, shared across the render pipeline. */
|
|
13
|
+
export interface RouteInfo {
|
|
14
|
+
readonly url: URL;
|
|
15
|
+
readonly params: RouteParams;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Supported file patterns in file-based routing. */
|
|
19
|
+
export type RouteFileType = 'page' | 'error' | 'redirect';
|
|
20
|
+
|
|
21
|
+
/** Redirect configuration. */
|
|
22
|
+
export interface RedirectConfig {
|
|
23
|
+
to: string;
|
|
24
|
+
status: 301 | 302;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Route configuration for a single matched route. */
|
|
28
|
+
export interface RouteConfig {
|
|
29
|
+
pattern: string;
|
|
30
|
+
type: RouteFileType;
|
|
31
|
+
modulePath: string;
|
|
32
|
+
files?: import('./route-tree.type.ts').RouteFiles;
|
|
33
|
+
parent?: string;
|
|
34
|
+
statusCode?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Result of matching a URL against routes. */
|
|
38
|
+
export interface MatchedRoute {
|
|
39
|
+
readonly route: RouteConfig;
|
|
40
|
+
readonly params: RouteParams;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Error boundary configuration. */
|
|
44
|
+
export interface ErrorBoundary {
|
|
45
|
+
pattern: string;
|
|
46
|
+
modulePath: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Router state for history management. */
|
|
50
|
+
export interface RouterState {
|
|
51
|
+
pathname: string;
|
|
52
|
+
params: RouteParams;
|
|
53
|
+
scrollY?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Navigation options. */
|
|
57
|
+
export interface NavigateOptions {
|
|
58
|
+
replace?: boolean;
|
|
59
|
+
state?: RouterState;
|
|
60
|
+
hash?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Router event types. */
|
|
64
|
+
export type RouterEventType = 'navigate' | 'error' | 'load';
|
|
65
|
+
|
|
66
|
+
/** Router event payload. */
|
|
67
|
+
export interface RouterEvent {
|
|
68
|
+
type: RouterEventType;
|
|
69
|
+
pathname: string;
|
|
70
|
+
params: RouteParams;
|
|
71
|
+
error?: Error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Router event listener. */
|
|
75
|
+
export type RouterEventListener = (event: RouterEvent) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** SPA rendering mode. */
|
|
6
|
+
export type SpaMode = 'none' | 'leaf' | 'root' | 'only';
|
|
7
|
+
|
|
8
|
+
/** Widget manifest entry for discovery and registration. */
|
|
9
|
+
export interface WidgetManifestEntry {
|
|
10
|
+
name: string;
|
|
11
|
+
modulePath: string;
|
|
12
|
+
tagName: string;
|
|
13
|
+
files?: { html?: string; md?: string; css?: string };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Full widgets manifest (array, sorted by name). */
|
|
17
|
+
export type WidgetsManifest = WidgetManifestEntry[];
|
|
18
|
+
|
|
19
|
+
/** Parsed widget block from markdown fenced code. */
|
|
20
|
+
export interface ParsedWidgetBlock {
|
|
21
|
+
fullMatch: string;
|
|
22
|
+
widgetName: string;
|
|
23
|
+
params: Record<string, unknown> | null;
|
|
24
|
+
parseError?: string;
|
|
25
|
+
startIndex: number;
|
|
26
|
+
endIndex: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure HTML utilities. No DOM, no browser APIs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** HTML attribute name marking a widget as server-rendered. */
|
|
6
|
+
export const SSR_ATTR = 'ssr';
|
|
7
|
+
|
|
8
|
+
/** HTML attribute name for lazy-loading widgets. */
|
|
9
|
+
export const LAZY_ATTR = 'lazy';
|
|
10
|
+
|
|
11
|
+
const BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
|
12
|
+
|
|
13
|
+
/** Throw if a redirect URL uses a dangerous protocol. */
|
|
14
|
+
export function assertSafeRedirect(url: string): void {
|
|
15
|
+
if (BLOCKED_PROTOCOLS.test(url.trim())) {
|
|
16
|
+
throw new Error(`Unsafe redirect URL blocked: ${url}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function escapeHtml(text: string): string {
|
|
21
|
+
return text
|
|
22
|
+
.replaceAll('&', '&')
|
|
23
|
+
.replaceAll('<', '<')
|
|
24
|
+
.replaceAll('>', '>')
|
|
25
|
+
.replaceAll('"', '"')
|
|
26
|
+
.replaceAll("'", ''')
|
|
27
|
+
.replaceAll('`', '`');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function unescapeHtml(text: string): string {
|
|
31
|
+
return text
|
|
32
|
+
.replaceAll('`', '`')
|
|
33
|
+
.replaceAll(''', "'")
|
|
34
|
+
.replaceAll('"', '"')
|
|
35
|
+
.replaceAll('>', '>')
|
|
36
|
+
.replaceAll('<', '<')
|
|
37
|
+
.replaceAll('&', '&');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function scopeWidgetCss(css: string, widgetName: string): string {
|
|
41
|
+
return `@scope (widget-${widgetName}) {\n${css}\n}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Status code to message mapping. */
|
|
45
|
+
export const STATUS_MESSAGES: Record<number, string> = {
|
|
46
|
+
401: 'Unauthorized',
|
|
47
|
+
403: 'Forbidden',
|
|
48
|
+
404: 'Not Found',
|
|
49
|
+
500: 'Internal Server Error',
|
|
50
|
+
};
|
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
* Skips fenced code blocks and links already under a known base path.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/** Rewrite internal absolute links in markdown to include the base path prefix. */
|
|
9
8
|
export function rewriteMdLinks(markdown: string, base: string, skipPrefixes: string[]): string {
|
|
10
9
|
const prefix = base + '/';
|
|
11
|
-
// Negative lookahead: skip links already under a known base path
|
|
12
10
|
const skip = skipPrefixes.map((p) => p.slice(1) + '/').join('|');
|
|
13
11
|
const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, 'g');
|
|
14
12
|
const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, 'g');
|
|
@@ -17,14 +15,14 @@ export function rewriteMdLinks(markdown: string, base: string, skipPrefixes: str
|
|
|
17
15
|
let inCodeBlock = false;
|
|
18
16
|
|
|
19
17
|
for (let i = 0; i < lines.length; i++) {
|
|
20
|
-
if (lines[i]
|
|
18
|
+
if (lines[i]!.startsWith('```')) {
|
|
21
19
|
inCodeBlock = !inCodeBlock;
|
|
22
20
|
continue;
|
|
23
21
|
}
|
|
24
22
|
if (inCodeBlock) continue;
|
|
25
23
|
|
|
26
|
-
lines[i] = lines[i]
|
|
27
|
-
lines[i] = lines[i]
|
|
24
|
+
lines[i] = lines[i]!.replaceAll(inlineRe, `](${prefix}`);
|
|
25
|
+
lines[i] = lines[i]!.replaceAll(refRe, `$1${prefix}`);
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
return lines.join('\n');
|
|
@@ -17,7 +17,6 @@ import type { RouteNode } from '../type/route-tree.type.ts';
|
|
|
17
17
|
export function resolveTargetNode(node: RouteNode, name: string, isRoot: boolean): RouteNode {
|
|
18
18
|
if (name === 'index') {
|
|
19
19
|
if (isRoot) return node;
|
|
20
|
-
// Non-root index → wildcard catch-all
|
|
21
20
|
node.wildcard ??= { param: 'rest', child: {} };
|
|
22
21
|
return node.wildcard.child;
|
|
23
22
|
}
|
|
@@ -28,7 +27,6 @@ export function resolveTargetNode(node: RouteNode, name: string, isRoot: boolean
|
|
|
28
27
|
return node.dynamic.child;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
// Static segment
|
|
32
30
|
node.children ??= {};
|
|
33
31
|
node.children[name] ??= {};
|
|
34
32
|
return node.children[name];
|