@emkodev/emroute 1.7.2 → 1.8.0-beta.1
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 +19 -45
- package/{src/renderer/ssr → core/renderer}/md.renderer.ts +21 -40
- package/{src/renderer/ssr → core/renderer}/ssr.renderer.ts +42 -53
- 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 +346 -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 +0 -2
- package/{src/route → core/util}/route-tree.util.ts +0 -2
- package/{src → core}/util/widget-resolve.util.ts +15 -45
- package/{src → core}/widget/widget.parser.ts +0 -21
- package/core/widget/widget.registry.ts +24 -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 +12 -33
- 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 +14 -31
- package/dist/core/renderer/md.renderer.js.map +1 -0
- package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.d.ts +11 -17
- package/dist/{src/renderer/ssr → core/renderer}/ssr.renderer.js +34 -36
- 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 +261 -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 +32 -0
- package/dist/{src → core}/util/widget-resolve.util.js +11 -41
- 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 +0 -21
- package/dist/core/widget/widget.parser.js.map +1 -0
- package/dist/core/widget/widget.registry.d.ts +13 -0
- package/dist/core/widget/widget.registry.js +19 -0
- package/dist/core/widget/widget.registry.js.map +1 -0
- package/dist/emroute.js +1137 -1151
- package/dist/emroute.js.map +36 -5
- package/dist/runtime/abstract.runtime.d.ts +50 -5
- package/dist/runtime/abstract.runtime.js +446 -6
- 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 +18 -2
- 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 +11 -1
- 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 +18 -2
- 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 +45 -36
- 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 -341
- 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 -68
- 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 +4 -7
- package/dist/src/element/component.element.js +23 -22
- 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 +4 -3
- package/dist/src/element/markdown.element.js.map +1 -1
- package/dist/src/index.d.ts +15 -13
- 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 +34 -12
- package/dist/src/renderer/spa/thin-client.js.map +1 -1
- package/dist/src/route/route.core.d.ts +3 -3
- package/dist/src/route/route.core.js +21 -7
- package/dist/src/route/route.core.js.map +1 -1
- 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 +483 -8
- package/runtime/bun/fs/bun-fs.runtime.ts +17 -1
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +11 -0
- package/runtime/fetch.runtime.ts +3 -3
- package/runtime/sitemap.generator.ts +2 -2
- package/runtime/universal/fs/universal-fs.runtime.ts +17 -1
- package/server/build.util.ts +53 -47
- package/server/codegen.util.ts +1 -1
- package/server/emroute.server.ts +12 -412
- package/src/element/component.element.ts +24 -31
- package/src/element/markdown.element.ts +5 -4
- package/src/index.ts +22 -18
- package/src/renderer/spa/{thin-client.ts → emroute.app.ts} +46 -22
- 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/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 -97
- package/src/component/abstract.component.ts +0 -231
- package/src/component/widget.component.ts +0 -85
- package/src/route/route.core.ts +0 -362
- package/src/route/route.trie.ts +0 -265
- 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/dist/emroute.js
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
// dist/
|
|
1
|
+
// dist/core/util/html.util.js
|
|
2
2
|
var SSR_ATTR = "ssr";
|
|
3
3
|
var LAZY_ATTR = "lazy";
|
|
4
|
-
var
|
|
4
|
+
var BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
|
5
|
+
function assertSafeRedirect(url) {
|
|
6
|
+
if (BLOCKED_PROTOCOLS.test(url.trim())) {
|
|
7
|
+
throw new Error(`Unsafe redirect URL blocked: ${url}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function escapeHtml(text) {
|
|
11
|
+
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'").replaceAll("`", "`");
|
|
12
|
+
}
|
|
13
|
+
function unescapeHtml(text) {
|
|
14
|
+
return text.replaceAll("`", "`").replaceAll("'", "'").replaceAll(""", '"').replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
15
|
+
}
|
|
16
|
+
function scopeWidgetCss(css, widgetName) {
|
|
17
|
+
return `@scope (widget-${widgetName}) {
|
|
18
|
+
${css}
|
|
19
|
+
}`;
|
|
20
|
+
}
|
|
21
|
+
var STATUS_MESSAGES = {
|
|
22
|
+
401: "Unauthorized",
|
|
23
|
+
403: "Forbidden",
|
|
24
|
+
404: "Not Found",
|
|
25
|
+
500: "Internal Server Error"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// dist/src/util/html.util.js
|
|
29
|
+
class SsrShadowRoot {
|
|
5
30
|
host;
|
|
6
31
|
_innerHTML = "";
|
|
7
32
|
constructor(host) {
|
|
@@ -16,8 +41,7 @@ var SsrShadowRoot = class {
|
|
|
16
41
|
setHTMLUnsafe(html, _options) {
|
|
17
42
|
this._innerHTML = html;
|
|
18
43
|
}
|
|
19
|
-
append(..._nodes) {
|
|
20
|
-
}
|
|
44
|
+
append(..._nodes) {}
|
|
21
45
|
querySelector(_selector) {
|
|
22
46
|
return null;
|
|
23
47
|
}
|
|
@@ -30,12 +54,12 @@ var SsrShadowRoot = class {
|
|
|
30
54
|
get firstChild() {
|
|
31
55
|
return null;
|
|
32
56
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class SsrHTMLElement {
|
|
35
60
|
_innerHTML = "";
|
|
36
61
|
_shadowRoot = null;
|
|
37
|
-
_attributes =
|
|
38
|
-
// Accept any CSS property assignment without error
|
|
62
|
+
_attributes = new Map;
|
|
39
63
|
style = new Proxy({}, {
|
|
40
64
|
set(_target, _prop, _value) {
|
|
41
65
|
return true;
|
|
@@ -43,7 +67,7 @@ var SsrHTMLElement = class {
|
|
|
43
67
|
get(_target, prop) {
|
|
44
68
|
if (typeof prop === "string")
|
|
45
69
|
return "";
|
|
46
|
-
return
|
|
70
|
+
return;
|
|
47
71
|
}
|
|
48
72
|
});
|
|
49
73
|
get innerHTML() {
|
|
@@ -90,73 +114,41 @@ var SsrHTMLElement = class {
|
|
|
90
114
|
querySelectorAll(_selector) {
|
|
91
115
|
return [];
|
|
92
116
|
}
|
|
93
|
-
append(..._nodes) {
|
|
94
|
-
}
|
|
117
|
+
append(..._nodes) {}
|
|
95
118
|
appendChild(node) {
|
|
96
119
|
return node;
|
|
97
120
|
}
|
|
98
|
-
};
|
|
99
|
-
var HTMLElementBase = globalThis.HTMLElement ?? SsrHTMLElement;
|
|
100
|
-
function escapeHtml(text) {
|
|
101
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'").replaceAll("`", "`");
|
|
102
|
-
}
|
|
103
|
-
function unescapeHtml(text) {
|
|
104
|
-
return text.replaceAll("`", "`").replaceAll("'", "'").replaceAll(""", '"').replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
105
|
-
}
|
|
106
|
-
function scopeWidgetCss(css, widgetName) {
|
|
107
|
-
return `@scope (widget-${widgetName}) {
|
|
108
|
-
${css}
|
|
109
|
-
}`;
|
|
110
121
|
}
|
|
111
|
-
var
|
|
112
|
-
401: "Unauthorized",
|
|
113
|
-
403: "Forbidden",
|
|
114
|
-
404: "Not Found",
|
|
115
|
-
500: "Internal Server Error"
|
|
116
|
-
};
|
|
122
|
+
var HTMLElementBase = globalThis.HTMLElement ?? SsrHTMLElement;
|
|
117
123
|
|
|
118
124
|
// dist/src/element/slot.element.js
|
|
119
|
-
|
|
120
|
-
}
|
|
125
|
+
class RouterSlot extends HTMLElementBase {
|
|
126
|
+
}
|
|
121
127
|
|
|
122
128
|
// dist/src/element/markdown.element.js
|
|
123
|
-
|
|
129
|
+
class MarkdownElement extends HTMLElementBase {
|
|
124
130
|
static renderer = null;
|
|
125
131
|
static rendererInitPromise = null;
|
|
126
132
|
abortController = null;
|
|
127
|
-
/**
|
|
128
|
-
* Set the markdown renderer.
|
|
129
|
-
* Must be called before any <mark-down> elements are connected.
|
|
130
|
-
*
|
|
131
|
-
* @example
|
|
132
|
-
* ```ts
|
|
133
|
-
* import { createEmkoRenderer } from './emko.renderer.ts';
|
|
134
|
-
* MarkdownElement.setRenderer(await createEmkoRenderer());
|
|
135
|
-
* ```
|
|
136
|
-
*/
|
|
137
133
|
static setRenderer(renderer) {
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
MarkdownElement.renderer = renderer;
|
|
135
|
+
MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
|
|
140
136
|
}
|
|
141
|
-
/** Get the current renderer (if set). Used by bootEmrouteApp to pass through to createEmrouteServer. */
|
|
142
137
|
static getConfiguredRenderer() {
|
|
143
|
-
return
|
|
138
|
+
return MarkdownElement.renderer;
|
|
144
139
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Get the current renderer, waiting for init if needed.
|
|
147
|
-
*/
|
|
148
140
|
static async getRenderer() {
|
|
149
|
-
const renderer =
|
|
141
|
+
const renderer = MarkdownElement.renderer;
|
|
150
142
|
if (!renderer) {
|
|
151
143
|
throw new Error("No markdown renderer configured. Call MarkdownElement.setRenderer() before using <mark-down> elements.");
|
|
152
144
|
}
|
|
153
|
-
if (
|
|
154
|
-
await
|
|
145
|
+
if (MarkdownElement.rendererInitPromise) {
|
|
146
|
+
await MarkdownElement.rendererInitPromise;
|
|
155
147
|
}
|
|
156
148
|
return renderer;
|
|
157
149
|
}
|
|
158
150
|
async connectedCallback() {
|
|
159
|
-
this.abortController = new AbortController
|
|
151
|
+
this.abortController = new AbortController;
|
|
160
152
|
await this.loadContent();
|
|
161
153
|
}
|
|
162
154
|
disconnectedCallback() {
|
|
@@ -177,7 +169,7 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
|
177
169
|
async loadFromSrc(src) {
|
|
178
170
|
const signal = this.abortController?.signal;
|
|
179
171
|
try {
|
|
180
|
-
const response = await fetch(src, { signal });
|
|
172
|
+
const response = await fetch(src, signal ? { signal } : {});
|
|
181
173
|
if (!response.ok) {
|
|
182
174
|
throw new Error(`Failed to fetch ${src}: ${response.status}`);
|
|
183
175
|
}
|
|
@@ -192,7 +184,7 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
|
192
184
|
}
|
|
193
185
|
async renderContent(markdown) {
|
|
194
186
|
try {
|
|
195
|
-
const renderer = await
|
|
187
|
+
const renderer = await MarkdownElement.getRenderer();
|
|
196
188
|
this.innerHTML = renderer.render(markdown);
|
|
197
189
|
} catch (error) {
|
|
198
190
|
this.showError(error);
|
|
@@ -202,21 +194,26 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
|
202
194
|
const message = error instanceof Error ? error.message : String(error);
|
|
203
195
|
this.innerHTML = `<div>Markdown Error: ${escapeHtml(message)}</div>`;
|
|
204
196
|
}
|
|
205
|
-
}
|
|
197
|
+
}
|
|
206
198
|
|
|
207
199
|
// dist/src/element/component.element.js
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
200
|
+
function filterUndefined(obj) {
|
|
201
|
+
const result = {};
|
|
202
|
+
let hasValue = false;
|
|
203
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
204
|
+
if (v !== undefined) {
|
|
205
|
+
result[k] = v;
|
|
206
|
+
hasValue = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return hasValue ? result : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class ComponentElement extends HTMLElementBase {
|
|
213
|
+
static lazyLoaders = new Map;
|
|
216
214
|
static extendContext;
|
|
217
|
-
/** Register (or clear) the context provider that enriches every widget's ComponentContext. */
|
|
218
215
|
static setContextProvider(provider) {
|
|
219
|
-
|
|
216
|
+
ComponentElement.extendContext = provider;
|
|
220
217
|
}
|
|
221
218
|
component;
|
|
222
219
|
effectiveFiles;
|
|
@@ -228,7 +225,6 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
228
225
|
deferred = null;
|
|
229
226
|
abortController = null;
|
|
230
227
|
intersectionObserver = null;
|
|
231
|
-
/** Promise that resolves with fetched data (available after loadData starts) */
|
|
232
228
|
dataPromise = null;
|
|
233
229
|
constructor(component, files) {
|
|
234
230
|
super();
|
|
@@ -238,52 +234,36 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
238
234
|
this.attachShadow({ mode: "open" });
|
|
239
235
|
}
|
|
240
236
|
}
|
|
241
|
-
/**
|
|
242
|
-
* Register a widget as a custom element: `widget-{name}`.
|
|
243
|
-
* Creates a fresh widget instance per DOM element (per-element state).
|
|
244
|
-
* Optional `files` parameter provides discovered file paths without mutating
|
|
245
|
-
* the component instance.
|
|
246
|
-
*/
|
|
247
237
|
static register(component, files) {
|
|
248
238
|
const tagName = `widget-${component.name}`;
|
|
249
239
|
if (!globalThis.customElements || customElements.get(tagName)) {
|
|
250
240
|
return;
|
|
251
241
|
}
|
|
252
242
|
const WidgetClass = component.constructor;
|
|
253
|
-
const BoundElement = class extends
|
|
243
|
+
const BoundElement = class extends ComponentElement {
|
|
254
244
|
constructor() {
|
|
255
|
-
super(new WidgetClass
|
|
245
|
+
super(new WidgetClass, files);
|
|
256
246
|
}
|
|
257
247
|
};
|
|
258
248
|
customElements.define(tagName, BoundElement);
|
|
259
249
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Register a widget class (not instance) as a custom element: `widget-{name}`.
|
|
262
|
-
* Used for manifest-based registration where classes are loaded dynamically.
|
|
263
|
-
*/
|
|
264
250
|
static registerClass(WidgetClass, name, files) {
|
|
265
251
|
const tagName = `widget-${name}`;
|
|
266
252
|
if (!globalThis.customElements || customElements.get(tagName)) {
|
|
267
253
|
return;
|
|
268
254
|
}
|
|
269
|
-
const BoundElement = class extends
|
|
255
|
+
const BoundElement = class extends ComponentElement {
|
|
270
256
|
constructor() {
|
|
271
|
-
super(new WidgetClass
|
|
257
|
+
super(new WidgetClass, files);
|
|
272
258
|
}
|
|
273
259
|
};
|
|
274
260
|
customElements.define(tagName, BoundElement);
|
|
275
261
|
}
|
|
276
|
-
/**
|
|
277
|
-
* Register a widget lazily: define the custom element immediately (so SSR
|
|
278
|
-
* content via Declarative Shadow DOM is adopted), but defer loading the
|
|
279
|
-
* module until connectedCallback fires. Once loaded, the real component
|
|
280
|
-
* replaces the placeholder and hydration proceeds normally.
|
|
281
|
-
*/
|
|
282
262
|
static registerLazy(name, files, loader) {
|
|
283
263
|
const tagName = `widget-${name}`;
|
|
284
264
|
if (!globalThis.customElements || customElements.get(tagName))
|
|
285
265
|
return;
|
|
286
|
-
|
|
266
|
+
ComponentElement.lazyLoaders.set(tagName, loader);
|
|
287
267
|
const placeholder = {
|
|
288
268
|
name,
|
|
289
269
|
getData: () => Promise.resolve(null),
|
|
@@ -292,17 +272,13 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
292
272
|
renderError: () => "",
|
|
293
273
|
renderMarkdownError: () => ""
|
|
294
274
|
};
|
|
295
|
-
const BoundElement = class extends
|
|
275
|
+
const BoundElement = class extends ComponentElement {
|
|
296
276
|
constructor() {
|
|
297
277
|
super(placeholder, files);
|
|
298
278
|
}
|
|
299
279
|
};
|
|
300
280
|
customElements.define(tagName, BoundElement);
|
|
301
281
|
}
|
|
302
|
-
/**
|
|
303
|
-
* Promise that resolves when component is ready (data loaded and rendered).
|
|
304
|
-
* Used by router to wait for async components.
|
|
305
|
-
*/
|
|
306
282
|
get ready() {
|
|
307
283
|
if (this.state === "ready") {
|
|
308
284
|
return Promise.resolve();
|
|
@@ -312,23 +288,18 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
312
288
|
}
|
|
313
289
|
async connectedCallback() {
|
|
314
290
|
const tagName = this.tagName.toLowerCase();
|
|
315
|
-
const lazyLoader =
|
|
291
|
+
const lazyLoader = ComponentElement.lazyLoaders.get(tagName);
|
|
316
292
|
if (lazyLoader) {
|
|
317
293
|
try {
|
|
318
|
-
|
|
319
|
-
if (!modulePromise) {
|
|
320
|
-
modulePromise = lazyLoader();
|
|
321
|
-
_ComponentElement.lazyModules.set(tagName, modulePromise);
|
|
322
|
-
}
|
|
323
|
-
const mod = await modulePromise;
|
|
294
|
+
const mod = await lazyLoader();
|
|
324
295
|
for (const exp of Object.values(mod)) {
|
|
325
296
|
if (exp && typeof exp === "object" && "getData" in exp) {
|
|
326
297
|
const WidgetClass = exp.constructor;
|
|
327
|
-
this.component = new WidgetClass
|
|
298
|
+
this.component = new WidgetClass;
|
|
328
299
|
break;
|
|
329
300
|
}
|
|
330
301
|
if (typeof exp === "function" && exp.prototype?.getData) {
|
|
331
|
-
this.component = new exp
|
|
302
|
+
this.component = new exp;
|
|
332
303
|
break;
|
|
333
304
|
}
|
|
334
305
|
}
|
|
@@ -342,7 +313,7 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
342
313
|
}
|
|
343
314
|
this.component.element = this;
|
|
344
315
|
this.style.contentVisibility = "auto";
|
|
345
|
-
this.abortController = new AbortController
|
|
316
|
+
this.abortController = new AbortController;
|
|
346
317
|
const signal = this.abortController.signal;
|
|
347
318
|
const params = {};
|
|
348
319
|
for (const attr of this.attributes) {
|
|
@@ -367,22 +338,22 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
367
338
|
if (signal.aborted)
|
|
368
339
|
return;
|
|
369
340
|
const currentUrl = globalThis.location ? new URL(location.href) : new URL("http://localhost/");
|
|
341
|
+
const filteredFiles = filterUndefined(files);
|
|
370
342
|
const base = {
|
|
371
343
|
url: currentUrl,
|
|
372
344
|
pathname: currentUrl.pathname,
|
|
373
345
|
searchParams: currentUrl.searchParams,
|
|
374
346
|
params: this.params ?? {},
|
|
375
|
-
|
|
347
|
+
...filteredFiles ? { files: filteredFiles } : {}
|
|
376
348
|
};
|
|
377
|
-
this.context =
|
|
349
|
+
this.context = ComponentElement.extendContext ? ComponentElement.extendContext(base) : base;
|
|
378
350
|
if (this.hasAttribute(SSR_ATTR)) {
|
|
379
351
|
this.removeAttribute(SSR_ATTR);
|
|
380
352
|
const lightText = this.textContent?.trim();
|
|
381
353
|
if (lightText) {
|
|
382
354
|
try {
|
|
383
355
|
this.data = JSON.parse(lightText);
|
|
384
|
-
} catch {
|
|
385
|
-
}
|
|
356
|
+
} catch {}
|
|
386
357
|
}
|
|
387
358
|
this.textContent = "";
|
|
388
359
|
this.state = "ready";
|
|
@@ -410,56 +381,42 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
410
381
|
}
|
|
411
382
|
disconnectedCallback() {
|
|
412
383
|
this.component.destroy?.();
|
|
413
|
-
this.component.element =
|
|
384
|
+
this.component.element = undefined;
|
|
414
385
|
this.intersectionObserver?.disconnect();
|
|
415
386
|
this.intersectionObserver = null;
|
|
416
387
|
this.abortController?.abort();
|
|
417
388
|
this.abortController = null;
|
|
418
389
|
this.state = "idle";
|
|
419
390
|
this.data = null;
|
|
420
|
-
this.context =
|
|
391
|
+
this.context = undefined;
|
|
421
392
|
this.dataPromise = null;
|
|
422
393
|
this.errorMessage = "";
|
|
423
394
|
this.signalReady();
|
|
424
395
|
this.deferred = null;
|
|
425
396
|
}
|
|
426
|
-
/**
|
|
427
|
-
* Reload component data. Aborts any in-flight request first.
|
|
428
|
-
*/
|
|
429
397
|
async reload() {
|
|
430
398
|
if (this.params === null)
|
|
431
399
|
return;
|
|
432
400
|
this.abortController?.abort();
|
|
433
|
-
this.abortController = new AbortController
|
|
401
|
+
this.abortController = new AbortController;
|
|
434
402
|
await this.loadData();
|
|
435
403
|
}
|
|
436
|
-
/**
|
|
437
|
-
* Fetch a single file by path, with caching.
|
|
438
|
-
* Absolute URLs (http/https) pass through; relative paths get '/' prefix.
|
|
439
|
-
*/
|
|
440
404
|
static loadFile(path) {
|
|
441
|
-
const cached = _ComponentElement.fileCache.get(path);
|
|
442
|
-
if (cached)
|
|
443
|
-
return cached;
|
|
444
405
|
const url = path.startsWith("http://") || path.startsWith("https://") ? path : path.startsWith("/") ? path : "/" + path;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
406
|
+
return fetch(url).then((res) => res.ok ? res.text() : undefined, () => {
|
|
407
|
+
return;
|
|
408
|
+
});
|
|
448
409
|
}
|
|
449
|
-
/**
|
|
450
|
-
* Load all companion files for this widget instance.
|
|
451
|
-
* Uses effectiveFiles (from registration) falling back to component.files.
|
|
452
|
-
*/
|
|
453
410
|
async loadFiles() {
|
|
454
411
|
const filePaths = this.effectiveFiles ?? this.component.files;
|
|
455
412
|
if (!filePaths)
|
|
456
413
|
return {};
|
|
457
414
|
const [html, md, css] = await Promise.all([
|
|
458
|
-
filePaths.html ?
|
|
459
|
-
filePaths.md ?
|
|
460
|
-
filePaths.css ?
|
|
415
|
+
filePaths.html ? ComponentElement.loadFile(filePaths.html) : undefined,
|
|
416
|
+
filePaths.md ? ComponentElement.loadFile(filePaths.md) : undefined,
|
|
417
|
+
filePaths.css ? ComponentElement.loadFile(filePaths.css) : undefined
|
|
461
418
|
]);
|
|
462
|
-
return { html, md, css };
|
|
419
|
+
return filterUndefined({ html, md, css }) ?? {};
|
|
463
420
|
}
|
|
464
421
|
async loadData() {
|
|
465
422
|
if (this.params === null)
|
|
@@ -470,7 +427,7 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
470
427
|
try {
|
|
471
428
|
const promise = this.component.getData({
|
|
472
429
|
params: this.params,
|
|
473
|
-
signal,
|
|
430
|
+
...signal ? { signal } : {},
|
|
474
431
|
context: this.context
|
|
475
432
|
});
|
|
476
433
|
this.dataPromise = promise;
|
|
@@ -523,189 +480,235 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
523
480
|
});
|
|
524
481
|
}
|
|
525
482
|
}
|
|
526
|
-
}
|
|
483
|
+
}
|
|
527
484
|
|
|
528
|
-
// dist/
|
|
529
|
-
|
|
530
|
-
widgets =
|
|
531
|
-
/** Register a widget by its name. */
|
|
485
|
+
// dist/core/widget/widget.registry.js
|
|
486
|
+
class WidgetRegistry {
|
|
487
|
+
widgets = new Map;
|
|
532
488
|
add(widget) {
|
|
533
489
|
this.widgets.set(widget.name, widget);
|
|
534
490
|
}
|
|
535
|
-
/** Look up a widget by name. */
|
|
536
491
|
get(name) {
|
|
537
492
|
return this.widgets.get(name);
|
|
538
493
|
}
|
|
539
|
-
/** Iterate all registered widgets. */
|
|
540
494
|
[Symbol.iterator]() {
|
|
541
495
|
return this.widgets.values();
|
|
542
496
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// dist/core/router/route.trie.js
|
|
500
|
+
class RouteTrie {
|
|
501
|
+
tree;
|
|
502
|
+
constructor(tree) {
|
|
503
|
+
this.tree = tree;
|
|
504
|
+
}
|
|
505
|
+
match(pathname) {
|
|
506
|
+
pathname = this.normalizePath(pathname);
|
|
507
|
+
if (pathname === "/") {
|
|
508
|
+
if (this.tree.files || this.tree.redirect) {
|
|
509
|
+
return { node: this.tree, pattern: "/", params: {} };
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
556
512
|
}
|
|
557
|
-
return
|
|
513
|
+
return this.walk(this.tree, this.splitSegments(pathname), 0, {}, "/");
|
|
558
514
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
565
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
566
|
-
});
|
|
515
|
+
findErrorBoundary(pathname) {
|
|
516
|
+
pathname = this.normalizePath(pathname);
|
|
517
|
+
if (pathname === "/")
|
|
518
|
+
return this.tree.errorBoundary;
|
|
519
|
+
return this.walkForBoundary(this.tree, this.splitSegments(pathname), 0, this.tree.errorBoundary);
|
|
567
520
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
521
|
+
findRoute(pattern) {
|
|
522
|
+
if (pattern === "/") {
|
|
523
|
+
return this.tree.files || this.tree.redirect ? this.tree : undefined;
|
|
524
|
+
}
|
|
525
|
+
const segments = this.splitSegments(pattern);
|
|
526
|
+
let node = this.tree;
|
|
527
|
+
for (const segment of segments) {
|
|
528
|
+
let child;
|
|
529
|
+
if (segment.startsWith(":") && segment.endsWith("*")) {
|
|
530
|
+
child = node.wildcard?.child;
|
|
531
|
+
} else if (segment.startsWith(":")) {
|
|
532
|
+
child = node.dynamic?.child;
|
|
533
|
+
} else {
|
|
534
|
+
child = node.children?.[segment];
|
|
535
|
+
}
|
|
536
|
+
if (!child)
|
|
537
|
+
return;
|
|
538
|
+
node = child;
|
|
539
|
+
}
|
|
540
|
+
return node.files || node.redirect ? node : undefined;
|
|
541
|
+
}
|
|
542
|
+
safeDecode(segment) {
|
|
543
|
+
try {
|
|
544
|
+
return decodeURIComponent(segment);
|
|
545
|
+
} catch {
|
|
546
|
+
return segment;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
splitSegments(pathname) {
|
|
550
|
+
return pathname.substring(1).split("/");
|
|
551
|
+
}
|
|
552
|
+
normalizePath(pathname) {
|
|
553
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
554
|
+
pathname = pathname.slice(0, -1);
|
|
555
|
+
}
|
|
556
|
+
if (!pathname.startsWith("/")) {
|
|
557
|
+
pathname = "/" + pathname;
|
|
558
|
+
}
|
|
559
|
+
return pathname;
|
|
560
|
+
}
|
|
561
|
+
walk(node, segments, index, params, pattern) {
|
|
562
|
+
if (index === segments.length) {
|
|
563
|
+
if (node.files || node.redirect) {
|
|
564
|
+
return { node, pattern, params: { ...params } };
|
|
565
|
+
}
|
|
566
|
+
if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
|
|
567
|
+
const wp = pattern === "/" ? `/:${node.wildcard.param}*` : `${pattern}/:${node.wildcard.param}*`;
|
|
568
|
+
return {
|
|
569
|
+
node: node.wildcard.child,
|
|
570
|
+
pattern: wp,
|
|
571
|
+
params: { ...params, [node.wildcard.param]: "" }
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const segment = segments[index];
|
|
577
|
+
const staticChild = node.children?.[segment];
|
|
578
|
+
if (staticChild) {
|
|
579
|
+
const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
|
|
580
|
+
const result = this.walk(staticChild, segments, index + 1, params, childPattern);
|
|
581
|
+
if (result)
|
|
582
|
+
return result;
|
|
583
|
+
}
|
|
584
|
+
if (node.dynamic) {
|
|
585
|
+
const { param, child } = node.dynamic;
|
|
586
|
+
params[param] = this.safeDecode(segment);
|
|
587
|
+
const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
|
|
588
|
+
const result = this.walk(child, segments, index + 1, params, childPattern);
|
|
589
|
+
if (result)
|
|
590
|
+
return result;
|
|
591
|
+
delete params[param];
|
|
592
|
+
}
|
|
593
|
+
if (node.wildcard && (node.wildcard.child.files || node.wildcard.child.redirect)) {
|
|
594
|
+
const { param, child } = node.wildcard;
|
|
595
|
+
let rest = this.safeDecode(segments[index]);
|
|
596
|
+
for (let i = index + 1;i < segments.length; i++) {
|
|
597
|
+
rest += "/" + this.safeDecode(segments[i]);
|
|
598
|
+
}
|
|
599
|
+
const wp = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
|
|
600
|
+
return {
|
|
601
|
+
node: child,
|
|
602
|
+
pattern: wp,
|
|
603
|
+
params: { ...params, [param]: rest }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
575
607
|
}
|
|
608
|
+
walkForBoundary(node, segments, index, deepest) {
|
|
609
|
+
if (index === segments.length) {
|
|
610
|
+
return node.errorBoundary ?? deepest;
|
|
611
|
+
}
|
|
612
|
+
const segment = segments[index];
|
|
613
|
+
const staticChild = node.children?.[segment];
|
|
614
|
+
if (staticChild) {
|
|
615
|
+
return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
|
|
616
|
+
}
|
|
617
|
+
if (node.dynamic) {
|
|
618
|
+
return this.walkForBoundary(node.dynamic.child, segments, index + 1, node.dynamic.child.errorBoundary ?? deepest);
|
|
619
|
+
}
|
|
620
|
+
if (node.wildcard) {
|
|
621
|
+
return node.wildcard.child.errorBoundary ?? deepest;
|
|
622
|
+
}
|
|
623
|
+
return deepest;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// dist/core/runtime/abstract.runtime.js
|
|
628
|
+
var ROUTES_MANIFEST_PATH = "/routes.manifest.json";
|
|
629
|
+
var WIDGETS_MANIFEST_PATH = "/widgets.manifest.json";
|
|
630
|
+
var ELEMENTS_MANIFEST_PATH = "/elements.manifest.json";
|
|
631
|
+
|
|
632
|
+
// dist/core/type/logger.type.js
|
|
633
|
+
var noop = () => {};
|
|
634
|
+
var defaultLogger = { error: noop, warn: noop };
|
|
635
|
+
function setLogger(_logger) {
|
|
636
|
+
console.warn("[emroute] setLogger() is deprecated. Pass `logger` in Emroute.create() config instead.");
|
|
576
637
|
}
|
|
638
|
+
|
|
639
|
+
// dist/core/pipeline/pipeline.js
|
|
577
640
|
var DEFAULT_ROOT_ROUTE = {
|
|
578
641
|
pattern: "/",
|
|
579
642
|
type: "page",
|
|
580
643
|
modulePath: "__default_root__"
|
|
581
644
|
};
|
|
582
|
-
function toRouteConfig(
|
|
583
|
-
const node = resolved.node;
|
|
645
|
+
function toRouteConfig(node, pattern) {
|
|
584
646
|
return {
|
|
585
|
-
pattern
|
|
647
|
+
pattern,
|
|
586
648
|
type: node.redirect ? "redirect" : "page",
|
|
587
649
|
modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
|
|
588
|
-
files: node.files
|
|
650
|
+
...node.files ? { files: node.files } : {}
|
|
589
651
|
};
|
|
590
652
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
653
|
+
|
|
654
|
+
class Pipeline {
|
|
655
|
+
runtime;
|
|
594
656
|
contextProvider;
|
|
595
|
-
|
|
596
|
-
moduleCache = /* @__PURE__ */ new Map();
|
|
597
|
-
widgetFileCache = /* @__PURE__ */ new Map();
|
|
657
|
+
logger;
|
|
598
658
|
moduleLoaders;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
this.
|
|
603
|
-
this.readFile = options.fileReader ?? ((path) => fetch(path, { headers: { Accept: "text/plain" } }).then((r) => r.text()));
|
|
604
|
-
this.contextProvider = options.extendContext;
|
|
659
|
+
constructor(options) {
|
|
660
|
+
this.runtime = options.runtime;
|
|
661
|
+
this.contextProvider = options.contextProvider;
|
|
662
|
+
this.logger = options.logger ?? defaultLogger;
|
|
605
663
|
this.moduleLoaders = options.moduleLoaders ?? {};
|
|
606
664
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
return this.currentRoute?.params ?? {};
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Add event listener for router events.
|
|
615
|
-
*/
|
|
616
|
-
addEventListener(listener) {
|
|
617
|
-
this.listeners.add(listener);
|
|
618
|
-
return () => this.listeners.delete(listener);
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* Emit router event to listeners.
|
|
622
|
-
*/
|
|
623
|
-
emit(event) {
|
|
624
|
-
for (const listener of this.listeners) {
|
|
625
|
-
try {
|
|
626
|
-
listener(event);
|
|
627
|
-
} catch (e) {
|
|
628
|
-
console.error("[Router] Event listener error:", e);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
665
|
+
async getResolver() {
|
|
666
|
+
const response = await this.runtime.query(ROUTES_MANIFEST_PATH);
|
|
667
|
+
const tree = response.status === 404 ? {} : await response.json();
|
|
668
|
+
return new RouteTrie(tree);
|
|
631
669
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
*/
|
|
636
|
-
match(url) {
|
|
637
|
-
const pathname = url.pathname;
|
|
638
|
-
const resolved = this.resolver.match(pathname);
|
|
670
|
+
async match(url) {
|
|
671
|
+
const resolver = await this.getResolver();
|
|
672
|
+
const resolved = resolver.match(url.pathname);
|
|
639
673
|
if (resolved) {
|
|
640
|
-
return {
|
|
641
|
-
route: toRouteConfig(resolved),
|
|
642
|
-
params: resolved.params
|
|
643
|
-
};
|
|
674
|
+
return { route: toRouteConfig(resolved.node, resolved.pattern), params: resolved.params };
|
|
644
675
|
}
|
|
645
|
-
if (pathname === "/" || pathname === "") {
|
|
646
|
-
return {
|
|
647
|
-
route: DEFAULT_ROOT_ROUTE,
|
|
648
|
-
params: {}
|
|
649
|
-
};
|
|
676
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
677
|
+
return { route: DEFAULT_ROOT_ROUTE, params: {} };
|
|
650
678
|
}
|
|
651
|
-
return
|
|
679
|
+
return;
|
|
652
680
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const node =
|
|
681
|
+
async findRoute(pattern) {
|
|
682
|
+
const resolver = await this.getResolver();
|
|
683
|
+
const node = resolver.findRoute(pattern);
|
|
656
684
|
if (!node)
|
|
657
|
-
return
|
|
658
|
-
return
|
|
659
|
-
pattern: `/${status}`,
|
|
660
|
-
type: "page",
|
|
661
|
-
modulePath: node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
|
|
662
|
-
files: node.files
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
/** Get global error handler (root errorBoundary). */
|
|
666
|
-
getErrorHandler() {
|
|
667
|
-
const modulePath = this.resolver.findErrorBoundary("/");
|
|
668
|
-
if (!modulePath)
|
|
669
|
-
return void 0;
|
|
670
|
-
return { pattern: "/", type: "error", modulePath };
|
|
685
|
+
return;
|
|
686
|
+
return toRouteConfig(node, pattern);
|
|
671
687
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
* Callers should only rely on modulePath.
|
|
676
|
-
*/
|
|
677
|
-
findErrorBoundary(pathname) {
|
|
678
|
-
const modulePath = this.resolver.findErrorBoundary(pathname);
|
|
688
|
+
async findErrorBoundary(pathname) {
|
|
689
|
+
const resolver = await this.getResolver();
|
|
690
|
+
const modulePath = resolver.findErrorBoundary(pathname);
|
|
679
691
|
if (!modulePath)
|
|
680
|
-
return
|
|
692
|
+
return;
|
|
681
693
|
return { pattern: pathname, modulePath };
|
|
682
694
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
*/
|
|
687
|
-
findRoute(pattern) {
|
|
688
|
-
const node = this.resolver.findRoute(pattern);
|
|
695
|
+
async getStatusPage(status) {
|
|
696
|
+
const resolver = await this.getResolver();
|
|
697
|
+
const node = resolver.findRoute(`/${status}`);
|
|
689
698
|
if (!node)
|
|
690
|
-
return
|
|
691
|
-
return {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
699
|
+
return;
|
|
700
|
+
return toRouteConfig(node, `/${status}`);
|
|
701
|
+
}
|
|
702
|
+
async getErrorHandler() {
|
|
703
|
+
const resolver = await this.getResolver();
|
|
704
|
+
const modulePath = resolver.findErrorBoundary("/");
|
|
705
|
+
if (!modulePath)
|
|
706
|
+
return;
|
|
707
|
+
return { pattern: "/", type: "error", modulePath };
|
|
697
708
|
}
|
|
698
|
-
/**
|
|
699
|
-
* Build route hierarchy from a pattern.
|
|
700
|
-
* Patterns are always unprefixed (no basePath).
|
|
701
|
-
*
|
|
702
|
-
* e.g., '/projects/:id/tasks'
|
|
703
|
-
* → ['/', '/projects', '/projects/:id', '/projects/:id/tasks']
|
|
704
|
-
*/
|
|
705
709
|
buildRouteHierarchy(pattern) {
|
|
706
|
-
if (pattern === "/")
|
|
710
|
+
if (pattern === "/")
|
|
707
711
|
return ["/"];
|
|
708
|
-
}
|
|
709
712
|
const segments = pattern.split("/").filter(Boolean);
|
|
710
713
|
const hierarchy = ["/"];
|
|
711
714
|
let current = "";
|
|
@@ -715,318 +718,100 @@ var RouteCore = class {
|
|
|
715
718
|
}
|
|
716
719
|
return hierarchy;
|
|
717
720
|
}
|
|
718
|
-
/**
|
|
719
|
-
* Normalize URL by removing trailing slashes (except bare '/').
|
|
720
|
-
*/
|
|
721
|
-
normalizeUrl(url) {
|
|
722
|
-
if (url.length > 1 && url.endsWith("/")) {
|
|
723
|
-
return url.slice(0, -1);
|
|
724
|
-
}
|
|
725
|
-
return url;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Convert relative path to absolute path.
|
|
729
|
-
*/
|
|
730
|
-
toAbsolutePath(path) {
|
|
731
|
-
return path.startsWith("/") ? path : "/" + path;
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* Load a module with caching.
|
|
735
|
-
* Uses pre-bundled loaders when available, falls back to dynamic import.
|
|
736
|
-
*/
|
|
737
721
|
async loadModule(modulePath) {
|
|
738
|
-
if (this.moduleCache.has(modulePath)) {
|
|
739
|
-
return this.moduleCache.get(modulePath);
|
|
740
|
-
}
|
|
741
|
-
let module;
|
|
742
722
|
const loader = this.moduleLoaders[modulePath];
|
|
743
723
|
if (loader) {
|
|
744
|
-
|
|
745
|
-
} else {
|
|
746
|
-
const absolutePath = this.toAbsolutePath(modulePath);
|
|
747
|
-
module = await import(__rewriteRelativeImportExtension(absolutePath));
|
|
724
|
+
return await loader();
|
|
748
725
|
}
|
|
749
|
-
|
|
750
|
-
return
|
|
726
|
+
const abs = modulePath.startsWith("/") ? modulePath : "/" + modulePath;
|
|
727
|
+
return await this.runtime.loadModule(abs);
|
|
728
|
+
}
|
|
729
|
+
getModuleFiles(mod) {
|
|
730
|
+
if (!mod || typeof mod !== "object")
|
|
731
|
+
return;
|
|
732
|
+
const files = mod.__files;
|
|
733
|
+
if (!files || typeof files !== "object")
|
|
734
|
+
return;
|
|
735
|
+
return files;
|
|
751
736
|
}
|
|
752
|
-
|
|
753
|
-
* Load widget file contents with caching.
|
|
754
|
-
*/
|
|
755
|
-
async loadWidgetFiles(widgetFiles) {
|
|
737
|
+
async loadFiles(files) {
|
|
756
738
|
const load = async (path) => {
|
|
757
|
-
const
|
|
758
|
-
const cached = this.widgetFileCache.get(absPath);
|
|
759
|
-
if (cached !== void 0)
|
|
760
|
-
return cached;
|
|
739
|
+
const abs = path.startsWith("/") ? path : "/" + path;
|
|
761
740
|
try {
|
|
762
|
-
|
|
763
|
-
this.widgetFileCache.set(absPath, content);
|
|
764
|
-
return content;
|
|
741
|
+
return await this.runtime.query(abs, { as: "text" });
|
|
765
742
|
} catch (e) {
|
|
766
|
-
console.warn(`[
|
|
767
|
-
return
|
|
743
|
+
console.warn(`[Pipeline] Failed to load file ${path}:`, e instanceof Error ? e.message : e);
|
|
744
|
+
return;
|
|
768
745
|
}
|
|
769
746
|
};
|
|
770
747
|
const [html, md, css] = await Promise.all([
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
748
|
+
files.html ? load(files.html) : undefined,
|
|
749
|
+
files.md ? load(files.md) : undefined,
|
|
750
|
+
files.css ? load(files.css) : undefined
|
|
774
751
|
]);
|
|
775
|
-
|
|
752
|
+
const result = {};
|
|
753
|
+
if (html !== undefined)
|
|
754
|
+
result.html = html;
|
|
755
|
+
if (md !== undefined)
|
|
756
|
+
result.md = md;
|
|
757
|
+
if (css !== undefined)
|
|
758
|
+
result.css = css;
|
|
759
|
+
return result;
|
|
776
760
|
}
|
|
777
|
-
/**
|
|
778
|
-
* Build a RouteInfo from a matched route and the resolved URL pathname.
|
|
779
|
-
* Called once per navigation; the result is reused across the route hierarchy.
|
|
780
|
-
*/
|
|
781
761
|
toRouteInfo(matched, url) {
|
|
782
|
-
return {
|
|
783
|
-
url,
|
|
784
|
-
params: matched.params
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Get inlined `__files` from a cached module (merged module pattern).
|
|
789
|
-
* Returns undefined if the module isn't cached or has no __files.
|
|
790
|
-
*/
|
|
791
|
-
getModuleFiles(modulePath) {
|
|
792
|
-
const cached = this.moduleCache.get(modulePath);
|
|
793
|
-
if (!cached || typeof cached !== "object")
|
|
794
|
-
return void 0;
|
|
795
|
-
const files = cached.__files;
|
|
796
|
-
if (!files || typeof files !== "object")
|
|
797
|
-
return void 0;
|
|
798
|
-
return files;
|
|
762
|
+
return { url, params: matched.params };
|
|
799
763
|
}
|
|
800
|
-
|
|
801
|
-
* Build a ComponentContext by extending RouteInfo with loaded file contents.
|
|
802
|
-
*
|
|
803
|
-
* When the route module is a merged module (contains `__files`), uses
|
|
804
|
-
* inlined content directly. Otherwise falls back to reading companion files.
|
|
805
|
-
*/
|
|
806
|
-
async buildComponentContext(routeInfo, route, signal, isLeaf) {
|
|
764
|
+
async buildContext(routeInfo, route, signal, isLeaf, loadedModule) {
|
|
807
765
|
const rf = route.files;
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
let html;
|
|
811
|
-
let md;
|
|
812
|
-
let css;
|
|
766
|
+
const inlined = loadedModule ? this.getModuleFiles(loadedModule) : undefined;
|
|
767
|
+
let files;
|
|
813
768
|
if (inlined) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
769
|
+
files = inlined;
|
|
770
|
+
} else if (rf) {
|
|
771
|
+
const filePaths = {};
|
|
772
|
+
if (rf.html)
|
|
773
|
+
filePaths.html = rf.html;
|
|
774
|
+
if (rf.md)
|
|
775
|
+
filePaths.md = rf.md;
|
|
776
|
+
if (rf.css)
|
|
777
|
+
filePaths.css = rf.css;
|
|
778
|
+
files = await this.loadFiles(filePaths);
|
|
817
779
|
} else {
|
|
818
|
-
|
|
819
|
-
[html, md, css] = await Promise.all([
|
|
820
|
-
rf?.html ? fetchFile(rf.html) : void 0,
|
|
821
|
-
rf?.md ? fetchFile(rf.md) : void 0,
|
|
822
|
-
rf?.css ? fetchFile(rf.css) : void 0
|
|
823
|
-
]);
|
|
780
|
+
files = {};
|
|
824
781
|
}
|
|
825
782
|
const base = {
|
|
826
783
|
...routeInfo,
|
|
827
784
|
pathname: routeInfo.url.pathname,
|
|
828
785
|
searchParams: routeInfo.url.searchParams,
|
|
829
|
-
files
|
|
830
|
-
signal,
|
|
831
|
-
isLeaf
|
|
786
|
+
files,
|
|
787
|
+
...signal ? { signal } : {},
|
|
788
|
+
...isLeaf !== undefined ? { isLeaf } : {}
|
|
832
789
|
};
|
|
833
790
|
return this.contextProvider ? this.contextProvider(base) : base;
|
|
834
791
|
}
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
// dist/src/route/route.trie.js
|
|
838
|
-
function createNode() {
|
|
839
|
-
return { static: /* @__PURE__ */ new Map() };
|
|
840
|
-
}
|
|
841
|
-
function safeDecode(segment) {
|
|
842
|
-
try {
|
|
843
|
-
return decodeURIComponent(segment);
|
|
844
|
-
} catch {
|
|
845
|
-
return segment;
|
|
846
|
-
}
|
|
847
792
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
node.pattern = pattern;
|
|
856
|
-
}
|
|
857
|
-
if (source.errorBoundary) {
|
|
858
|
-
node.errorBoundary = source.errorBoundary;
|
|
859
|
-
}
|
|
860
|
-
if (source.children) {
|
|
861
|
-
for (const [segment, child] of Object.entries(source.children)) {
|
|
862
|
-
const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
|
|
863
|
-
node.static.set(segment, convertNode(child, childPattern));
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
if (source.dynamic) {
|
|
867
|
-
const { param, child } = source.dynamic;
|
|
868
|
-
const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
|
|
869
|
-
node.dynamic = { param, node: convertNode(child, childPattern) };
|
|
870
|
-
}
|
|
871
|
-
if (source.wildcard) {
|
|
872
|
-
const { param, child } = source.wildcard;
|
|
873
|
-
const childPattern = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
|
|
874
|
-
node.wildcard = { param, node: convertNode(child, childPattern) };
|
|
793
|
+
|
|
794
|
+
// dist/core/util/widget-resolve.util.js
|
|
795
|
+
var MAX_WIDGET_DEPTH = 10;
|
|
796
|
+
async function resolveRecursively(content, parse, resolve, replace, depth = 0, logger = defaultLogger) {
|
|
797
|
+
if (depth >= MAX_WIDGET_DEPTH) {
|
|
798
|
+
logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). ` + "Possible circular dependency or excessive nesting.");
|
|
799
|
+
return content;
|
|
875
800
|
}
|
|
876
|
-
|
|
801
|
+
const widgets = parse(content);
|
|
802
|
+
if (widgets.length === 0)
|
|
803
|
+
return content;
|
|
804
|
+
const replacements = new Map;
|
|
805
|
+
await Promise.all(widgets.map(async (widget) => {
|
|
806
|
+
let rendered = await resolve(widget);
|
|
807
|
+
rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1, logger);
|
|
808
|
+
replacements.set(widget, rendered);
|
|
809
|
+
}));
|
|
810
|
+
return replace(content, replacements);
|
|
877
811
|
}
|
|
878
|
-
|
|
879
|
-
root;
|
|
880
|
-
constructor(tree) {
|
|
881
|
-
this.root = convertNode(tree, "/");
|
|
882
|
-
}
|
|
883
|
-
match(pathname) {
|
|
884
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
885
|
-
pathname = pathname.slice(0, -1);
|
|
886
|
-
}
|
|
887
|
-
if (!pathname.startsWith("/")) {
|
|
888
|
-
pathname = "/" + pathname;
|
|
889
|
-
}
|
|
890
|
-
if (pathname === "/") {
|
|
891
|
-
if (this.root.route) {
|
|
892
|
-
return { node: this.root.route, pattern: "/", params: {} };
|
|
893
|
-
}
|
|
894
|
-
return void 0;
|
|
895
|
-
}
|
|
896
|
-
const segments = splitSegments(pathname);
|
|
897
|
-
return this.walk(this.root, segments, 0, {});
|
|
898
|
-
}
|
|
899
|
-
findErrorBoundary(pathname) {
|
|
900
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
901
|
-
pathname = pathname.slice(0, -1);
|
|
902
|
-
}
|
|
903
|
-
if (!pathname.startsWith("/")) {
|
|
904
|
-
pathname = "/" + pathname;
|
|
905
|
-
}
|
|
906
|
-
if (pathname === "/")
|
|
907
|
-
return this.root.errorBoundary;
|
|
908
|
-
const segments = splitSegments(pathname);
|
|
909
|
-
return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
|
|
910
|
-
}
|
|
911
|
-
findRoute(pattern) {
|
|
912
|
-
if (pattern === "/") {
|
|
913
|
-
return this.root.route;
|
|
914
|
-
}
|
|
915
|
-
const segments = splitSegments(pattern);
|
|
916
|
-
let node = this.root;
|
|
917
|
-
for (const segment of segments) {
|
|
918
|
-
let child;
|
|
919
|
-
if (segment.startsWith(":") && segment.endsWith("*")) {
|
|
920
|
-
child = node.wildcard?.node;
|
|
921
|
-
} else if (segment.startsWith(":")) {
|
|
922
|
-
child = node.dynamic?.node;
|
|
923
|
-
} else {
|
|
924
|
-
child = node.static.get(segment);
|
|
925
|
-
}
|
|
926
|
-
if (!child)
|
|
927
|
-
return void 0;
|
|
928
|
-
node = child;
|
|
929
|
-
}
|
|
930
|
-
return node.route;
|
|
931
|
-
}
|
|
932
|
-
// ── Private matching ──────────────────────────────────────────────────
|
|
933
|
-
walk(node, segments, index, params) {
|
|
934
|
-
if (index === segments.length) {
|
|
935
|
-
if (node.route) {
|
|
936
|
-
return { node: node.route, pattern: node.pattern, params: { ...params } };
|
|
937
|
-
}
|
|
938
|
-
if (node.wildcard?.node.route) {
|
|
939
|
-
return {
|
|
940
|
-
node: node.wildcard.node.route,
|
|
941
|
-
pattern: node.wildcard.node.pattern,
|
|
942
|
-
params: { ...params, [node.wildcard.param]: "" }
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
return void 0;
|
|
946
|
-
}
|
|
947
|
-
const segment = segments[index];
|
|
948
|
-
const staticChild = node.static.get(segment);
|
|
949
|
-
if (staticChild) {
|
|
950
|
-
const result = this.walk(staticChild, segments, index + 1, params);
|
|
951
|
-
if (result)
|
|
952
|
-
return result;
|
|
953
|
-
}
|
|
954
|
-
if (node.dynamic) {
|
|
955
|
-
const { param, node: dynamicNode } = node.dynamic;
|
|
956
|
-
params[param] = safeDecode(segment);
|
|
957
|
-
const result = this.walk(dynamicNode, segments, index + 1, params);
|
|
958
|
-
if (result)
|
|
959
|
-
return result;
|
|
960
|
-
delete params[param];
|
|
961
|
-
}
|
|
962
|
-
if (node.wildcard?.node.route) {
|
|
963
|
-
const { param, node: wildcardNode } = node.wildcard;
|
|
964
|
-
let rest = safeDecode(segments[index]);
|
|
965
|
-
for (let i = index + 1; i < segments.length; i++) {
|
|
966
|
-
rest += "/" + safeDecode(segments[i]);
|
|
967
|
-
}
|
|
968
|
-
return {
|
|
969
|
-
node: wildcardNode.route,
|
|
970
|
-
pattern: wildcardNode.pattern,
|
|
971
|
-
params: { ...params, [param]: rest }
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
return void 0;
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Walk for error boundary. Follows the same priority as match
|
|
978
|
-
* (static → dynamic → wildcard) without backtracking across branches.
|
|
979
|
-
* Returns the deepest error boundary module path found along the path.
|
|
980
|
-
*/
|
|
981
|
-
walkForBoundary(node, segments, index, deepest) {
|
|
982
|
-
if (index === segments.length) {
|
|
983
|
-
return node.errorBoundary ?? deepest;
|
|
984
|
-
}
|
|
985
|
-
const segment = segments[index];
|
|
986
|
-
const staticChild = node.static.get(segment);
|
|
987
|
-
if (staticChild) {
|
|
988
|
-
return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
|
|
989
|
-
}
|
|
990
|
-
if (node.dynamic) {
|
|
991
|
-
return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
|
|
992
|
-
}
|
|
993
|
-
if (node.wildcard) {
|
|
994
|
-
return node.wildcard.node.errorBoundary ?? deepest;
|
|
995
|
-
}
|
|
996
|
-
return deepest;
|
|
997
|
-
}
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
// dist/src/type/logger.type.js
|
|
1001
|
-
var noop = () => {
|
|
1002
|
-
};
|
|
1003
|
-
var logger = { error: noop, warn: noop };
|
|
1004
|
-
function setLogger(impl) {
|
|
1005
|
-
logger.error = impl.error.bind(impl);
|
|
1006
|
-
logger.warn = impl.warn.bind(impl);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
// dist/src/util/widget-resolve.util.js
|
|
1010
|
-
var MAX_WIDGET_DEPTH = 10;
|
|
1011
|
-
async function resolveRecursively(content, parse, resolve, replace, depth = 0) {
|
|
1012
|
-
if (depth >= MAX_WIDGET_DEPTH) {
|
|
1013
|
-
logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). Possible circular dependency or excessive nesting.`);
|
|
1014
|
-
return content;
|
|
1015
|
-
}
|
|
1016
|
-
const widgets = parse(content);
|
|
1017
|
-
if (widgets.length === 0)
|
|
1018
|
-
return content;
|
|
1019
|
-
const replacements = /* @__PURE__ */ new Map();
|
|
1020
|
-
await Promise.all(widgets.map(async (widget) => {
|
|
1021
|
-
let rendered = await resolve(widget);
|
|
1022
|
-
rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1);
|
|
1023
|
-
replacements.set(widget, rendered);
|
|
1024
|
-
}));
|
|
1025
|
-
return replace(content, replacements);
|
|
1026
|
-
}
|
|
1027
|
-
function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider) {
|
|
812
|
+
function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider, logger = defaultLogger) {
|
|
1028
813
|
const tagPattern = /<widget-(?<name>[a-z][a-z0-9-]*)(?<attrs>\s[^>]*)?>(?<content>.*?)<\/widget-\k<name>>/gis;
|
|
1029
|
-
const wrappers =
|
|
814
|
+
const wrappers = new Map;
|
|
1030
815
|
const ssrAttrPattern = new RegExp(`\\s${SSR_ATTR}(?:\\s|=|$)`);
|
|
1031
816
|
const parse = (content) => {
|
|
1032
817
|
const matches = content.matchAll(tagPattern).toArray();
|
|
@@ -1051,7 +836,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
|
|
|
1051
836
|
...routeInfo,
|
|
1052
837
|
pathname: routeInfo.url.pathname,
|
|
1053
838
|
searchParams: routeInfo.url.searchParams,
|
|
1054
|
-
files
|
|
839
|
+
...files ? { files } : {}
|
|
1055
840
|
};
|
|
1056
841
|
const context = contextProvider ? contextProvider(baseContext) : baseContext;
|
|
1057
842
|
const data = await widget.getData({ params, context });
|
|
@@ -1063,7 +848,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
|
|
|
1063
848
|
});
|
|
1064
849
|
return rendered;
|
|
1065
850
|
} catch (e) {
|
|
1066
|
-
logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e :
|
|
851
|
+
logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e : undefined);
|
|
1067
852
|
return match[0];
|
|
1068
853
|
}
|
|
1069
854
|
};
|
|
@@ -1080,7 +865,7 @@ function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider
|
|
|
1080
865
|
}
|
|
1081
866
|
return result;
|
|
1082
867
|
};
|
|
1083
|
-
return resolveRecursively(html, parse, resolve, replace);
|
|
868
|
+
return resolveRecursively(html, parse, resolve, replace, 0, logger);
|
|
1084
869
|
}
|
|
1085
870
|
function parseAttrsToParams(attrsString) {
|
|
1086
871
|
const params = {};
|
|
@@ -1093,7 +878,7 @@ function parseAttrsToParams(attrsString) {
|
|
|
1093
878
|
continue;
|
|
1094
879
|
const key = attrName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1095
880
|
const rawValue = dq ?? sq ?? uq;
|
|
1096
|
-
if (rawValue ===
|
|
881
|
+
if (rawValue === undefined) {
|
|
1097
882
|
params[key] = "";
|
|
1098
883
|
continue;
|
|
1099
884
|
}
|
|
@@ -1110,32 +895,11 @@ function escapeAttr(value) {
|
|
|
1110
895
|
return value.replaceAll("&", "&").replaceAll("'", "'");
|
|
1111
896
|
}
|
|
1112
897
|
|
|
1113
|
-
// dist/
|
|
1114
|
-
|
|
1115
|
-
/** Host element reference, set by ComponentElement in the browser. */
|
|
898
|
+
// dist/core/component/abstract.component.js
|
|
899
|
+
class Component {
|
|
1116
900
|
element;
|
|
1117
|
-
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
1118
901
|
files;
|
|
1119
|
-
/**
|
|
1120
|
-
* When true, SSR serializes the getData() result into the element's
|
|
1121
|
-
* light DOM so the client can access it immediately in hydrate()
|
|
1122
|
-
* without re-fetching.
|
|
1123
|
-
*
|
|
1124
|
-
* Default is false — hydrate() receives `data: null`. Most widgets
|
|
1125
|
-
* don't need this because the rendered Shadow DOM already contains
|
|
1126
|
-
* the visual representation of the data.
|
|
1127
|
-
*
|
|
1128
|
-
* If you find yourself parsing the shadow DOM in hydrate() trying to
|
|
1129
|
-
* reconstruct the original data object, set this to true instead.
|
|
1130
|
-
* The server-fetched data will be available as `args.data` in hydrate().
|
|
1131
|
-
*/
|
|
1132
902
|
exposeSsrData;
|
|
1133
|
-
/**
|
|
1134
|
-
* Render as HTML for browser context.
|
|
1135
|
-
*
|
|
1136
|
-
* Default implementation converts renderMarkdown() output to HTML.
|
|
1137
|
-
* Override for custom HTML rendering with rich styling/interactivity.
|
|
1138
|
-
*/
|
|
1139
903
|
renderHTML(args) {
|
|
1140
904
|
if (args.data === null) {
|
|
1141
905
|
return `<div data-component="${this.name}">Loading...</div>`;
|
|
@@ -1147,56 +911,23 @@ var Component = class {
|
|
|
1147
911
|
});
|
|
1148
912
|
return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
|
|
1149
913
|
}
|
|
1150
|
-
/**
|
|
1151
|
-
* Render error state.
|
|
1152
|
-
*/
|
|
1153
914
|
renderError(args) {
|
|
1154
915
|
const msg = args.error instanceof Error ? args.error.message : String(args.error);
|
|
1155
916
|
return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
|
|
1156
917
|
}
|
|
1157
|
-
/**
|
|
1158
|
-
* Render error as markdown.
|
|
1159
|
-
*/
|
|
1160
918
|
renderMarkdownError(error) {
|
|
1161
919
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1162
920
|
return `> **Error** (\`${this.name}\`): ${msg}`;
|
|
1163
921
|
}
|
|
1164
|
-
}
|
|
922
|
+
}
|
|
1165
923
|
|
|
1166
|
-
// dist/
|
|
1167
|
-
|
|
924
|
+
// dist/core/component/page.component.js
|
|
925
|
+
class PageComponent extends Component {
|
|
1168
926
|
name = "page";
|
|
1169
|
-
/** Route pattern this page handles (optional — set by subclasses) */
|
|
1170
927
|
pattern;
|
|
1171
|
-
/**
|
|
1172
|
-
* Fetch or compute page data. Override in subclasses.
|
|
1173
|
-
* Default: returns null (no data needed).
|
|
1174
|
-
*
|
|
1175
|
-
* @example
|
|
1176
|
-
* ```ts
|
|
1177
|
-
* override getData({ params, context }: this['DataArgs']) {
|
|
1178
|
-
* return fetch(`/api/${params.id}`, { signal: context?.signal });
|
|
1179
|
-
* }
|
|
1180
|
-
* ```
|
|
1181
|
-
*/
|
|
1182
928
|
getData(_args) {
|
|
1183
929
|
return Promise.resolve(null);
|
|
1184
930
|
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Render page as HTML.
|
|
1187
|
-
*
|
|
1188
|
-
* Fallback chain:
|
|
1189
|
-
* 1. html file content from context
|
|
1190
|
-
* 2. md file content wrapped in `<mark-down>`
|
|
1191
|
-
* 3. `<router-slot />` (bare slot for child routes)
|
|
1192
|
-
*
|
|
1193
|
-
* @example
|
|
1194
|
-
* ```ts
|
|
1195
|
-
* override renderHTML({ data, params, context }: this['RenderArgs']) {
|
|
1196
|
-
* return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
|
|
1197
|
-
* }
|
|
1198
|
-
* ```
|
|
1199
|
-
*/
|
|
1200
931
|
renderHTML(args) {
|
|
1201
932
|
const files = args.context.files;
|
|
1202
933
|
const style = files?.css ? `<style>${files.css}</style>
|
|
@@ -1210,25 +941,12 @@ var PageComponent = class extends Component {
|
|
|
1210
941
|
}
|
|
1211
942
|
if (files?.md) {
|
|
1212
943
|
const hasSlot = files.md.includes("```router-slot");
|
|
1213
|
-
const slot = args.context.isLeaf || hasSlot ? "" :
|
|
944
|
+
const slot = args.context.isLeaf || hasSlot ? "" : `
|
|
945
|
+
<router-slot></router-slot>`;
|
|
1214
946
|
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
|
|
1215
947
|
}
|
|
1216
948
|
return args.context.isLeaf ? "" : "<router-slot></router-slot>";
|
|
1217
949
|
}
|
|
1218
|
-
/**
|
|
1219
|
-
* Render page as Markdown.
|
|
1220
|
-
*
|
|
1221
|
-
* Fallback chain:
|
|
1222
|
-
* 1. md file content from context
|
|
1223
|
-
* 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
|
|
1224
|
-
*
|
|
1225
|
-
* @example
|
|
1226
|
-
* ```ts
|
|
1227
|
-
* override renderMarkdown({ data, params, context }: this['RenderArgs']) {
|
|
1228
|
-
* return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
|
|
1229
|
-
* }
|
|
1230
|
-
* ```
|
|
1231
|
-
*/
|
|
1232
950
|
renderMarkdown(args) {
|
|
1233
951
|
const files = args.context.files;
|
|
1234
952
|
if (files?.md) {
|
|
@@ -1236,53 +954,41 @@ var PageComponent = class extends Component {
|
|
|
1236
954
|
}
|
|
1237
955
|
return args.context.isLeaf ? "" : "```router-slot\n```";
|
|
1238
956
|
}
|
|
1239
|
-
/**
|
|
1240
|
-
* Page title. Override in subclasses.
|
|
1241
|
-
* Default: undefined (no title).
|
|
1242
|
-
*
|
|
1243
|
-
* @example
|
|
1244
|
-
* ```ts
|
|
1245
|
-
* override getTitle({ data, params }: this['RenderArgs']) {
|
|
1246
|
-
* return `Project ${params.id}`;
|
|
1247
|
-
* }
|
|
1248
|
-
* ```
|
|
1249
|
-
*/
|
|
1250
957
|
getTitle(_args) {
|
|
1251
|
-
return
|
|
958
|
+
return;
|
|
1252
959
|
}
|
|
1253
|
-
}
|
|
1254
|
-
var page_component_default = new PageComponent
|
|
960
|
+
}
|
|
961
|
+
var page_component_default = new PageComponent;
|
|
1255
962
|
|
|
1256
|
-
// dist/
|
|
1257
|
-
|
|
1258
|
-
|
|
963
|
+
// dist/core/renderer/ssr.renderer.js
|
|
964
|
+
class SsrRenderer {
|
|
965
|
+
pipeline;
|
|
1259
966
|
widgets;
|
|
1260
967
|
widgetFiles;
|
|
1261
|
-
|
|
1262
|
-
|
|
968
|
+
logger;
|
|
969
|
+
constructor(pipeline, options = {}) {
|
|
970
|
+
this.pipeline = pipeline;
|
|
971
|
+
this.logger = pipeline.logger;
|
|
1263
972
|
this.widgets = options.widgets ?? null;
|
|
1264
973
|
this.widgetFiles = options.widgetFiles ?? {};
|
|
1265
974
|
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Render a URL to a content string.
|
|
1268
|
-
*/
|
|
1269
975
|
async render(url, signal) {
|
|
1270
|
-
const matched = this.
|
|
976
|
+
const matched = await this.pipeline.match(url);
|
|
1271
977
|
if (!matched) {
|
|
1272
|
-
const statusPage = this.
|
|
978
|
+
const statusPage = await this.pipeline.getStatusPage(404);
|
|
1273
979
|
if (statusPage) {
|
|
1274
980
|
try {
|
|
1275
981
|
const ri = { url, params: {} };
|
|
1276
|
-
const result = await this.renderRouteContent(ri, statusPage,
|
|
1277
|
-
return { content: this.stripSlots(result.content), status: 404, title: result.title };
|
|
982
|
+
const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
|
|
983
|
+
return { content: this.stripSlots(result.content), status: 404, ...result.title !== undefined ? { title: result.title } : {} };
|
|
1278
984
|
} catch (e) {
|
|
1279
|
-
logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e :
|
|
985
|
+
this.logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e : undefined);
|
|
1280
986
|
}
|
|
1281
987
|
}
|
|
1282
988
|
return { content: this.renderStatusPage(404, url), status: 404 };
|
|
1283
989
|
}
|
|
1284
990
|
if (matched.route.type === "redirect") {
|
|
1285
|
-
const module = await this.
|
|
991
|
+
const module = await this.pipeline.loadModule(matched.route.modulePath);
|
|
1286
992
|
const redirectConfig = module.default;
|
|
1287
993
|
assertSafeRedirect(redirectConfig.to);
|
|
1288
994
|
return {
|
|
@@ -1291,36 +997,36 @@ var SsrRenderer = class _SsrRenderer {
|
|
|
1291
997
|
redirect: redirectConfig.to
|
|
1292
998
|
};
|
|
1293
999
|
}
|
|
1294
|
-
const routeInfo = this.
|
|
1000
|
+
const routeInfo = this.pipeline.toRouteInfo(matched, url);
|
|
1295
1001
|
try {
|
|
1296
1002
|
const { content, title } = await this.renderPage(routeInfo, matched, signal);
|
|
1297
|
-
return { content, status: 200, title };
|
|
1003
|
+
return { content, status: 200, ...title !== undefined ? { title } : {} };
|
|
1298
1004
|
} catch (error) {
|
|
1299
1005
|
if (error instanceof Response) {
|
|
1300
|
-
const statusPage = this.
|
|
1006
|
+
const statusPage = await this.pipeline.getStatusPage(error.status);
|
|
1301
1007
|
if (statusPage) {
|
|
1302
1008
|
try {
|
|
1303
1009
|
const ri = { url, params: {} };
|
|
1304
|
-
const result = await this.renderRouteContent(ri, statusPage,
|
|
1010
|
+
const result = await this.renderRouteContent(ri, statusPage, undefined, signal);
|
|
1305
1011
|
return {
|
|
1306
1012
|
content: this.stripSlots(result.content),
|
|
1307
1013
|
status: error.status,
|
|
1308
|
-
title: result.title
|
|
1014
|
+
...result.title !== undefined ? { title: result.title } : {}
|
|
1309
1015
|
};
|
|
1310
1016
|
} catch (e) {
|
|
1311
|
-
logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e :
|
|
1017
|
+
this.logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e : undefined);
|
|
1312
1018
|
}
|
|
1313
1019
|
}
|
|
1314
1020
|
return { content: this.renderStatusPage(error.status, url), status: error.status };
|
|
1315
1021
|
}
|
|
1316
|
-
logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error :
|
|
1317
|
-
const boundary = this.
|
|
1022
|
+
this.logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error : undefined);
|
|
1023
|
+
const boundary = await this.pipeline.findErrorBoundary(url.pathname);
|
|
1318
1024
|
if (boundary) {
|
|
1319
1025
|
const result = await this.tryRenderErrorModule(boundary.modulePath, url, "boundary");
|
|
1320
1026
|
if (result)
|
|
1321
1027
|
return result;
|
|
1322
1028
|
}
|
|
1323
|
-
const errorHandler = this.
|
|
1029
|
+
const errorHandler = await this.pipeline.getErrorHandler();
|
|
1324
1030
|
if (errorHandler) {
|
|
1325
1031
|
const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, "handler");
|
|
1326
1032
|
if (result)
|
|
@@ -1329,15 +1035,12 @@ var SsrRenderer = class _SsrRenderer {
|
|
|
1329
1035
|
return { content: this.renderErrorPage(error, url), status: 500 };
|
|
1330
1036
|
}
|
|
1331
1037
|
}
|
|
1332
|
-
/**
|
|
1333
|
-
* Render a matched page by composing the route hierarchy.
|
|
1334
|
-
*/
|
|
1335
1038
|
async renderPage(routeInfo, matched, signal) {
|
|
1336
|
-
const hierarchy = this.
|
|
1039
|
+
const hierarchy = this.pipeline.buildRouteHierarchy(matched.route.pattern);
|
|
1337
1040
|
const segments = [];
|
|
1338
|
-
for (let i = 0;
|
|
1041
|
+
for (let i = 0;i < hierarchy.length; i++) {
|
|
1339
1042
|
const routePattern = hierarchy[i];
|
|
1340
|
-
let route = this.
|
|
1043
|
+
let route = await this.pipeline.findRoute(routePattern);
|
|
1341
1044
|
if (!route && routePattern === "/") {
|
|
1342
1045
|
route = DEFAULT_ROOT_ROUTE;
|
|
1343
1046
|
}
|
|
@@ -1351,7 +1054,7 @@ var SsrRenderer = class _SsrRenderer {
|
|
|
1351
1054
|
let result = "";
|
|
1352
1055
|
let pageTitle;
|
|
1353
1056
|
let lastRenderedPattern = "";
|
|
1354
|
-
for (let i = 0;
|
|
1057
|
+
for (let i = 0;i < segments.length; i++) {
|
|
1355
1058
|
const { content, title } = results[i];
|
|
1356
1059
|
if (title) {
|
|
1357
1060
|
pageTitle = title;
|
|
@@ -1361,59 +1064,57 @@ var SsrRenderer = class _SsrRenderer {
|
|
|
1361
1064
|
} else {
|
|
1362
1065
|
const injected = this.injectSlot(result, content, lastRenderedPattern);
|
|
1363
1066
|
if (injected === result) {
|
|
1364
|
-
logger.warn(`[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> for child route "${hierarchy[i]}" to render into. Add <router-slot></router-slot> to the parent template.`);
|
|
1067
|
+
this.logger.warn(`[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> ` + `for child route "${hierarchy[i]}" to render into. ` + `Add <router-slot></router-slot> to the parent template.`);
|
|
1365
1068
|
}
|
|
1366
1069
|
result = injected;
|
|
1367
1070
|
}
|
|
1368
1071
|
lastRenderedPattern = segments[i].route.pattern;
|
|
1369
1072
|
}
|
|
1370
1073
|
result = this.stripSlots(result);
|
|
1371
|
-
return { content: result, title: pageTitle };
|
|
1074
|
+
return { content: result, ...pageTitle !== undefined ? { title: pageTitle } : {} };
|
|
1372
1075
|
}
|
|
1373
|
-
/** Load component, build context, get data, render content, get title. */
|
|
1374
1076
|
async loadRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1375
1077
|
const files = route.files ?? {};
|
|
1376
1078
|
const tsModule = files.ts ?? files.js;
|
|
1377
|
-
const
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
1079
|
+
const loadedModule = tsModule ? await this.pipeline.loadModule(tsModule) : undefined;
|
|
1080
|
+
const component = loadedModule?.default ?? page_component_default;
|
|
1081
|
+
const context = await this.pipeline.buildContext(routeInfo, route, signal, isLeaf, loadedModule);
|
|
1082
|
+
const data = await component.getData({ params: routeInfo.params, ...signal ? { signal } : {}, context });
|
|
1380
1083
|
const content = this.renderContent(component, { data, params: routeInfo.params, context });
|
|
1381
1084
|
const title = component.getTitle({ data, params: routeInfo.params, context });
|
|
1382
|
-
return { content, title };
|
|
1085
|
+
return { content, ...title !== undefined ? { title } : {} };
|
|
1383
1086
|
}
|
|
1384
|
-
/** Render a component for error boundary/handler with minimal context. */
|
|
1385
1087
|
renderComponent(component, data, context) {
|
|
1386
1088
|
return this.renderContent(component, { data, params: {}, context });
|
|
1387
1089
|
}
|
|
1388
1090
|
static EMPTY_URL = new URL("http://error");
|
|
1389
|
-
/** Try to load and render an error boundary or handler module. Returns null on failure. */
|
|
1390
1091
|
async tryRenderErrorModule(modulePath, url, kind) {
|
|
1391
1092
|
try {
|
|
1392
|
-
const module = await this.
|
|
1093
|
+
const module = await this.pipeline.loadModule(modulePath);
|
|
1393
1094
|
const component = module.default;
|
|
1394
1095
|
const minCtx = {
|
|
1395
|
-
url:
|
|
1096
|
+
url: SsrRenderer.EMPTY_URL,
|
|
1396
1097
|
params: {},
|
|
1397
1098
|
pathname: "",
|
|
1398
|
-
searchParams: new URLSearchParams
|
|
1099
|
+
searchParams: new URLSearchParams
|
|
1399
1100
|
};
|
|
1400
1101
|
const data = await component.getData({ params: {}, context: minCtx });
|
|
1401
1102
|
const content = this.renderComponent(component, data, minCtx);
|
|
1402
1103
|
return { content, status: 500 };
|
|
1403
1104
|
} catch (e) {
|
|
1404
|
-
logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e :
|
|
1105
|
+
this.logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e : undefined);
|
|
1405
1106
|
return null;
|
|
1406
1107
|
}
|
|
1407
1108
|
}
|
|
1408
|
-
}
|
|
1109
|
+
}
|
|
1409
1110
|
|
|
1410
|
-
// dist/
|
|
1411
|
-
|
|
1111
|
+
// dist/core/renderer/html.renderer.js
|
|
1112
|
+
class SsrHtmlRenderer extends SsrRenderer {
|
|
1412
1113
|
label = "SSR HTML";
|
|
1413
1114
|
markdownRenderer;
|
|
1414
1115
|
markdownReady = null;
|
|
1415
|
-
constructor(
|
|
1416
|
-
super(
|
|
1116
|
+
constructor(pipeline, options = {}) {
|
|
1117
|
+
super(pipeline, options);
|
|
1417
1118
|
this.markdownRenderer = options.markdownRenderer ?? null;
|
|
1418
1119
|
if (this.markdownRenderer?.init) {
|
|
1419
1120
|
this.markdownReady = this.markdownRenderer.init();
|
|
@@ -1426,9 +1127,6 @@ var SsrHtmlRouter = class extends SsrRenderer {
|
|
|
1426
1127
|
stripSlots(result) {
|
|
1427
1128
|
return result.replace(/<router-slot[^>]*><\/router-slot>/g, "");
|
|
1428
1129
|
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Render a single route's content.
|
|
1431
|
-
*/
|
|
1432
1130
|
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1433
1131
|
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
1434
1132
|
return { content: `<router-slot pattern="${route.pattern}"></router-slot>` };
|
|
@@ -1440,10 +1138,10 @@ var SsrHtmlRouter = class extends SsrRenderer {
|
|
|
1440
1138
|
if (this.widgets) {
|
|
1441
1139
|
content = await resolveWidgetTags(content, this.widgets, routeInfo, (name, declared) => {
|
|
1442
1140
|
const files = this.widgetFiles[name] ?? declared;
|
|
1443
|
-
return files ? this.
|
|
1444
|
-
}, this.
|
|
1141
|
+
return files ? this.pipeline.loadFiles(files) : Promise.resolve({});
|
|
1142
|
+
}, this.pipeline.contextProvider, this.logger);
|
|
1445
1143
|
}
|
|
1446
|
-
return { content, title };
|
|
1144
|
+
return { content, ...title !== undefined ? { title } : {} };
|
|
1447
1145
|
}
|
|
1448
1146
|
renderContent(component, args) {
|
|
1449
1147
|
return component.renderHTML(args);
|
|
@@ -1465,14 +1163,9 @@ var SsrHtmlRouter = class extends SsrRenderer {
|
|
|
1465
1163
|
<p>${escapeHtml(message)}</p>
|
|
1466
1164
|
`;
|
|
1467
1165
|
}
|
|
1468
|
-
/** Add pattern attribute to bare <router-slot> tags. */
|
|
1469
1166
|
attributeSlots(content, routePattern) {
|
|
1470
1167
|
return content.replace(/<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g, `<router-slot pattern="${routePattern}"$1></router-slot>`);
|
|
1471
1168
|
}
|
|
1472
|
-
/**
|
|
1473
|
-
* Expand <mark-down> tags by rendering markdown to HTML server-side.
|
|
1474
|
-
* Leaves content unchanged if no markdown renderer is configured.
|
|
1475
|
-
*/
|
|
1476
1169
|
async expandMarkdown(content) {
|
|
1477
1170
|
if (!this.markdownRenderer)
|
|
1478
1171
|
return content;
|
|
@@ -1485,13 +1178,12 @@ var SsrHtmlRouter = class extends SsrRenderer {
|
|
|
1485
1178
|
const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
|
|
1486
1179
|
return content.replace(pattern, (_match, escaped) => {
|
|
1487
1180
|
const markdown = unescapeHtml(escaped);
|
|
1488
|
-
|
|
1489
|
-
return rendered;
|
|
1181
|
+
return renderer.render(markdown);
|
|
1490
1182
|
});
|
|
1491
1183
|
}
|
|
1492
|
-
}
|
|
1184
|
+
}
|
|
1493
1185
|
|
|
1494
|
-
// dist/
|
|
1186
|
+
// dist/core/widget/widget.parser.js
|
|
1495
1187
|
var WIDGET_PATTERN = /```widget:(?<name>[a-z][a-z0-9-]*)\n(?<params>.*?)```/gs;
|
|
1496
1188
|
function parseWidgetBlocks(markdown) {
|
|
1497
1189
|
const blocks = [];
|
|
@@ -1534,17 +1226,18 @@ function replaceWidgetBlocks(markdown, replacements) {
|
|
|
1534
1226
|
return result;
|
|
1535
1227
|
}
|
|
1536
1228
|
|
|
1537
|
-
// dist/
|
|
1229
|
+
// dist/core/renderer/md.renderer.js
|
|
1538
1230
|
var BARE_SLOT_BLOCK = "```router-slot\n```";
|
|
1539
1231
|
function routerSlotBlock(pattern) {
|
|
1540
1232
|
return `\`\`\`router-slot
|
|
1541
1233
|
{"pattern":"${pattern}"}
|
|
1542
1234
|
\`\`\``;
|
|
1543
1235
|
}
|
|
1544
|
-
|
|
1236
|
+
|
|
1237
|
+
class SsrMdRenderer extends SsrRenderer {
|
|
1545
1238
|
label = "SSR MD";
|
|
1546
|
-
constructor(
|
|
1547
|
-
super(
|
|
1239
|
+
constructor(pipeline, options = {}) {
|
|
1240
|
+
super(pipeline, options);
|
|
1548
1241
|
}
|
|
1549
1242
|
injectSlot(parent, child, parentPattern) {
|
|
1550
1243
|
return parent.replace(routerSlotBlock(parentPattern), child);
|
|
@@ -1552,9 +1245,6 @@ var SsrMdRouter = class extends SsrRenderer {
|
|
|
1552
1245
|
stripSlots(result) {
|
|
1553
1246
|
return result.replace(/```router-slot\n(?:\{[^}]*\}\n)?```/g, "").trim();
|
|
1554
1247
|
}
|
|
1555
|
-
/**
|
|
1556
|
-
* Render a single route's content to Markdown.
|
|
1557
|
-
*/
|
|
1558
1248
|
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1559
1249
|
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
1560
1250
|
return { content: routerSlotBlock(route.pattern) };
|
|
@@ -1565,7 +1255,7 @@ var SsrMdRouter = class extends SsrRenderer {
|
|
|
1565
1255
|
if (this.widgets) {
|
|
1566
1256
|
content = await this.resolveWidgets(content, routeInfo);
|
|
1567
1257
|
}
|
|
1568
|
-
return { content, title };
|
|
1258
|
+
return { content, ...title !== undefined ? { title } : {} };
|
|
1569
1259
|
}
|
|
1570
1260
|
renderContent(component, args) {
|
|
1571
1261
|
return component.renderMarkdown(args);
|
|
@@ -1583,10 +1273,6 @@ Path: \`${url.pathname}\``;
|
|
|
1583
1273
|
|
|
1584
1274
|
Path: \`${url.pathname}\``;
|
|
1585
1275
|
}
|
|
1586
|
-
/**
|
|
1587
|
-
* Resolve fenced widget blocks in markdown content.
|
|
1588
|
-
* Replaces ```widget:name blocks with rendered markdown output.
|
|
1589
|
-
*/
|
|
1590
1276
|
resolveWidgets(content, routeInfo) {
|
|
1591
1277
|
return resolveRecursively(content, parseWidgetBlocks, async (block) => {
|
|
1592
1278
|
if (block.parseError || !block.params) {
|
|
@@ -1600,33 +1286,34 @@ Path: \`${url.pathname}\``;
|
|
|
1600
1286
|
let files;
|
|
1601
1287
|
const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
|
|
1602
1288
|
if (filePaths) {
|
|
1603
|
-
files = await this.
|
|
1289
|
+
files = await this.pipeline.loadFiles(filePaths);
|
|
1604
1290
|
}
|
|
1605
1291
|
const baseContext = {
|
|
1606
1292
|
...routeInfo,
|
|
1607
1293
|
pathname: routeInfo.url.pathname,
|
|
1608
1294
|
searchParams: routeInfo.url.searchParams,
|
|
1609
|
-
files
|
|
1295
|
+
...files ? { files } : {}
|
|
1610
1296
|
};
|
|
1611
|
-
const context = this.
|
|
1297
|
+
const context = this.pipeline.contextProvider ? this.pipeline.contextProvider(baseContext) : baseContext;
|
|
1612
1298
|
const data = await widget.getData({ params: block.params, context });
|
|
1613
1299
|
return widget.renderMarkdown({ data, params: block.params, context });
|
|
1614
1300
|
} catch (e) {
|
|
1615
1301
|
return widget.renderMarkdownError(e);
|
|
1616
1302
|
}
|
|
1617
|
-
}, replaceWidgetBlocks);
|
|
1303
|
+
}, replaceWidgetBlocks, 0, this.logger);
|
|
1618
1304
|
}
|
|
1619
|
-
}
|
|
1305
|
+
}
|
|
1620
1306
|
|
|
1621
|
-
// dist/
|
|
1307
|
+
// dist/core/util/md.util.js
|
|
1622
1308
|
function rewriteMdLinks(markdown, base, skipPrefixes) {
|
|
1623
1309
|
const prefix = base + "/";
|
|
1624
1310
|
const skip = skipPrefixes.map((p) => p.slice(1) + "/").join("|");
|
|
1625
1311
|
const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, "g");
|
|
1626
1312
|
const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, "g");
|
|
1627
|
-
const lines = markdown.split(
|
|
1313
|
+
const lines = markdown.split(`
|
|
1314
|
+
`);
|
|
1628
1315
|
let inCodeBlock = false;
|
|
1629
|
-
for (let i = 0;
|
|
1316
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1630
1317
|
if (lines[i].startsWith("```")) {
|
|
1631
1318
|
inCodeBlock = !inCodeBlock;
|
|
1632
1319
|
continue;
|
|
@@ -1636,10 +1323,239 @@ function rewriteMdLinks(markdown, base, skipPrefixes) {
|
|
|
1636
1323
|
lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
|
|
1637
1324
|
lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
|
|
1638
1325
|
}
|
|
1639
|
-
return lines.join(
|
|
1326
|
+
return lines.join(`
|
|
1327
|
+
`);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// dist/core/server/emroute.server.js
|
|
1331
|
+
var DEFAULT_BASE_PATH = { html: "/html", md: "/md", app: "/app" };
|
|
1332
|
+
|
|
1333
|
+
class Emroute {
|
|
1334
|
+
runtime;
|
|
1335
|
+
htmlBase;
|
|
1336
|
+
mdBase;
|
|
1337
|
+
appBase;
|
|
1338
|
+
spa;
|
|
1339
|
+
title;
|
|
1340
|
+
htmlRenderer;
|
|
1341
|
+
mdRenderer;
|
|
1342
|
+
shell;
|
|
1343
|
+
constructor(htmlRenderer, mdRenderer, shell, runtime, htmlBase, mdBase, appBase, spa, title) {
|
|
1344
|
+
this.runtime = runtime;
|
|
1345
|
+
this.htmlBase = htmlBase;
|
|
1346
|
+
this.mdBase = mdBase;
|
|
1347
|
+
this.appBase = appBase;
|
|
1348
|
+
this.spa = spa;
|
|
1349
|
+
this.title = title;
|
|
1350
|
+
this.htmlRenderer = htmlRenderer;
|
|
1351
|
+
this.mdRenderer = mdRenderer;
|
|
1352
|
+
this.shell = shell;
|
|
1353
|
+
}
|
|
1354
|
+
static async create(config, runtime) {
|
|
1355
|
+
const { spa = "root" } = config;
|
|
1356
|
+
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
1357
|
+
const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
|
|
1358
|
+
if (manifestResponse.status === 404 && !config.routeTree) {
|
|
1359
|
+
throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. ` + "Provide routeTree in config or ensure the runtime produces it.");
|
|
1360
|
+
}
|
|
1361
|
+
if (config.routeTree && manifestResponse.status === 404) {
|
|
1362
|
+
await runtime.command(ROUTES_MANIFEST_PATH, {
|
|
1363
|
+
body: JSON.stringify(config.routeTree)
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
const pipeline = new Pipeline({
|
|
1367
|
+
runtime,
|
|
1368
|
+
...config.extendContext ? { contextProvider: config.extendContext } : {},
|
|
1369
|
+
...config.moduleLoaders ? { moduleLoaders: config.moduleLoaders } : {}
|
|
1370
|
+
});
|
|
1371
|
+
let widgets = config.widgets;
|
|
1372
|
+
let widgetFiles = {};
|
|
1373
|
+
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
1374
|
+
if (widgetsResponse.status !== 404) {
|
|
1375
|
+
const entries = await widgetsResponse.json();
|
|
1376
|
+
if (config.widgets) {
|
|
1377
|
+
widgets = config.widgets;
|
|
1378
|
+
for (const entry of entries) {
|
|
1379
|
+
if (entry.files)
|
|
1380
|
+
widgetFiles[entry.name] = entry.files;
|
|
1381
|
+
}
|
|
1382
|
+
} else {
|
|
1383
|
+
const imported = await Emroute.importWidgets(entries, runtime);
|
|
1384
|
+
widgets = imported.registry;
|
|
1385
|
+
widgetFiles = imported.widgetFiles;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
let ssrHtmlRenderer = null;
|
|
1389
|
+
let ssrMdRenderer = null;
|
|
1390
|
+
if (spa !== "only") {
|
|
1391
|
+
ssrHtmlRenderer = new SsrHtmlRenderer(pipeline, {
|
|
1392
|
+
...config.markdownRenderer ? { markdownRenderer: config.markdownRenderer } : {},
|
|
1393
|
+
...widgets ? { widgets } : {},
|
|
1394
|
+
widgetFiles
|
|
1395
|
+
});
|
|
1396
|
+
ssrMdRenderer = new SsrMdRenderer(pipeline, {
|
|
1397
|
+
...widgets ? { widgets } : {},
|
|
1398
|
+
widgetFiles
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
const title = config.title ?? "emroute";
|
|
1402
|
+
const shellBase = spa === "root" || spa === "only" ? appBase : htmlBase;
|
|
1403
|
+
let shell = await Emroute.resolveShell(runtime, title, shellBase);
|
|
1404
|
+
if ((await runtime.query("/main.css")).status !== 404) {
|
|
1405
|
+
shell = shell.replace("</head>", ` <link rel="stylesheet" href="/main.css">
|
|
1406
|
+
</head>`);
|
|
1407
|
+
}
|
|
1408
|
+
return new Emroute(ssrHtmlRenderer, ssrMdRenderer, shell, runtime, htmlBase, mdBase, appBase, spa, title);
|
|
1409
|
+
}
|
|
1410
|
+
async handleRequest(req) {
|
|
1411
|
+
const url = new URL(req.url);
|
|
1412
|
+
const pathname = url.pathname;
|
|
1413
|
+
const mdPrefix = this.mdBase + "/";
|
|
1414
|
+
const htmlPrefix = this.htmlBase + "/";
|
|
1415
|
+
const appPrefix = this.appBase + "/";
|
|
1416
|
+
if (this.mdRenderer && (pathname.startsWith(mdPrefix) || pathname === this.mdBase)) {
|
|
1417
|
+
const routePath = pathname === this.mdBase ? "/" : pathname.slice(this.mdBase.length);
|
|
1418
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
1419
|
+
const canonical = this.mdBase + routePath.slice(0, -1) + (url.search || "");
|
|
1420
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
1421
|
+
}
|
|
1422
|
+
try {
|
|
1423
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
1424
|
+
const { content, status, redirect } = await this.mdRenderer.render(routeUrl, req.signal);
|
|
1425
|
+
if (redirect) {
|
|
1426
|
+
const target = redirect.startsWith("/") ? this.mdBase + redirect : redirect;
|
|
1427
|
+
return Response.redirect(new URL(target, url.origin), status);
|
|
1428
|
+
}
|
|
1429
|
+
return new Response(rewriteMdLinks(content, this.mdBase, [this.mdBase, this.htmlBase]), {
|
|
1430
|
+
status,
|
|
1431
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
|
|
1432
|
+
});
|
|
1433
|
+
} catch (e) {
|
|
1434
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
1435
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (this.htmlRenderer && (pathname.startsWith(htmlPrefix) || pathname === this.htmlBase)) {
|
|
1439
|
+
const routePath = pathname === this.htmlBase ? "/" : pathname.slice(this.htmlBase.length);
|
|
1440
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
1441
|
+
const canonical = this.htmlBase + routePath.slice(0, -1) + (url.search || "");
|
|
1442
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
1446
|
+
const result = await this.htmlRenderer.render(routeUrl, req.signal);
|
|
1447
|
+
if (result.redirect) {
|
|
1448
|
+
const target = result.redirect.startsWith("/") ? this.htmlBase + result.redirect : result.redirect;
|
|
1449
|
+
return Response.redirect(new URL(target, url.origin), result.status);
|
|
1450
|
+
}
|
|
1451
|
+
const ssrTitle = result.title ?? this.title;
|
|
1452
|
+
const html = Emroute.injectSsrContent(this.shell, result.content, ssrTitle, pathname);
|
|
1453
|
+
return new Response(html, {
|
|
1454
|
+
status: result.status,
|
|
1455
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1456
|
+
});
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
1459
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (pathname.startsWith(appPrefix) || pathname === this.appBase) {
|
|
1463
|
+
return new Response(this.shell, {
|
|
1464
|
+
status: 200,
|
|
1465
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
if (pathname.startsWith(htmlPrefix) || pathname === this.htmlBase || pathname.startsWith(mdPrefix) || pathname === this.mdBase) {
|
|
1469
|
+
return new Response(this.shell, {
|
|
1470
|
+
status: 200,
|
|
1471
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
const lastSegment = pathname.split("/").pop() ?? "";
|
|
1475
|
+
if (lastSegment.includes(".")) {
|
|
1476
|
+
const fileResponse = await this.runtime.handle(pathname);
|
|
1477
|
+
if (fileResponse.status === 200)
|
|
1478
|
+
return fileResponse;
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
const base = this.spa === "root" || this.spa === "only" ? this.appBase : this.htmlBase;
|
|
1482
|
+
const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
|
|
1483
|
+
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
1484
|
+
}
|
|
1485
|
+
static extractWidgetExport(mod) {
|
|
1486
|
+
for (const value of Object.values(mod)) {
|
|
1487
|
+
if (!value)
|
|
1488
|
+
continue;
|
|
1489
|
+
if (typeof value === "object" && "getData" in value) {
|
|
1490
|
+
return value;
|
|
1491
|
+
}
|
|
1492
|
+
if (typeof value === "function" && value.prototype?.getData) {
|
|
1493
|
+
return new value;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
static async importWidgets(entries, runtime) {
|
|
1499
|
+
const registry = new WidgetRegistry;
|
|
1500
|
+
const widgetFiles = {};
|
|
1501
|
+
for (const entry of entries) {
|
|
1502
|
+
try {
|
|
1503
|
+
const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
|
|
1504
|
+
const mod = await runtime.loadModule(runtimePath);
|
|
1505
|
+
const instance = Emroute.extractWidgetExport(mod);
|
|
1506
|
+
if (!instance)
|
|
1507
|
+
continue;
|
|
1508
|
+
registry.add(instance);
|
|
1509
|
+
const inlined = mod.__files;
|
|
1510
|
+
if (inlined && typeof inlined === "object") {
|
|
1511
|
+
widgetFiles[entry.name] = inlined;
|
|
1512
|
+
} else if (entry.files) {
|
|
1513
|
+
widgetFiles[entry.name] = entry.files;
|
|
1514
|
+
}
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
1517
|
+
if (entry.files)
|
|
1518
|
+
widgetFiles[entry.name] = entry.files;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return { registry, widgetFiles };
|
|
1522
|
+
}
|
|
1523
|
+
static buildHtmlShell(title, htmlBase) {
|
|
1524
|
+
const baseTag = htmlBase ? `
|
|
1525
|
+
<base href="${escapeHtml(htmlBase)}/">` : "";
|
|
1526
|
+
return `<!DOCTYPE html>
|
|
1527
|
+
<html>
|
|
1528
|
+
<head>${baseTag}
|
|
1529
|
+
<meta charset="utf-8">
|
|
1530
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1531
|
+
<title>${escapeHtml(title)}</title>
|
|
1532
|
+
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
1533
|
+
</head>
|
|
1534
|
+
<body>
|
|
1535
|
+
<router-slot></router-slot>
|
|
1536
|
+
</body>
|
|
1537
|
+
</html>`;
|
|
1538
|
+
}
|
|
1539
|
+
static injectSsrContent(html, content, title, ssrRoute) {
|
|
1540
|
+
const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
|
|
1541
|
+
if (!slotPattern.test(html))
|
|
1542
|
+
return html;
|
|
1543
|
+
const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
|
|
1544
|
+
html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
|
|
1545
|
+
if (title) {
|
|
1546
|
+
html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
1547
|
+
}
|
|
1548
|
+
return html;
|
|
1549
|
+
}
|
|
1550
|
+
static async resolveShell(runtime, title, htmlBase) {
|
|
1551
|
+
const response = await runtime.query("/index.html");
|
|
1552
|
+
if (response.status !== 404)
|
|
1553
|
+
return await response.text();
|
|
1554
|
+
return Emroute.buildHtmlShell(title, htmlBase);
|
|
1555
|
+
}
|
|
1640
1556
|
}
|
|
1641
1557
|
|
|
1642
|
-
// dist/
|
|
1558
|
+
// dist/core/util/route-tree.util.js
|
|
1643
1559
|
function resolveTargetNode(node, name, isRoot) {
|
|
1644
1560
|
if (name === "index") {
|
|
1645
1561
|
if (isRoot)
|
|
@@ -1658,34 +1574,81 @@ function resolveTargetNode(node, name, isRoot) {
|
|
|
1658
1574
|
}
|
|
1659
1575
|
|
|
1660
1576
|
// dist/runtime/abstract.runtime.js
|
|
1577
|
+
var CONTENT_TYPES = new Map([
|
|
1578
|
+
[".html", "text/html; charset=utf-8"],
|
|
1579
|
+
[".css", "text/css; charset=utf-8"],
|
|
1580
|
+
[".js", "application/javascript; charset=utf-8"],
|
|
1581
|
+
[".mjs", "application/javascript; charset=utf-8"],
|
|
1582
|
+
[".ts", "text/typescript; charset=utf-8"],
|
|
1583
|
+
[".json", "application/json; charset=utf-8"],
|
|
1584
|
+
[".md", "text/plain; charset=utf-8"],
|
|
1585
|
+
[".txt", "text/plain; charset=utf-8"],
|
|
1586
|
+
[".wasm", "application/wasm"],
|
|
1587
|
+
[".map", "application/json; charset=utf-8"],
|
|
1588
|
+
[".png", "image/png"],
|
|
1589
|
+
[".jpg", "image/jpeg"],
|
|
1590
|
+
[".jpeg", "image/jpeg"],
|
|
1591
|
+
[".gif", "image/gif"],
|
|
1592
|
+
[".svg", "image/svg+xml"],
|
|
1593
|
+
[".ico", "image/x-icon"],
|
|
1594
|
+
[".webp", "image/webp"],
|
|
1595
|
+
[".avif", "image/avif"],
|
|
1596
|
+
[".woff", "font/woff"],
|
|
1597
|
+
[".woff2", "font/woff2"],
|
|
1598
|
+
[".ttf", "font/ttf"]
|
|
1599
|
+
]);
|
|
1661
1600
|
var DEFAULT_ROUTES_DIR = "/routes";
|
|
1662
1601
|
var DEFAULT_WIDGETS_DIR = "/widgets";
|
|
1663
|
-
var
|
|
1664
|
-
|
|
1665
|
-
var Runtime = class {
|
|
1602
|
+
var DEFAULT_ELEMENTS_DIR = "/elements";
|
|
1603
|
+
class Runtime {
|
|
1666
1604
|
config;
|
|
1667
1605
|
constructor(config = {}) {
|
|
1668
1606
|
this.config = config;
|
|
1669
1607
|
this.config = config;
|
|
1670
1608
|
}
|
|
1671
|
-
/** Write. Defaults to PUT; pass `{ method: "DELETE" }` etc. to override. */
|
|
1672
1609
|
command(resource, options) {
|
|
1673
1610
|
const path = typeof resource === "string" ? resource : new URL(resource instanceof Request ? resource.url : resource.toString()).pathname;
|
|
1674
|
-
const
|
|
1611
|
+
const method = options?.method ?? "PUT";
|
|
1612
|
+
const isDelete = method === "DELETE";
|
|
1613
|
+
const result = this.handle(resource, { method, ...options });
|
|
1675
1614
|
const routesDir = this.config.routesDir ?? DEFAULT_ROUTES_DIR;
|
|
1615
|
+
const widgetsDir = this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR;
|
|
1616
|
+
const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
|
|
1676
1617
|
if (path.startsWith(routesDir + "/")) {
|
|
1677
1618
|
return result.then(async (res) => {
|
|
1678
|
-
|
|
1619
|
+
if (isDelete) {
|
|
1620
|
+
await this.pruneRouteFromManifest(path, routesDir);
|
|
1621
|
+
} else {
|
|
1622
|
+
await this.mergeRouteIntoManifest(path, routesDir);
|
|
1623
|
+
await this.retranspileIfNeeded(path, routesDir, "route");
|
|
1624
|
+
}
|
|
1625
|
+
return res;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
if (path.startsWith(widgetsDir + "/")) {
|
|
1629
|
+
return result.then(async (res) => {
|
|
1630
|
+
if (isDelete) {
|
|
1631
|
+
await this.pruneWidgetFromManifest(path, widgetsDir);
|
|
1632
|
+
} else {
|
|
1633
|
+
await this.mergeWidgetIntoManifest(path, widgetsDir);
|
|
1634
|
+
await this.retranspileIfNeeded(path, widgetsDir, "widget");
|
|
1635
|
+
}
|
|
1636
|
+
return res;
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
if (path.startsWith(elementsDir + "/")) {
|
|
1640
|
+
return result.then(async (res) => {
|
|
1641
|
+
if (isDelete) {
|
|
1642
|
+
await this.pruneElementFromManifest(path, elementsDir);
|
|
1643
|
+
} else {
|
|
1644
|
+
await this.mergeElementIntoManifest(path, elementsDir);
|
|
1645
|
+
await this.retranspileIfNeeded(path, elementsDir, "element");
|
|
1646
|
+
}
|
|
1679
1647
|
return res;
|
|
1680
1648
|
});
|
|
1681
1649
|
}
|
|
1682
1650
|
return result;
|
|
1683
1651
|
}
|
|
1684
|
-
/**
|
|
1685
|
-
* Parse a single route file path and merge it into the stored manifest.
|
|
1686
|
-
* Avoids a full directory rescan — just reads the current manifest,
|
|
1687
|
-
* inserts the new entry, and writes it back.
|
|
1688
|
-
*/
|
|
1689
1652
|
async mergeRouteIntoManifest(filePath, routesDir) {
|
|
1690
1653
|
const relativePath = filePath.slice(routesDir.length + 1);
|
|
1691
1654
|
const parts = relativePath.split("/");
|
|
@@ -1694,64 +1657,336 @@ var Runtime = class {
|
|
|
1694
1657
|
const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
|
|
1695
1658
|
if (!match)
|
|
1696
1659
|
return;
|
|
1697
|
-
const [, name, kind, ext] = match;
|
|
1698
|
-
const response = await this.handle(ROUTES_MANIFEST_PATH);
|
|
1699
|
-
const tree = response.status === 404 ? {} : await response.json();
|
|
1700
|
-
let node = tree;
|
|
1701
|
-
for (const dir of dirSegments) {
|
|
1702
|
-
if (dir.startsWith("[") && dir.endsWith("]")) {
|
|
1703
|
-
const param = dir.slice(1, -1);
|
|
1704
|
-
node.dynamic ??= { param, child: {} };
|
|
1705
|
-
node = node.dynamic.child;
|
|
1660
|
+
const [, name, kind, ext] = match;
|
|
1661
|
+
const response = await this.handle(ROUTES_MANIFEST_PATH);
|
|
1662
|
+
const tree = response.status === 404 ? {} : await response.json();
|
|
1663
|
+
let node = tree;
|
|
1664
|
+
for (const dir of dirSegments) {
|
|
1665
|
+
if (dir.startsWith("[") && dir.endsWith("]")) {
|
|
1666
|
+
const param = dir.slice(1, -1);
|
|
1667
|
+
node.dynamic ??= { param, child: {} };
|
|
1668
|
+
node = node.dynamic.child;
|
|
1669
|
+
} else {
|
|
1670
|
+
node.children ??= {};
|
|
1671
|
+
node.children[dir] ??= {};
|
|
1672
|
+
node = node.children[dir];
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
if (kind === "error") {
|
|
1676
|
+
node.errorBoundary = filePath;
|
|
1677
|
+
} else {
|
|
1678
|
+
const target = resolveTargetNode(node, name, dirSegments.length === 0);
|
|
1679
|
+
if (kind === "redirect") {
|
|
1680
|
+
target.redirect = filePath;
|
|
1681
|
+
} else {
|
|
1682
|
+
target.files ??= {};
|
|
1683
|
+
target.files[ext] = filePath;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
this.routesManifestCache = null;
|
|
1687
|
+
await this.handle(ROUTES_MANIFEST_PATH, {
|
|
1688
|
+
method: "PUT",
|
|
1689
|
+
body: JSON.stringify(tree)
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async pruneRouteFromManifest(filePath, routesDir) {
|
|
1693
|
+
const relativePath = filePath.slice(routesDir.length + 1);
|
|
1694
|
+
const parts = relativePath.split("/");
|
|
1695
|
+
const filename = parts[parts.length - 1];
|
|
1696
|
+
const dirSegments = parts.slice(0, -1);
|
|
1697
|
+
const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
|
|
1698
|
+
if (!match)
|
|
1699
|
+
return;
|
|
1700
|
+
const [, name, kind, ext] = match;
|
|
1701
|
+
const response = await this.handle(ROUTES_MANIFEST_PATH);
|
|
1702
|
+
if (response.status === 404)
|
|
1703
|
+
return;
|
|
1704
|
+
const tree = await response.json();
|
|
1705
|
+
const ancestors = [];
|
|
1706
|
+
let node = tree;
|
|
1707
|
+
for (const dir of dirSegments) {
|
|
1708
|
+
if (dir.startsWith("[") && dir.endsWith("]")) {
|
|
1709
|
+
if (!node.dynamic)
|
|
1710
|
+
return;
|
|
1711
|
+
ancestors.push({ node, key: dir, via: "dynamic" });
|
|
1712
|
+
node = node.dynamic.child;
|
|
1713
|
+
} else {
|
|
1714
|
+
if (!node.children?.[dir])
|
|
1715
|
+
return;
|
|
1716
|
+
ancestors.push({ node, key: dir, via: "children" });
|
|
1717
|
+
node = node.children[dir];
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (kind === "error") {
|
|
1721
|
+
if (node.errorBoundary === filePath)
|
|
1722
|
+
delete node.errorBoundary;
|
|
1723
|
+
} else {
|
|
1724
|
+
const isRoot = dirSegments.length === 0;
|
|
1725
|
+
const target = this.findTargetNode(node, name, isRoot);
|
|
1726
|
+
if (!target)
|
|
1727
|
+
return;
|
|
1728
|
+
if (kind === "redirect") {
|
|
1729
|
+
if (target.redirect === filePath)
|
|
1730
|
+
delete target.redirect;
|
|
1731
|
+
} else {
|
|
1732
|
+
if (target.files?.[ext] === filePath) {
|
|
1733
|
+
delete target.files[ext];
|
|
1734
|
+
if (Object.keys(target.files).length === 0)
|
|
1735
|
+
delete target.files;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (target !== node && this.isEmptyNode(target)) {
|
|
1739
|
+
if (name === "index" && !isRoot) {
|
|
1740
|
+
delete node.wildcard;
|
|
1741
|
+
} else if (name.startsWith("[") && name.endsWith("]")) {
|
|
1742
|
+
delete node.dynamic;
|
|
1743
|
+
} else if (node.children) {
|
|
1744
|
+
delete node.children[name];
|
|
1745
|
+
if (Object.keys(node.children).length === 0)
|
|
1746
|
+
delete node.children;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
for (let i = ancestors.length - 1;i >= 0; i--) {
|
|
1751
|
+
const { node: parent, key, via } = ancestors[i];
|
|
1752
|
+
const child = via === "dynamic" ? parent.dynamic?.child : parent.children?.[key];
|
|
1753
|
+
if (child && this.isEmptyNode(child)) {
|
|
1754
|
+
if (via === "dynamic") {
|
|
1755
|
+
delete parent.dynamic;
|
|
1756
|
+
} else if (parent.children) {
|
|
1757
|
+
delete parent.children[key];
|
|
1758
|
+
if (Object.keys(parent.children).length === 0)
|
|
1759
|
+
delete parent.children;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
this.routesManifestCache = null;
|
|
1764
|
+
await this.handle(ROUTES_MANIFEST_PATH, {
|
|
1765
|
+
method: "PUT",
|
|
1766
|
+
body: JSON.stringify(tree)
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
findTargetNode(node, name, isRoot) {
|
|
1770
|
+
if (name === "index") {
|
|
1771
|
+
return isRoot ? node : node.wildcard?.child ?? null;
|
|
1772
|
+
}
|
|
1773
|
+
if (name.startsWith("[") && name.endsWith("]")) {
|
|
1774
|
+
return node.dynamic?.child ?? null;
|
|
1775
|
+
}
|
|
1776
|
+
return node.children?.[name] ?? null;
|
|
1777
|
+
}
|
|
1778
|
+
isEmptyNode(node) {
|
|
1779
|
+
return !node.files && !node.errorBoundary && !node.redirect && !node.children && !node.dynamic && !node.wildcard;
|
|
1780
|
+
}
|
|
1781
|
+
async pruneWidgetFromManifest(filePath, widgetsDir) {
|
|
1782
|
+
const relativePath = filePath.slice(widgetsDir.length + 1);
|
|
1783
|
+
const parts = relativePath.split("/");
|
|
1784
|
+
if (parts.length !== 2)
|
|
1785
|
+
return;
|
|
1786
|
+
const [dirName, filename] = parts;
|
|
1787
|
+
const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
|
|
1788
|
+
if (!match)
|
|
1789
|
+
return;
|
|
1790
|
+
const [, name, ext] = match;
|
|
1791
|
+
if (name !== dirName)
|
|
1792
|
+
return;
|
|
1793
|
+
const response = await this.handle(WIDGETS_MANIFEST_PATH);
|
|
1794
|
+
if (response.status === 404)
|
|
1795
|
+
return;
|
|
1796
|
+
const entries = await response.json();
|
|
1797
|
+
if (ext === "ts" || ext === "js") {
|
|
1798
|
+
const idx = entries.findIndex((e) => e.name === name);
|
|
1799
|
+
if (idx === -1)
|
|
1800
|
+
return;
|
|
1801
|
+
entries.splice(idx, 1);
|
|
1802
|
+
} else {
|
|
1803
|
+
const entry = entries.find((e) => e.name === name);
|
|
1804
|
+
if (!entry?.files)
|
|
1805
|
+
return;
|
|
1806
|
+
delete entry.files[ext];
|
|
1807
|
+
if (Object.keys(entry.files).length === 0)
|
|
1808
|
+
delete entry.files;
|
|
1809
|
+
}
|
|
1810
|
+
this.widgetsManifestCache = null;
|
|
1811
|
+
await this.handle(WIDGETS_MANIFEST_PATH, {
|
|
1812
|
+
method: "PUT",
|
|
1813
|
+
body: JSON.stringify(entries)
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
async pruneElementFromManifest(filePath, elementsDir) {
|
|
1817
|
+
const relativePath = filePath.slice(elementsDir.length + 1);
|
|
1818
|
+
const parts = relativePath.split("/");
|
|
1819
|
+
if (parts.length !== 2)
|
|
1820
|
+
return;
|
|
1821
|
+
const [dirName, filename] = parts;
|
|
1822
|
+
const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
|
|
1823
|
+
if (!match)
|
|
1824
|
+
return;
|
|
1825
|
+
const [, name] = match;
|
|
1826
|
+
if (name !== dirName)
|
|
1827
|
+
return;
|
|
1828
|
+
const response = await this.handle(ELEMENTS_MANIFEST_PATH);
|
|
1829
|
+
if (response.status === 404)
|
|
1830
|
+
return;
|
|
1831
|
+
const entries = await response.json();
|
|
1832
|
+
const idx = entries.findIndex((e) => e.name === name);
|
|
1833
|
+
if (idx === -1)
|
|
1834
|
+
return;
|
|
1835
|
+
entries.splice(idx, 1);
|
|
1836
|
+
this.elementsManifestCache = null;
|
|
1837
|
+
await this.handle(ELEMENTS_MANIFEST_PATH, {
|
|
1838
|
+
method: "PUT",
|
|
1839
|
+
body: JSON.stringify(entries)
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
async retranspileIfNeeded(filePath, dir, kind) {
|
|
1843
|
+
if (filePath.endsWith(".js"))
|
|
1844
|
+
return;
|
|
1845
|
+
const relativePath = filePath.slice(dir.length + 1);
|
|
1846
|
+
const parts = relativePath.split("/");
|
|
1847
|
+
const filename = parts[parts.length - 1];
|
|
1848
|
+
let jsPath;
|
|
1849
|
+
if (kind === "route") {
|
|
1850
|
+
const match = filename.match(/^(.+?)\.(page)\.(ts|html|md|css)$/);
|
|
1851
|
+
if (!match)
|
|
1852
|
+
return;
|
|
1853
|
+
const [, name] = match;
|
|
1854
|
+
jsPath = `${dir}/${parts.slice(0, -1).join("/")}${parts.length > 1 ? "/" : ""}${name}.page.js`;
|
|
1855
|
+
} else if (kind === "widget") {
|
|
1856
|
+
const match = filename.match(/^(.+?)\.(widget)\.(ts|html|md|css)$/);
|
|
1857
|
+
if (!match)
|
|
1858
|
+
return;
|
|
1859
|
+
const [, name] = match;
|
|
1860
|
+
jsPath = `${dir}/${name}/${name}.widget.js`;
|
|
1861
|
+
} else {
|
|
1862
|
+
const match = filename.match(/^(.+?)\.(element)\.ts$/);
|
|
1863
|
+
if (!match)
|
|
1864
|
+
return;
|
|
1865
|
+
const [, name] = match;
|
|
1866
|
+
jsPath = `${dir}/${name}/${name}.element.js`;
|
|
1867
|
+
}
|
|
1868
|
+
const jsResponse = await this.handle(jsPath);
|
|
1869
|
+
if (jsResponse.status === 404)
|
|
1870
|
+
return;
|
|
1871
|
+
const tsPath = jsPath.replace(/\.js$/, ".ts");
|
|
1872
|
+
let tsSource;
|
|
1873
|
+
try {
|
|
1874
|
+
tsSource = await this.query(tsPath, { as: "text" });
|
|
1875
|
+
} catch {
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const companionExts = kind === "element" ? [] : ["html", "md", "css"];
|
|
1879
|
+
const files = {};
|
|
1880
|
+
for (const ext of companionExts) {
|
|
1881
|
+
const companionPath = tsPath.replace(/\.ts$/, `.${ext}`);
|
|
1882
|
+
try {
|
|
1883
|
+
files[ext] = await this.query(companionPath, { as: "text" });
|
|
1884
|
+
} catch {}
|
|
1885
|
+
}
|
|
1886
|
+
let jsCode;
|
|
1887
|
+
try {
|
|
1888
|
+
jsCode = await this.transpile(tsSource);
|
|
1889
|
+
} catch {
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
if (Object.keys(files).length > 0) {
|
|
1893
|
+
const entries = Object.entries(files).map(([k, v]) => `${k}: \`${v.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\``).join(", ");
|
|
1894
|
+
jsCode += `
|
|
1895
|
+
export const __files = { ${entries} };
|
|
1896
|
+
`;
|
|
1897
|
+
}
|
|
1898
|
+
await this.handle(jsPath, { method: "PUT", body: jsCode });
|
|
1899
|
+
}
|
|
1900
|
+
loadModule(_path) {
|
|
1901
|
+
throw new Error(`loadModule not implemented for ${this.constructor.name}`);
|
|
1902
|
+
}
|
|
1903
|
+
transpile(_source) {
|
|
1904
|
+
throw new Error(`transpile not implemented for ${this.constructor.name}`);
|
|
1905
|
+
}
|
|
1906
|
+
async mergeWidgetIntoManifest(filePath, widgetsDir) {
|
|
1907
|
+
const relativePath = filePath.slice(widgetsDir.length + 1);
|
|
1908
|
+
const parts = relativePath.split("/");
|
|
1909
|
+
if (parts.length !== 2)
|
|
1910
|
+
return;
|
|
1911
|
+
const [dirName, filename] = parts;
|
|
1912
|
+
const match = filename.match(/^(.+?)\.widget\.(ts|js|html|md|css)$/);
|
|
1913
|
+
if (!match)
|
|
1914
|
+
return;
|
|
1915
|
+
const [, name, ext] = match;
|
|
1916
|
+
if (name !== dirName)
|
|
1917
|
+
return;
|
|
1918
|
+
const response = await this.handle(WIDGETS_MANIFEST_PATH);
|
|
1919
|
+
const entries = response.status === 404 ? [] : await response.json();
|
|
1920
|
+
const prefix = widgetsDir.replace(/^\//, "");
|
|
1921
|
+
if (ext === "ts" || ext === "js") {
|
|
1922
|
+
let entry = entries.find((e) => e.name === name);
|
|
1923
|
+
if (!entry) {
|
|
1924
|
+
entry = {
|
|
1925
|
+
name,
|
|
1926
|
+
modulePath: `${prefix}/${name}/${filename}`,
|
|
1927
|
+
tagName: `widget-${name}`
|
|
1928
|
+
};
|
|
1929
|
+
entries.push(entry);
|
|
1930
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1706
1931
|
} else {
|
|
1707
|
-
|
|
1708
|
-
node.children[dir] ??= {};
|
|
1709
|
-
node = node.children[dir];
|
|
1932
|
+
entry.modulePath = `${prefix}/${name}/${filename}`;
|
|
1710
1933
|
}
|
|
1711
|
-
}
|
|
1712
|
-
if (kind === "error") {
|
|
1713
|
-
node.errorBoundary = filePath;
|
|
1714
1934
|
} else {
|
|
1715
|
-
const
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
target.files[ext] = filePath;
|
|
1721
|
-
}
|
|
1935
|
+
const entry = entries.find((e) => e.name === name);
|
|
1936
|
+
if (!entry)
|
|
1937
|
+
return;
|
|
1938
|
+
entry.files ??= {};
|
|
1939
|
+
entry.files[ext] = `${prefix}/${name}/${filename}`;
|
|
1722
1940
|
}
|
|
1723
|
-
this.
|
|
1724
|
-
await this.handle(
|
|
1941
|
+
this.widgetsManifestCache = null;
|
|
1942
|
+
await this.handle(WIDGETS_MANIFEST_PATH, {
|
|
1725
1943
|
method: "PUT",
|
|
1726
|
-
body: JSON.stringify(
|
|
1944
|
+
body: JSON.stringify(entries)
|
|
1727
1945
|
});
|
|
1728
1946
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1947
|
+
async mergeElementIntoManifest(filePath, elementsDir) {
|
|
1948
|
+
const relativePath = filePath.slice(elementsDir.length + 1);
|
|
1949
|
+
const parts = relativePath.split("/");
|
|
1950
|
+
if (parts.length !== 2)
|
|
1951
|
+
return;
|
|
1952
|
+
const [dirName, filename] = parts;
|
|
1953
|
+
const match = filename.match(/^(.+?)\.element\.(ts|js)$/);
|
|
1954
|
+
if (!match)
|
|
1955
|
+
return;
|
|
1956
|
+
const [, name] = match;
|
|
1957
|
+
if (name !== dirName)
|
|
1958
|
+
return;
|
|
1959
|
+
if (!name.includes("-"))
|
|
1960
|
+
return;
|
|
1961
|
+
const response = await this.handle(ELEMENTS_MANIFEST_PATH);
|
|
1962
|
+
const entries = response.status === 404 ? [] : await response.json();
|
|
1963
|
+
const prefix = elementsDir.replace(/^\//, "");
|
|
1964
|
+
let entry = entries.find((e) => e.name === name);
|
|
1965
|
+
if (!entry) {
|
|
1966
|
+
entry = {
|
|
1967
|
+
name,
|
|
1968
|
+
modulePath: `${prefix}/${name}/${filename}`,
|
|
1969
|
+
tagName: name
|
|
1970
|
+
};
|
|
1971
|
+
entries.push(entry);
|
|
1972
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1973
|
+
} else {
|
|
1974
|
+
entry.modulePath = `${prefix}/${name}/${filename}`;
|
|
1975
|
+
}
|
|
1976
|
+
this.elementsManifestCache = null;
|
|
1977
|
+
await this.handle(ELEMENTS_MANIFEST_PATH, {
|
|
1978
|
+
method: "PUT",
|
|
1979
|
+
body: JSON.stringify(entries)
|
|
1980
|
+
});
|
|
1742
1981
|
}
|
|
1743
|
-
// ── Manifest resolution ─────────────────────────────────────────────
|
|
1744
1982
|
routesManifestCache = null;
|
|
1745
1983
|
widgetsManifestCache = null;
|
|
1746
|
-
|
|
1984
|
+
elementsManifestCache = null;
|
|
1747
1985
|
invalidateManifests() {
|
|
1748
1986
|
this.routesManifestCache = null;
|
|
1749
1987
|
this.widgetsManifestCache = null;
|
|
1988
|
+
this.elementsManifestCache = null;
|
|
1750
1989
|
}
|
|
1751
|
-
/**
|
|
1752
|
-
* Resolve the routes manifest. Called when the concrete runtime returns
|
|
1753
|
-
* 404 for ROUTES_MANIFEST_PATH. Scans `config.routesDir` (or default).
|
|
1754
|
-
*/
|
|
1755
1990
|
async resolveRoutesManifest() {
|
|
1756
1991
|
if (this.routesManifestCache)
|
|
1757
1992
|
return this.routesManifestCache.clone();
|
|
@@ -1764,10 +1999,6 @@ var Runtime = class {
|
|
|
1764
1999
|
this.routesManifestCache = Response.json(tree);
|
|
1765
2000
|
return this.routesManifestCache.clone();
|
|
1766
2001
|
}
|
|
1767
|
-
/**
|
|
1768
|
-
* Resolve the widgets manifest. Called when the concrete runtime returns
|
|
1769
|
-
* 404 for WIDGETS_MANIFEST_PATH. Scans `config.widgetsDir` (or default).
|
|
1770
|
-
*/
|
|
1771
2002
|
async resolveWidgetsManifest() {
|
|
1772
2003
|
if (this.widgetsManifestCache)
|
|
1773
2004
|
return this.widgetsManifestCache.clone();
|
|
@@ -1780,8 +2011,19 @@ var Runtime = class {
|
|
|
1780
2011
|
this.widgetsManifestCache = Response.json(entries);
|
|
1781
2012
|
return this.widgetsManifestCache.clone();
|
|
1782
2013
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
2014
|
+
async resolveElementsManifest() {
|
|
2015
|
+
if (this.elementsManifestCache)
|
|
2016
|
+
return this.elementsManifestCache.clone();
|
|
2017
|
+
const elementsDir = this.config.elementsDir ?? DEFAULT_ELEMENTS_DIR;
|
|
2018
|
+
const dirResponse = await this.query(elementsDir + "/");
|
|
2019
|
+
if (dirResponse.status === 404) {
|
|
2020
|
+
return new Response("Not Found", { status: 404 });
|
|
2021
|
+
}
|
|
2022
|
+
const entries = await this.scanElements(elementsDir, elementsDir.replace(/^\//, ""));
|
|
2023
|
+
this.elementsManifestCache = Response.json(entries);
|
|
2024
|
+
return this.elementsManifestCache.clone();
|
|
2025
|
+
}
|
|
2026
|
+
async* walkDirectory(dir) {
|
|
1785
2027
|
const trailingDir = dir.endsWith("/") ? dir : dir + "/";
|
|
1786
2028
|
const response = await this.query(trailingDir);
|
|
1787
2029
|
const entries = await response.json();
|
|
@@ -1794,10 +2036,6 @@ var Runtime = class {
|
|
|
1794
2036
|
}
|
|
1795
2037
|
}
|
|
1796
2038
|
}
|
|
1797
|
-
/**
|
|
1798
|
-
* Scan a routes directory and build a RouteNode tree.
|
|
1799
|
-
* The filesystem structure maps directly to the tree — no intermediate array.
|
|
1800
|
-
*/
|
|
1801
2039
|
async scanRoutes(routesDir) {
|
|
1802
2040
|
const root = {};
|
|
1803
2041
|
const allFiles = [];
|
|
@@ -1880,271 +2118,41 @@ var Runtime = class {
|
|
|
1880
2118
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1881
2119
|
return entries;
|
|
1882
2120
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
if (node.errorBoundary)
|
|
1895
|
-
paths.add(node.errorBoundary);
|
|
1896
|
-
if (node.children) {
|
|
1897
|
-
for (const child of Object.values(node.children))
|
|
1898
|
-
walk(child);
|
|
1899
|
-
}
|
|
1900
|
-
if (node.dynamic)
|
|
1901
|
-
walk(node.dynamic.child);
|
|
1902
|
-
if (node.wildcard)
|
|
1903
|
-
walk(node.wildcard.child);
|
|
1904
|
-
}
|
|
1905
|
-
walk(tree);
|
|
1906
|
-
const loaders = {};
|
|
1907
|
-
for (const path of paths) {
|
|
1908
|
-
loaders[path] = () => runtime.loadModule(path);
|
|
1909
|
-
}
|
|
1910
|
-
return loaders;
|
|
1911
|
-
}
|
|
1912
|
-
function extractWidgetExport(mod) {
|
|
1913
|
-
for (const value of Object.values(mod)) {
|
|
1914
|
-
if (!value)
|
|
1915
|
-
continue;
|
|
1916
|
-
if (typeof value === "object" && "getData" in value) {
|
|
1917
|
-
return value;
|
|
1918
|
-
}
|
|
1919
|
-
if (typeof value === "function" && value.prototype?.getData) {
|
|
1920
|
-
return new value();
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
return null;
|
|
1924
|
-
}
|
|
1925
|
-
async function importWidgets(entries, runtime, manual) {
|
|
1926
|
-
const registry = new WidgetRegistry();
|
|
1927
|
-
const widgetFiles = {};
|
|
1928
|
-
for (const entry of entries) {
|
|
1929
|
-
try {
|
|
1930
|
-
const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
|
|
1931
|
-
const mod = await runtime.loadModule(runtimePath);
|
|
1932
|
-
const instance = extractWidgetExport(mod);
|
|
1933
|
-
if (!instance)
|
|
2121
|
+
async scanElements(elementsDir, pathPrefix) {
|
|
2122
|
+
const entries = [];
|
|
2123
|
+
const trailingDir = elementsDir.endsWith("/") ? elementsDir : elementsDir + "/";
|
|
2124
|
+
const response = await this.query(trailingDir);
|
|
2125
|
+
const listing = await response.json();
|
|
2126
|
+
for (const item of listing) {
|
|
2127
|
+
if (!item.endsWith("/"))
|
|
2128
|
+
continue;
|
|
2129
|
+
const name = item.slice(0, -1);
|
|
2130
|
+
if (!name.includes("-")) {
|
|
2131
|
+
console.warn(`[emroute] Skipping element "${name}": custom element names must contain a hyphen (e.g. "my-element")`);
|
|
1934
2132
|
continue;
|
|
1935
|
-
registry.add(instance);
|
|
1936
|
-
const inlined = mod.__files;
|
|
1937
|
-
if (inlined && typeof inlined === "object") {
|
|
1938
|
-
widgetFiles[entry.name] = inlined;
|
|
1939
|
-
} else if (entry.files) {
|
|
1940
|
-
widgetFiles[entry.name] = entry.files;
|
|
1941
|
-
}
|
|
1942
|
-
} catch (e) {
|
|
1943
|
-
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
1944
|
-
if (entry.files)
|
|
1945
|
-
widgetFiles[entry.name] = entry.files;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
if (manual) {
|
|
1949
|
-
for (const widget of manual) {
|
|
1950
|
-
registry.add(widget);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
return { registry, widgetFiles };
|
|
1954
|
-
}
|
|
1955
|
-
function buildHtmlShell(title, htmlBase) {
|
|
1956
|
-
const baseTag = htmlBase ? `
|
|
1957
|
-
<base href="${escapeHtml(htmlBase)}/">` : "";
|
|
1958
|
-
return `<!DOCTYPE html>
|
|
1959
|
-
<html>
|
|
1960
|
-
<head>${baseTag}
|
|
1961
|
-
<meta charset="utf-8">
|
|
1962
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1963
|
-
<title>${escapeHtml(title)}</title>
|
|
1964
|
-
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
1965
|
-
</head>
|
|
1966
|
-
<body>
|
|
1967
|
-
<router-slot></router-slot>
|
|
1968
|
-
</body>
|
|
1969
|
-
</html>`;
|
|
1970
|
-
}
|
|
1971
|
-
function injectSsrContent(html, content, title, ssrRoute) {
|
|
1972
|
-
const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
|
|
1973
|
-
if (!slotPattern.test(html))
|
|
1974
|
-
return html;
|
|
1975
|
-
const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
|
|
1976
|
-
html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
|
|
1977
|
-
if (title) {
|
|
1978
|
-
html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
1979
|
-
}
|
|
1980
|
-
return html;
|
|
1981
|
-
}
|
|
1982
|
-
async function resolveShell(runtime, title, htmlBase) {
|
|
1983
|
-
const response = await runtime.query("/index.html");
|
|
1984
|
-
if (response.status !== 404)
|
|
1985
|
-
return await response.text();
|
|
1986
|
-
return buildHtmlShell(title, htmlBase);
|
|
1987
|
-
}
|
|
1988
|
-
async function createEmrouteServer(config, runtime) {
|
|
1989
|
-
const { spa = "root" } = config;
|
|
1990
|
-
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
1991
|
-
let routeTree;
|
|
1992
|
-
if (config.routeTree) {
|
|
1993
|
-
routeTree = config.routeTree;
|
|
1994
|
-
} else {
|
|
1995
|
-
const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
|
|
1996
|
-
if (manifestResponse.status === 404) {
|
|
1997
|
-
throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. Provide routeTree in config or ensure the runtime produces it.`);
|
|
1998
|
-
}
|
|
1999
|
-
routeTree = await manifestResponse.json();
|
|
2000
|
-
}
|
|
2001
|
-
const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
|
|
2002
|
-
const resolver = new RouteTrie(routeTree);
|
|
2003
|
-
let widgets = config.widgets;
|
|
2004
|
-
let widgetFiles = {};
|
|
2005
|
-
let discoveredWidgetEntries = [];
|
|
2006
|
-
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
2007
|
-
if (widgetsResponse.status !== 404) {
|
|
2008
|
-
discoveredWidgetEntries = await widgetsResponse.json();
|
|
2009
|
-
if (config.widgets) {
|
|
2010
|
-
widgets = config.widgets;
|
|
2011
|
-
for (const entry of discoveredWidgetEntries) {
|
|
2012
|
-
if (entry.files)
|
|
2013
|
-
widgetFiles[entry.name] = entry.files;
|
|
2014
|
-
}
|
|
2015
|
-
} else {
|
|
2016
|
-
const imported = await importWidgets(discoveredWidgetEntries, runtime);
|
|
2017
|
-
widgets = imported.registry;
|
|
2018
|
-
widgetFiles = imported.widgetFiles;
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
let ssrHtmlRouter = null;
|
|
2022
|
-
let ssrMdRouter = null;
|
|
2023
|
-
function buildSsrRouters() {
|
|
2024
|
-
if (spa === "only") {
|
|
2025
|
-
ssrHtmlRouter = null;
|
|
2026
|
-
ssrMdRouter = null;
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
ssrHtmlRouter = new SsrHtmlRouter(resolver, {
|
|
2030
|
-
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
2031
|
-
moduleLoaders,
|
|
2032
|
-
markdownRenderer: config.markdownRenderer,
|
|
2033
|
-
extendContext: config.extendContext,
|
|
2034
|
-
widgets,
|
|
2035
|
-
widgetFiles
|
|
2036
|
-
});
|
|
2037
|
-
ssrMdRouter = new SsrMdRouter(resolver, {
|
|
2038
|
-
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
2039
|
-
moduleLoaders,
|
|
2040
|
-
extendContext: config.extendContext,
|
|
2041
|
-
widgets,
|
|
2042
|
-
widgetFiles
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
buildSsrRouters();
|
|
2046
|
-
const title = config.title ?? "emroute";
|
|
2047
|
-
let shell = await resolveShell(runtime, title, htmlBase);
|
|
2048
|
-
if ((await runtime.query("/main.css")).status !== 404) {
|
|
2049
|
-
shell = shell.replace("</head>", ' <link rel="stylesheet" href="/main.css">\n</head>');
|
|
2050
|
-
}
|
|
2051
|
-
async function handleRequest(req) {
|
|
2052
|
-
const url = new URL(req.url);
|
|
2053
|
-
const pathname = url.pathname;
|
|
2054
|
-
const mdPrefix = mdBase + "/";
|
|
2055
|
-
const htmlPrefix = htmlBase + "/";
|
|
2056
|
-
const appPrefix = appBase + "/";
|
|
2057
|
-
if (ssrMdRouter && (pathname.startsWith(mdPrefix) || pathname === mdBase)) {
|
|
2058
|
-
const routePath = pathname === mdBase ? "/" : pathname.slice(mdBase.length);
|
|
2059
|
-
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2060
|
-
const canonical = mdBase + routePath.slice(0, -1) + (url.search || "");
|
|
2061
|
-
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
2062
|
-
}
|
|
2063
|
-
try {
|
|
2064
|
-
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2065
|
-
const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
|
|
2066
|
-
if (redirect) {
|
|
2067
|
-
const target = redirect.startsWith("/") ? mdBase + redirect : redirect;
|
|
2068
|
-
return Response.redirect(new URL(target, url.origin), status);
|
|
2069
|
-
}
|
|
2070
|
-
return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
|
|
2071
|
-
status,
|
|
2072
|
-
headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
|
|
2073
|
-
});
|
|
2074
|
-
} catch (e) {
|
|
2075
|
-
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2076
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
if (ssrHtmlRouter && (pathname.startsWith(htmlPrefix) || pathname === htmlBase)) {
|
|
2080
|
-
const routePath = pathname === htmlBase ? "/" : pathname.slice(htmlBase.length);
|
|
2081
|
-
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2082
|
-
const canonical = htmlBase + routePath.slice(0, -1) + (url.search || "");
|
|
2083
|
-
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
2084
2133
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const ssrTitle = result.title ?? title;
|
|
2093
|
-
const html = injectSsrContent(shell, result.content, ssrTitle, pathname);
|
|
2094
|
-
return new Response(html, {
|
|
2095
|
-
status: result.status,
|
|
2096
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2097
|
-
});
|
|
2098
|
-
} catch (e) {
|
|
2099
|
-
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2100
|
-
return new Response("Internal Server Error", { status: 500 });
|
|
2134
|
+
let moduleFile = `${name}.element.ts`;
|
|
2135
|
+
let modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
2136
|
+
if ((await this.query(modulePath)).status === 404) {
|
|
2137
|
+
moduleFile = `${name}.element.js`;
|
|
2138
|
+
modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
2139
|
+
if ((await this.query(modulePath)).status === 404)
|
|
2140
|
+
continue;
|
|
2101
2141
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
if (pathname.startsWith(htmlPrefix) || pathname === htmlBase || pathname.startsWith(mdPrefix) || pathname === mdBase) {
|
|
2110
|
-
return new Response(shell, {
|
|
2111
|
-
status: 200,
|
|
2112
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2142
|
+
const prefix = pathPrefix ? `${pathPrefix}/` : "";
|
|
2143
|
+
entries.push({
|
|
2144
|
+
name,
|
|
2145
|
+
modulePath: `${prefix}${name}/${moduleFile}`,
|
|
2146
|
+
tagName: name
|
|
2113
2147
|
});
|
|
2114
2148
|
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
const fileResponse = await runtime.handle(pathname);
|
|
2118
|
-
if (fileResponse.status === 200)
|
|
2119
|
-
return fileResponse;
|
|
2120
|
-
return null;
|
|
2121
|
-
}
|
|
2122
|
-
const base = spa === "root" || spa === "only" ? appBase : htmlBase;
|
|
2123
|
-
const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
|
|
2124
|
-
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
2149
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2150
|
+
return entries;
|
|
2125
2151
|
}
|
|
2126
|
-
return {
|
|
2127
|
-
handleRequest,
|
|
2128
|
-
get htmlRouter() {
|
|
2129
|
-
return ssrHtmlRouter;
|
|
2130
|
-
},
|
|
2131
|
-
get mdRouter() {
|
|
2132
|
-
return ssrMdRouter;
|
|
2133
|
-
},
|
|
2134
|
-
get routeTree() {
|
|
2135
|
-
return routeTree;
|
|
2136
|
-
},
|
|
2137
|
-
get widgetEntries() {
|
|
2138
|
-
return discoveredWidgetEntries;
|
|
2139
|
-
},
|
|
2140
|
-
get shell() {
|
|
2141
|
-
return shell;
|
|
2142
|
-
}
|
|
2143
|
-
};
|
|
2144
2152
|
}
|
|
2145
2153
|
|
|
2146
2154
|
// dist/runtime/fetch.runtime.js
|
|
2147
|
-
var
|
|
2155
|
+
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
|
|
2148
2156
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
2149
2157
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
2150
2158
|
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
@@ -2152,11 +2160,9 @@ var __rewriteRelativeImportExtension2 = function(path, preserveJsx) {
|
|
|
2152
2160
|
}
|
|
2153
2161
|
return path;
|
|
2154
2162
|
};
|
|
2155
|
-
|
|
2163
|
+
|
|
2164
|
+
class FetchRuntime extends Runtime {
|
|
2156
2165
|
origin;
|
|
2157
|
-
/**
|
|
2158
|
-
* @param origin — Server origin, e.g. `'http://localhost:4100'` or `location.origin`.
|
|
2159
|
-
*/
|
|
2160
2166
|
constructor(origin, config = {}) {
|
|
2161
2167
|
super(config);
|
|
2162
2168
|
this.origin = origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
@@ -2176,7 +2182,7 @@ var FetchRuntime = class extends Runtime {
|
|
|
2176
2182
|
const response = await fetch(url);
|
|
2177
2183
|
const js = await response.text();
|
|
2178
2184
|
const blob = new Blob([js], { type: "application/javascript" });
|
|
2179
|
-
return import(
|
|
2185
|
+
return import(__rewriteRelativeImportExtension(URL.createObjectURL(blob)));
|
|
2180
2186
|
}
|
|
2181
2187
|
toUrl(resource) {
|
|
2182
2188
|
if (typeof resource === "string")
|
|
@@ -2185,10 +2191,10 @@ var FetchRuntime = class extends Runtime {
|
|
|
2185
2191
|
return `${this.origin}${resource.pathname}${resource.search}`;
|
|
2186
2192
|
return `${this.origin}${new URL(resource.url).pathname}`;
|
|
2187
2193
|
}
|
|
2188
|
-
}
|
|
2194
|
+
}
|
|
2189
2195
|
|
|
2190
|
-
// dist/src/renderer/spa/
|
|
2191
|
-
|
|
2196
|
+
// dist/src/renderer/spa/emroute.app.js
|
|
2197
|
+
class EmrouteApp {
|
|
2192
2198
|
server;
|
|
2193
2199
|
appBase;
|
|
2194
2200
|
slot = null;
|
|
@@ -2208,7 +2214,7 @@ var EmrouteApp = class {
|
|
|
2208
2214
|
console.warn("[EmrouteApp] Navigation API not available");
|
|
2209
2215
|
return;
|
|
2210
2216
|
}
|
|
2211
|
-
this.abortController = new AbortController
|
|
2217
|
+
this.abortController = new AbortController;
|
|
2212
2218
|
const { signal } = this.abortController;
|
|
2213
2219
|
navigation.addEventListener("navigate", (event) => {
|
|
2214
2220
|
if (!event.canIntercept)
|
|
@@ -2264,12 +2270,12 @@ var EmrouteApp = class {
|
|
|
2264
2270
|
return pathname;
|
|
2265
2271
|
}
|
|
2266
2272
|
async handleNavigation(url, signal) {
|
|
2267
|
-
if (!this.slot || !this.server.
|
|
2273
|
+
if (!this.slot || !this.server.htmlRenderer)
|
|
2268
2274
|
return;
|
|
2269
2275
|
const routePath = this.stripAppBase(url.pathname);
|
|
2270
2276
|
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2271
2277
|
try {
|
|
2272
|
-
const { content, title, redirect } = await this.server.
|
|
2278
|
+
const { content, title, redirect } = await this.server.htmlRenderer.render(routeUrl, signal);
|
|
2273
2279
|
if (signal.aborted)
|
|
2274
2280
|
return;
|
|
2275
2281
|
if (redirect) {
|
|
@@ -2299,7 +2305,7 @@ var EmrouteApp = class {
|
|
|
2299
2305
|
}
|
|
2300
2306
|
}
|
|
2301
2307
|
}
|
|
2302
|
-
}
|
|
2308
|
+
}
|
|
2303
2309
|
async function createEmrouteApp(server, options) {
|
|
2304
2310
|
const g = globalThis;
|
|
2305
2311
|
if (g.__emroute_app) {
|
|
@@ -2321,21 +2327,37 @@ async function bootEmrouteApp(options) {
|
|
|
2321
2327
|
const routeTree = await routesResponse.json();
|
|
2322
2328
|
const widgetsResponse = await runtime.handle(WIDGETS_MANIFEST_PATH);
|
|
2323
2329
|
const widgetEntries = widgetsResponse.ok ? await widgetsResponse.json() : [];
|
|
2324
|
-
const
|
|
2325
|
-
const
|
|
2330
|
+
const elementsResponse = await runtime.handle(ELEMENTS_MANIFEST_PATH);
|
|
2331
|
+
const elementEntries = elementsResponse.ok ? await elementsResponse.json() : [];
|
|
2332
|
+
const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, elementEntries, runtime);
|
|
2333
|
+
const widgets = new WidgetRegistry;
|
|
2326
2334
|
for (const entry of widgetEntries) {
|
|
2327
2335
|
ComponentElement.registerLazy(entry.name, entry.files, moduleLoaders[entry.modulePath]);
|
|
2328
2336
|
}
|
|
2329
|
-
const
|
|
2337
|
+
for (const entry of elementEntries) {
|
|
2338
|
+
const loader = moduleLoaders[entry.modulePath];
|
|
2339
|
+
if (loader) {
|
|
2340
|
+
loader().then((mod) => {
|
|
2341
|
+
const cls = mod.default;
|
|
2342
|
+
if (typeof cls === "function" && !customElements.get(entry.tagName)) {
|
|
2343
|
+
customElements.define(entry.tagName, cls);
|
|
2344
|
+
}
|
|
2345
|
+
}).catch((e) => {
|
|
2346
|
+
console.error(`[emroute] Failed to load element ${entry.tagName}:`, e);
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const mdRenderer = MarkdownElement.getConfiguredRenderer();
|
|
2351
|
+
const server = await Emroute.create({
|
|
2330
2352
|
routeTree,
|
|
2331
2353
|
widgets,
|
|
2332
2354
|
moduleLoaders,
|
|
2333
|
-
markdownRenderer:
|
|
2355
|
+
...mdRenderer ? { markdownRenderer: mdRenderer } : {}
|
|
2334
2356
|
}, runtime);
|
|
2335
2357
|
return createEmrouteApp(server, options);
|
|
2336
2358
|
}
|
|
2337
|
-
function buildLazyLoaders(tree, widgetEntries, runtime) {
|
|
2338
|
-
const paths =
|
|
2359
|
+
function buildLazyLoaders(tree, widgetEntries, elementEntries, runtime) {
|
|
2360
|
+
const paths = new Set;
|
|
2339
2361
|
function walk(node) {
|
|
2340
2362
|
const modulePath = node.files?.ts ?? node.files?.js;
|
|
2341
2363
|
if (modulePath)
|
|
@@ -2356,6 +2378,8 @@ function buildLazyLoaders(tree, widgetEntries, runtime) {
|
|
|
2356
2378
|
walk(tree);
|
|
2357
2379
|
for (const entry of widgetEntries)
|
|
2358
2380
|
paths.add(entry.modulePath);
|
|
2381
|
+
for (const entry of elementEntries)
|
|
2382
|
+
paths.add(entry.modulePath);
|
|
2359
2383
|
const loaders = {};
|
|
2360
2384
|
for (const path of paths) {
|
|
2361
2385
|
const absolute = path.startsWith("/") ? path : "/" + path;
|
|
@@ -2363,24 +2387,8 @@ function buildLazyLoaders(tree, widgetEntries, runtime) {
|
|
|
2363
2387
|
}
|
|
2364
2388
|
return loaders;
|
|
2365
2389
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
var WidgetComponent = class extends Component {
|
|
2369
|
-
/**
|
|
2370
|
-
* Render widget as HTML.
|
|
2371
|
-
*
|
|
2372
|
-
* Fallback chain:
|
|
2373
|
-
* 1. html file content from context
|
|
2374
|
-
* 2. md file content wrapped in `<mark-down>`
|
|
2375
|
-
* 3. base Component default (markdown→HTML conversion)
|
|
2376
|
-
*
|
|
2377
|
-
* @example
|
|
2378
|
-
* ```ts
|
|
2379
|
-
* override renderHTML({ data, params }: this['RenderArgs']) {
|
|
2380
|
-
* return `<span>${params.coin}: $${data?.price}</span>`;
|
|
2381
|
-
* }
|
|
2382
|
-
* ```
|
|
2383
|
-
*/
|
|
2390
|
+
// dist/core/component/widget.component.js
|
|
2391
|
+
class WidgetComponent extends Component {
|
|
2384
2392
|
renderHTML(args) {
|
|
2385
2393
|
const files = args.context.files;
|
|
2386
2394
|
const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>
|
|
@@ -2396,20 +2404,6 @@ var WidgetComponent = class extends Component {
|
|
|
2396
2404
|
}
|
|
2397
2405
|
return super.renderHTML(args);
|
|
2398
2406
|
}
|
|
2399
|
-
/**
|
|
2400
|
-
* Render widget as Markdown.
|
|
2401
|
-
*
|
|
2402
|
-
* Fallback chain:
|
|
2403
|
-
* 1. md file content from context
|
|
2404
|
-
* 2. empty string
|
|
2405
|
-
*
|
|
2406
|
-
* @example
|
|
2407
|
-
* ```ts
|
|
2408
|
-
* override renderMarkdown({ data, params }: this['RenderArgs']) {
|
|
2409
|
-
* return `**${params.coin}**: $${data?.price}`;
|
|
2410
|
-
* }
|
|
2411
|
-
* ```
|
|
2412
|
-
*/
|
|
2413
2407
|
renderMarkdown(args) {
|
|
2414
2408
|
const files = args.context.files;
|
|
2415
2409
|
if (files?.md) {
|
|
@@ -2417,12 +2411,9 @@ var WidgetComponent = class extends Component {
|
|
|
2417
2411
|
}
|
|
2418
2412
|
return "";
|
|
2419
2413
|
}
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2414
|
+
}
|
|
2422
2415
|
// dist/src/overlay/overlay.css.js
|
|
2423
|
-
var overlayCSS =
|
|
2424
|
-
/* css */
|
|
2425
|
-
`
|
|
2416
|
+
var overlayCSS = `
|
|
2426
2417
|
:root {
|
|
2427
2418
|
--overlay-backdrop: oklch(0% 0 0 / 0.5);
|
|
2428
2419
|
--overlay-surface: oklch(100% 0 0);
|
|
@@ -2583,8 +2574,7 @@ dialog[data-overlay-modal][data-dismissing]::backdrop {
|
|
|
2583
2574
|
opacity: 0;
|
|
2584
2575
|
scale: 0.95;
|
|
2585
2576
|
}
|
|
2586
|
-
|
|
2587
|
-
);
|
|
2577
|
+
`;
|
|
2588
2578
|
|
|
2589
2579
|
// dist/src/overlay/overlay.service.js
|
|
2590
2580
|
var ANIMATION_SAFETY_TIMEOUT = 300;
|
|
@@ -2626,7 +2616,7 @@ function createOverlayService() {
|
|
|
2626
2616
|
document.body.appendChild(dialog);
|
|
2627
2617
|
dialog.addEventListener("click", (e) => {
|
|
2628
2618
|
if (e.target === dialog) {
|
|
2629
|
-
closeModal(
|
|
2619
|
+
closeModal(undefined);
|
|
2630
2620
|
}
|
|
2631
2621
|
});
|
|
2632
2622
|
return dialog;
|
|
@@ -2657,12 +2647,12 @@ function createOverlayService() {
|
|
|
2657
2647
|
if (d.open) {
|
|
2658
2648
|
d.close();
|
|
2659
2649
|
if (modalResolve) {
|
|
2660
|
-
modalResolve(
|
|
2650
|
+
modalResolve(undefined);
|
|
2661
2651
|
modalResolve = null;
|
|
2662
2652
|
}
|
|
2663
2653
|
if (modalOnClose) {
|
|
2664
2654
|
modalOnClose();
|
|
2665
|
-
modalOnClose =
|
|
2655
|
+
modalOnClose = undefined;
|
|
2666
2656
|
}
|
|
2667
2657
|
}
|
|
2668
2658
|
d.innerHTML = "";
|
|
@@ -2680,7 +2670,7 @@ function createOverlayService() {
|
|
|
2680
2670
|
const onClose = modalOnClose;
|
|
2681
2671
|
const dialogRef = dialog;
|
|
2682
2672
|
modalResolve = null;
|
|
2683
|
-
modalOnClose =
|
|
2673
|
+
modalOnClose = undefined;
|
|
2684
2674
|
animateDismiss(dialogRef, () => {
|
|
2685
2675
|
if (dialogRef && dialogRef.open) {
|
|
2686
2676
|
dialogRef.close();
|
|
@@ -2726,8 +2716,7 @@ function createOverlayService() {
|
|
|
2726
2716
|
cleanupPopoverAnchorObserver();
|
|
2727
2717
|
try {
|
|
2728
2718
|
el.hidePopover();
|
|
2729
|
-
} catch {
|
|
2730
|
-
}
|
|
2719
|
+
} catch {}
|
|
2731
2720
|
el.removeAttribute("data-dismissing");
|
|
2732
2721
|
el.innerHTML = "";
|
|
2733
2722
|
options.render(el);
|
|
@@ -2766,8 +2755,7 @@ function createOverlayService() {
|
|
|
2766
2755
|
return;
|
|
2767
2756
|
try {
|
|
2768
2757
|
popoverEl.hidePopover();
|
|
2769
|
-
} catch {
|
|
2770
|
-
}
|
|
2758
|
+
} catch {}
|
|
2771
2759
|
popoverEl.removeAttribute("data-dismissing");
|
|
2772
2760
|
}
|
|
2773
2761
|
function cleanupPopoverAnchorObserver() {
|
|
@@ -2791,8 +2779,7 @@ function createOverlayService() {
|
|
|
2791
2779
|
animateDismiss(popoverEl, () => {
|
|
2792
2780
|
try {
|
|
2793
2781
|
popoverEl.hidePopover();
|
|
2794
|
-
} catch {
|
|
2795
|
-
}
|
|
2782
|
+
} catch {}
|
|
2796
2783
|
});
|
|
2797
2784
|
}
|
|
2798
2785
|
function dismissAll() {
|
|
@@ -2800,11 +2787,11 @@ function createOverlayService() {
|
|
|
2800
2787
|
const resolve = modalResolve;
|
|
2801
2788
|
const onClose = modalOnClose;
|
|
2802
2789
|
modalResolve = null;
|
|
2803
|
-
modalOnClose =
|
|
2790
|
+
modalOnClose = undefined;
|
|
2804
2791
|
dialog.removeAttribute("data-dismissing");
|
|
2805
2792
|
dialog.close();
|
|
2806
2793
|
if (resolve)
|
|
2807
|
-
resolve(
|
|
2794
|
+
resolve(undefined);
|
|
2808
2795
|
if (onClose)
|
|
2809
2796
|
onClose();
|
|
2810
2797
|
}
|
|
@@ -2818,8 +2805,7 @@ function createOverlayService() {
|
|
|
2818
2805
|
for (const el of document.querySelectorAll(":popover-open")) {
|
|
2819
2806
|
el.hidePopover();
|
|
2820
2807
|
}
|
|
2821
|
-
} catch {
|
|
2822
|
-
}
|
|
2808
|
+
} catch {}
|
|
2823
2809
|
for (const el of document.querySelectorAll("dialog[open]")) {
|
|
2824
2810
|
if (el !== dialog)
|
|
2825
2811
|
el.close();
|
|
@@ -2834,9 +2820,8 @@ function createOverlayService() {
|
|
|
2834
2820
|
dismissAll
|
|
2835
2821
|
};
|
|
2836
2822
|
}
|
|
2837
|
-
|
|
2838
2823
|
// dist/src/widget/page-title.widget.js
|
|
2839
|
-
|
|
2824
|
+
class PageTitleWidget extends WidgetComponent {
|
|
2840
2825
|
name = "page-title";
|
|
2841
2826
|
getData(args) {
|
|
2842
2827
|
return Promise.resolve({ title: args.params.title });
|
|
@@ -2855,14 +2840,14 @@ var PageTitleWidget = class extends WidgetComponent {
|
|
|
2855
2840
|
if (!params.title || typeof params.title !== "string") {
|
|
2856
2841
|
return 'page-title widget requires a "title" string param';
|
|
2857
2842
|
}
|
|
2858
|
-
return
|
|
2843
|
+
return;
|
|
2859
2844
|
}
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2845
|
+
}
|
|
2862
2846
|
// dist/src/widget/breadcrumb.widget.js
|
|
2863
|
-
var DEFAULT_HTML_SEPARATOR = "
|
|
2847
|
+
var DEFAULT_HTML_SEPARATOR = " › ";
|
|
2864
2848
|
var DEFAULT_MD_SEPARATOR = " > ";
|
|
2865
|
-
|
|
2849
|
+
|
|
2850
|
+
class BreadcrumbWidget extends WidgetComponent {
|
|
2866
2851
|
name = "breadcrumb";
|
|
2867
2852
|
getData(args) {
|
|
2868
2853
|
const pathname = args.context.pathname || "/";
|
|
@@ -2900,8 +2885,7 @@ var BreadcrumbWidget = class extends WidgetComponent {
|
|
|
2900
2885
|
const sep = args.params.separator ?? DEFAULT_MD_SEPARATOR;
|
|
2901
2886
|
return args.data.segments.map((seg, i, arr) => i === arr.length - 1 ? `**${seg.label}**` : `[${seg.label}](${seg.href})`).join(sep);
|
|
2902
2887
|
}
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2888
|
+
}
|
|
2905
2889
|
// dist/src/renderer/spa/mod.js
|
|
2906
2890
|
if (globalThis.customElements) {
|
|
2907
2891
|
if (!customElements.get("router-slot"))
|
|
@@ -2910,25 +2894,27 @@ if (globalThis.customElements) {
|
|
|
2910
2894
|
customElements.define("mark-down", MarkdownElement);
|
|
2911
2895
|
}
|
|
2912
2896
|
export {
|
|
2913
|
-
|
|
2914
|
-
Component,
|
|
2915
|
-
ComponentElement,
|
|
2916
|
-
DEFAULT_BASE_PATH,
|
|
2917
|
-
EmrouteApp,
|
|
2918
|
-
FetchRuntime,
|
|
2919
|
-
MarkdownElement,
|
|
2920
|
-
PageComponent,
|
|
2921
|
-
PageTitleWidget,
|
|
2922
|
-
RouteTrie,
|
|
2923
|
-
RouterSlot,
|
|
2924
|
-
WidgetComponent,
|
|
2925
|
-
WidgetRegistry,
|
|
2926
|
-
bootEmrouteApp,
|
|
2927
|
-
createEmrouteApp,
|
|
2928
|
-
createEmrouteServer,
|
|
2929
|
-
createOverlayService,
|
|
2930
|
-
escapeHtml,
|
|
2897
|
+
setLogger,
|
|
2931
2898
|
scopeWidgetCss,
|
|
2932
|
-
|
|
2899
|
+
escapeHtml,
|
|
2900
|
+
createOverlayService,
|
|
2901
|
+
createEmrouteApp,
|
|
2902
|
+
bootEmrouteApp,
|
|
2903
|
+
WidgetRegistry,
|
|
2904
|
+
WidgetComponent,
|
|
2905
|
+
RouterSlot,
|
|
2906
|
+
RouteTrie,
|
|
2907
|
+
PageTitleWidget,
|
|
2908
|
+
PageComponent,
|
|
2909
|
+
MarkdownElement,
|
|
2910
|
+
FetchRuntime,
|
|
2911
|
+
EmrouteApp,
|
|
2912
|
+
Emroute,
|
|
2913
|
+
DEFAULT_BASE_PATH,
|
|
2914
|
+
ComponentElement,
|
|
2915
|
+
Component,
|
|
2916
|
+
BreadcrumbWidget
|
|
2933
2917
|
};
|
|
2918
|
+
|
|
2919
|
+
//# debugId=90218169F64D555264756E2164756E21
|
|
2934
2920
|
//# sourceMappingURL=emroute.js.map
|