@emkodev/emroute 1.6.6-beta.2 → 1.6.6-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/emroute.js +2757 -0
- package/dist/emroute.js.map +7 -0
- package/dist/runtime/abstract.runtime.d.ts +0 -28
- package/dist/runtime/abstract.runtime.js +10 -58
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/esbuild-runtime-loader.plugin.js +3 -0
- package/dist/runtime/bun/esbuild-runtime-loader.plugin.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +0 -5
- package/dist/runtime/bun/fs/bun-fs.runtime.js +1 -95
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +0 -5
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +2 -96
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/fetch.runtime.d.ts +26 -0
- package/dist/runtime/fetch.runtime.js +55 -0
- package/dist/runtime/fetch.runtime.js.map +1 -0
- package/dist/runtime/sitemap.generator.d.ts +4 -4
- package/dist/runtime/sitemap.generator.js +32 -11
- package/dist/runtime/sitemap.generator.js.map +1 -1
- package/dist/runtime/universal/fs/universal-fs.runtime.d.ts +0 -5
- package/dist/runtime/universal/fs/universal-fs.runtime.js +1 -95
- package/dist/runtime/universal/fs/universal-fs.runtime.js.map +1 -1
- package/dist/server/build.util.d.ts +38 -0
- package/dist/server/build.util.js +133 -0
- package/dist/server/build.util.js.map +1 -0
- package/dist/server/codegen.util.d.ts +3 -0
- package/dist/server/codegen.util.js +28 -10
- package/dist/server/codegen.util.js.map +1 -1
- package/dist/server/emroute.server.js +53 -29
- package/dist/server/emroute.server.js.map +1 -1
- package/dist/server/esbuild-manifest.plugin.js +6 -4
- package/dist/server/esbuild-manifest.plugin.js.map +1 -1
- package/dist/server/server-api.type.d.ts +6 -0
- package/dist/src/component/abstract.component.d.ts +5 -3
- package/dist/src/component/abstract.component.js.map +1 -1
- package/dist/src/element/component.element.js +5 -4
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/renderer/spa/mod.d.ts +2 -3
- package/dist/src/renderer/spa/mod.js +2 -3
- package/dist/src/renderer/spa/mod.js.map +1 -1
- package/dist/src/renderer/spa/thin-client.d.ts +34 -0
- package/dist/src/renderer/spa/thin-client.js +138 -0
- package/dist/src/renderer/spa/thin-client.js.map +1 -0
- package/dist/src/renderer/ssr/html.renderer.d.ts +3 -3
- package/dist/src/renderer/ssr/html.renderer.js +6 -6
- package/dist/src/renderer/ssr/html.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/md.renderer.d.ts +3 -3
- package/dist/src/renderer/ssr/md.renderer.js +12 -7
- package/dist/src/renderer/ssr/md.renderer.js.map +1 -1
- package/dist/src/renderer/ssr/ssr.renderer.d.ts +7 -6
- package/dist/src/renderer/ssr/ssr.renderer.js +42 -44
- package/dist/src/renderer/ssr/ssr.renderer.js.map +1 -1
- package/dist/src/route/route.core.d.ts +16 -6
- package/dist/src/route/route.core.js +44 -23
- package/dist/src/route/route.core.js.map +1 -1
- package/dist/src/type/route-tree.type.d.ts +2 -0
- package/dist/src/type/route.type.d.ts +6 -24
- package/dist/src/util/md.util.d.ts +8 -0
- package/dist/src/util/md.util.js +28 -0
- package/dist/src/util/md.util.js.map +1 -0
- package/dist/src/util/widget-resolve.util.js +6 -1
- package/dist/src/util/widget-resolve.util.js.map +1 -1
- package/dist/src/widget/breadcrumb.widget.d.ts +0 -1
- package/dist/src/widget/breadcrumb.widget.js +4 -15
- package/dist/src/widget/breadcrumb.widget.js.map +1 -1
- package/package.json +13 -2
- package/runtime/abstract.runtime.ts +9 -82
- package/runtime/bun/esbuild-runtime-loader.plugin.ts +2 -0
- package/runtime/bun/fs/bun-fs.runtime.ts +0 -109
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +1 -112
- package/runtime/fetch.runtime.ts +70 -0
- package/runtime/sitemap.generator.ts +37 -12
- package/runtime/universal/fs/universal-fs.runtime.ts +0 -109
- package/server/build.util.ts +168 -0
- package/server/codegen.util.ts +29 -11
- package/server/emroute.server.ts +50 -30
- package/server/esbuild-manifest.plugin.ts +5 -3
- package/server/server-api.type.ts +7 -0
- package/src/component/abstract.component.ts +5 -3
- package/src/element/component.element.ts +5 -4
- package/src/renderer/spa/mod.ts +2 -8
- package/src/renderer/spa/thin-client.ts +165 -0
- package/src/renderer/ssr/html.renderer.ts +6 -5
- package/src/renderer/ssr/md.renderer.ts +12 -6
- package/src/renderer/ssr/ssr.renderer.ts +54 -48
- package/src/route/route.core.ts +49 -28
- package/src/type/route-tree.type.ts +2 -0
- package/src/type/route.type.ts +7 -32
- package/src/util/md.util.ts +31 -0
- package/src/util/widget-resolve.util.ts +6 -1
- package/src/widget/breadcrumb.widget.ts +4 -16
- package/server/scanner.util.ts +0 -243
- package/src/renderer/spa/base.renderer.ts +0 -186
- package/src/renderer/spa/hash.renderer.ts +0 -238
- package/src/renderer/spa/html.renderer.ts +0 -399
- package/src/route/route.matcher.ts +0 -260
- package/src/web-doc/index.md +0 -15
package/dist/emroute.js
ADDED
|
@@ -0,0 +1,2757 @@
|
|
|
1
|
+
// dist/src/util/html.util.js
|
|
2
|
+
var SSR_ATTR = "ssr";
|
|
3
|
+
var LAZY_ATTR = "lazy";
|
|
4
|
+
var SsrShadowRoot = class {
|
|
5
|
+
host;
|
|
6
|
+
_innerHTML = "";
|
|
7
|
+
constructor(host) {
|
|
8
|
+
this.host = host;
|
|
9
|
+
}
|
|
10
|
+
get innerHTML() {
|
|
11
|
+
return this._innerHTML;
|
|
12
|
+
}
|
|
13
|
+
set innerHTML(value) {
|
|
14
|
+
this._innerHTML = value;
|
|
15
|
+
}
|
|
16
|
+
setHTMLUnsafe(html, _options) {
|
|
17
|
+
this._innerHTML = html;
|
|
18
|
+
}
|
|
19
|
+
append(..._nodes) {
|
|
20
|
+
}
|
|
21
|
+
querySelector(_selector) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
querySelectorAll(_selector) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
get childNodes() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
get firstChild() {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var SsrHTMLElement = class {
|
|
35
|
+
_innerHTML = "";
|
|
36
|
+
_shadowRoot = null;
|
|
37
|
+
_attributes = /* @__PURE__ */ new Map();
|
|
38
|
+
// Accept any CSS property assignment without error
|
|
39
|
+
style = new Proxy({}, {
|
|
40
|
+
set(_target, _prop, _value) {
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
get(_target, prop) {
|
|
44
|
+
if (typeof prop === "string")
|
|
45
|
+
return "";
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
get innerHTML() {
|
|
50
|
+
return this._innerHTML;
|
|
51
|
+
}
|
|
52
|
+
set innerHTML(value) {
|
|
53
|
+
this._innerHTML = value;
|
|
54
|
+
}
|
|
55
|
+
get shadowRoot() {
|
|
56
|
+
return this._shadowRoot;
|
|
57
|
+
}
|
|
58
|
+
get childNodes() {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
get firstChild() {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
get attributes() {
|
|
65
|
+
const attrs = [];
|
|
66
|
+
for (const [name, value] of this._attributes) {
|
|
67
|
+
attrs.push({ name, value });
|
|
68
|
+
}
|
|
69
|
+
return attrs;
|
|
70
|
+
}
|
|
71
|
+
attachShadow(_init) {
|
|
72
|
+
this._shadowRoot = new SsrShadowRoot(this);
|
|
73
|
+
return this._shadowRoot;
|
|
74
|
+
}
|
|
75
|
+
getAttribute(name) {
|
|
76
|
+
return this._attributes.get(name) ?? null;
|
|
77
|
+
}
|
|
78
|
+
setAttribute(name, value) {
|
|
79
|
+
this._attributes.set(name, value);
|
|
80
|
+
}
|
|
81
|
+
removeAttribute(name) {
|
|
82
|
+
this._attributes.delete(name);
|
|
83
|
+
}
|
|
84
|
+
hasAttribute(name) {
|
|
85
|
+
return this._attributes.has(name);
|
|
86
|
+
}
|
|
87
|
+
querySelector(_selector) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
querySelectorAll(_selector) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
append(..._nodes) {
|
|
94
|
+
}
|
|
95
|
+
appendChild(node) {
|
|
96
|
+
return node;
|
|
97
|
+
}
|
|
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
|
+
}
|
|
111
|
+
var STATUS_MESSAGES = {
|
|
112
|
+
401: "Unauthorized",
|
|
113
|
+
403: "Forbidden",
|
|
114
|
+
404: "Not Found",
|
|
115
|
+
500: "Internal Server Error"
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// dist/src/element/slot.element.js
|
|
119
|
+
var RouterSlot = class extends HTMLElementBase {
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// dist/src/element/markdown.element.js
|
|
123
|
+
var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
124
|
+
static renderer = null;
|
|
125
|
+
static rendererInitPromise = null;
|
|
126
|
+
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
|
+
static setRenderer(renderer) {
|
|
138
|
+
_MarkdownElement.renderer = renderer;
|
|
139
|
+
_MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the current renderer, waiting for init if needed.
|
|
143
|
+
*/
|
|
144
|
+
static async getRenderer() {
|
|
145
|
+
const renderer = _MarkdownElement.renderer;
|
|
146
|
+
if (!renderer) {
|
|
147
|
+
throw new Error("No markdown renderer configured. Call MarkdownElement.setRenderer() before using <mark-down> elements.");
|
|
148
|
+
}
|
|
149
|
+
if (_MarkdownElement.rendererInitPromise) {
|
|
150
|
+
await _MarkdownElement.rendererInitPromise;
|
|
151
|
+
}
|
|
152
|
+
return renderer;
|
|
153
|
+
}
|
|
154
|
+
async connectedCallback() {
|
|
155
|
+
this.abortController = new AbortController();
|
|
156
|
+
await this.loadContent();
|
|
157
|
+
}
|
|
158
|
+
disconnectedCallback() {
|
|
159
|
+
this.abortController?.abort();
|
|
160
|
+
this.abortController = null;
|
|
161
|
+
}
|
|
162
|
+
async loadContent() {
|
|
163
|
+
const src = this.getAttribute("src");
|
|
164
|
+
const inlineContent = this.textContent?.trim();
|
|
165
|
+
if (src) {
|
|
166
|
+
await this.loadFromSrc(src);
|
|
167
|
+
} else if (inlineContent) {
|
|
168
|
+
await this.renderContent(inlineContent);
|
|
169
|
+
} else {
|
|
170
|
+
this.innerHTML = "";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async loadFromSrc(src) {
|
|
174
|
+
const signal = this.abortController?.signal;
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(src, { signal });
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Failed to fetch ${src}: ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
const markdown = await response.text();
|
|
181
|
+
await this.renderContent(markdown);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.showError(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async renderContent(markdown) {
|
|
190
|
+
try {
|
|
191
|
+
const renderer = await _MarkdownElement.getRenderer();
|
|
192
|
+
this.innerHTML = renderer.render(markdown);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.showError(error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
showError(error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
this.innerHTML = `<div>Markdown Error: ${escapeHtml(message)}</div>`;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// dist/src/element/component.element.js
|
|
204
|
+
var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
205
|
+
/** Shared file content cache — deduplicates fetches across all widget instances. */
|
|
206
|
+
static fileCache = /* @__PURE__ */ new Map();
|
|
207
|
+
/** App-level context provider set once during router initialization. */
|
|
208
|
+
static extendContext;
|
|
209
|
+
/** Register (or clear) the context provider that enriches every widget's ComponentContext. */
|
|
210
|
+
static setContextProvider(provider) {
|
|
211
|
+
_ComponentElement.extendContext = provider;
|
|
212
|
+
}
|
|
213
|
+
component;
|
|
214
|
+
effectiveFiles;
|
|
215
|
+
params = null;
|
|
216
|
+
data = null;
|
|
217
|
+
context;
|
|
218
|
+
state = "idle";
|
|
219
|
+
errorMessage = "";
|
|
220
|
+
deferred = null;
|
|
221
|
+
abortController = null;
|
|
222
|
+
intersectionObserver = null;
|
|
223
|
+
/** Promise that resolves with fetched data (available after loadData starts) */
|
|
224
|
+
dataPromise = null;
|
|
225
|
+
constructor(component, files) {
|
|
226
|
+
super();
|
|
227
|
+
this.component = component;
|
|
228
|
+
this.effectiveFiles = files;
|
|
229
|
+
if (!this.shadowRoot) {
|
|
230
|
+
this.attachShadow({ mode: "open" });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Register a widget as a custom element: `widget-{name}`.
|
|
235
|
+
* Creates a fresh widget instance per DOM element (per-element state).
|
|
236
|
+
* Optional `files` parameter provides discovered file paths without mutating
|
|
237
|
+
* the component instance.
|
|
238
|
+
*/
|
|
239
|
+
static register(component, files) {
|
|
240
|
+
const tagName = `widget-${component.name}`;
|
|
241
|
+
if (!globalThis.customElements || customElements.get(tagName)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const WidgetClass = component.constructor;
|
|
245
|
+
const BoundElement = class extends _ComponentElement {
|
|
246
|
+
constructor() {
|
|
247
|
+
super(new WidgetClass(), files);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
customElements.define(tagName, BoundElement);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Register a widget class (not instance) as a custom element: `widget-{name}`.
|
|
254
|
+
* Used for manifest-based registration where classes are loaded dynamically.
|
|
255
|
+
*/
|
|
256
|
+
static registerClass(WidgetClass, name, files) {
|
|
257
|
+
const tagName = `widget-${name}`;
|
|
258
|
+
if (!globalThis.customElements || customElements.get(tagName)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const BoundElement = class extends _ComponentElement {
|
|
262
|
+
constructor() {
|
|
263
|
+
super(new WidgetClass(), files);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
customElements.define(tagName, BoundElement);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Promise that resolves when component is ready (data loaded and rendered).
|
|
270
|
+
* Used by router to wait for async components.
|
|
271
|
+
*/
|
|
272
|
+
get ready() {
|
|
273
|
+
if (this.state === "ready") {
|
|
274
|
+
return Promise.resolve();
|
|
275
|
+
}
|
|
276
|
+
this.deferred ??= Promise.withResolvers();
|
|
277
|
+
return this.deferred.promise;
|
|
278
|
+
}
|
|
279
|
+
async connectedCallback() {
|
|
280
|
+
this.component.element = this;
|
|
281
|
+
this.style.contentVisibility = "auto";
|
|
282
|
+
this.abortController = new AbortController();
|
|
283
|
+
const signal = this.abortController.signal;
|
|
284
|
+
const params = {};
|
|
285
|
+
for (const attr of this.attributes) {
|
|
286
|
+
if (attr.name === SSR_ATTR || attr.name === LAZY_ATTR)
|
|
287
|
+
continue;
|
|
288
|
+
const key = attr.name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
289
|
+
try {
|
|
290
|
+
params[key] = JSON.parse(attr.value);
|
|
291
|
+
} catch {
|
|
292
|
+
params[key] = attr.value;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
this.params = params;
|
|
296
|
+
if (this.component.validateParams && this.params !== null) {
|
|
297
|
+
const error = this.component.validateParams(this.params);
|
|
298
|
+
if (error) {
|
|
299
|
+
this.setError(error);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const files = await this.loadFiles();
|
|
304
|
+
if (signal.aborted)
|
|
305
|
+
return;
|
|
306
|
+
const currentUrl = globalThis.location ? new URL(location.href) : new URL("http://localhost/");
|
|
307
|
+
const base = {
|
|
308
|
+
url: currentUrl,
|
|
309
|
+
pathname: currentUrl.pathname,
|
|
310
|
+
searchParams: currentUrl.searchParams,
|
|
311
|
+
params: this.params ?? {},
|
|
312
|
+
files: files.html || files.md || files.css ? files : void 0
|
|
313
|
+
};
|
|
314
|
+
this.context = _ComponentElement.extendContext ? _ComponentElement.extendContext(base) : base;
|
|
315
|
+
if (this.hasAttribute(SSR_ATTR)) {
|
|
316
|
+
this.removeAttribute(SSR_ATTR);
|
|
317
|
+
const lightText = this.textContent?.trim();
|
|
318
|
+
if (lightText) {
|
|
319
|
+
try {
|
|
320
|
+
this.data = JSON.parse(lightText);
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.textContent = "";
|
|
325
|
+
this.state = "ready";
|
|
326
|
+
if (this.component.hydrate) {
|
|
327
|
+
const args = { data: this.data, params: this.params, context: this.context };
|
|
328
|
+
queueMicrotask(() => {
|
|
329
|
+
this.component.hydrate(args);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
this.signalReady();
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (this.hasAttribute(LAZY_ATTR)) {
|
|
336
|
+
this.intersectionObserver = new IntersectionObserver(([entry]) => {
|
|
337
|
+
if (entry.isIntersecting) {
|
|
338
|
+
this.intersectionObserver?.disconnect();
|
|
339
|
+
this.intersectionObserver = null;
|
|
340
|
+
this.loadData();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
this.intersectionObserver.observe(this);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await this.loadData();
|
|
347
|
+
}
|
|
348
|
+
disconnectedCallback() {
|
|
349
|
+
this.component.destroy?.();
|
|
350
|
+
this.component.element = void 0;
|
|
351
|
+
this.intersectionObserver?.disconnect();
|
|
352
|
+
this.intersectionObserver = null;
|
|
353
|
+
this.abortController?.abort();
|
|
354
|
+
this.abortController = null;
|
|
355
|
+
this.state = "idle";
|
|
356
|
+
this.data = null;
|
|
357
|
+
this.context = void 0;
|
|
358
|
+
this.dataPromise = null;
|
|
359
|
+
this.errorMessage = "";
|
|
360
|
+
this.signalReady();
|
|
361
|
+
this.deferred = null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Reload component data. Aborts any in-flight request first.
|
|
365
|
+
*/
|
|
366
|
+
async reload() {
|
|
367
|
+
if (this.params === null)
|
|
368
|
+
return;
|
|
369
|
+
this.abortController?.abort();
|
|
370
|
+
this.abortController = new AbortController();
|
|
371
|
+
await this.loadData();
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Fetch a single file by path, with caching.
|
|
375
|
+
* Absolute URLs (http/https) pass through; relative paths get '/' prefix.
|
|
376
|
+
*/
|
|
377
|
+
static loadFile(path) {
|
|
378
|
+
const cached = _ComponentElement.fileCache.get(path);
|
|
379
|
+
if (cached)
|
|
380
|
+
return cached;
|
|
381
|
+
const url = path.startsWith("http://") || path.startsWith("https://") ? path : path.startsWith("/") ? path : "/" + path;
|
|
382
|
+
const promise = fetch(url).then((res) => res.ok ? res.text() : void 0, () => void 0);
|
|
383
|
+
_ComponentElement.fileCache.set(path, promise);
|
|
384
|
+
return promise;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Load all companion files for this widget instance.
|
|
388
|
+
* Uses effectiveFiles (from registration) falling back to component.files.
|
|
389
|
+
*/
|
|
390
|
+
async loadFiles() {
|
|
391
|
+
const filePaths = this.effectiveFiles ?? this.component.files;
|
|
392
|
+
if (!filePaths)
|
|
393
|
+
return {};
|
|
394
|
+
const [html, md, css] = await Promise.all([
|
|
395
|
+
filePaths.html ? _ComponentElement.loadFile(filePaths.html) : void 0,
|
|
396
|
+
filePaths.md ? _ComponentElement.loadFile(filePaths.md) : void 0,
|
|
397
|
+
filePaths.css ? _ComponentElement.loadFile(filePaths.css) : void 0
|
|
398
|
+
]);
|
|
399
|
+
return { html, md, css };
|
|
400
|
+
}
|
|
401
|
+
async loadData() {
|
|
402
|
+
if (this.params === null)
|
|
403
|
+
return;
|
|
404
|
+
const signal = this.abortController?.signal;
|
|
405
|
+
this.state = "loading";
|
|
406
|
+
this.render();
|
|
407
|
+
try {
|
|
408
|
+
const promise = this.component.getData({
|
|
409
|
+
params: this.params,
|
|
410
|
+
signal,
|
|
411
|
+
context: this.context
|
|
412
|
+
});
|
|
413
|
+
this.dataPromise = promise;
|
|
414
|
+
this.data = await promise;
|
|
415
|
+
if (signal?.aborted)
|
|
416
|
+
return;
|
|
417
|
+
this.state = "ready";
|
|
418
|
+
} catch (e) {
|
|
419
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
420
|
+
return;
|
|
421
|
+
if (signal?.aborted)
|
|
422
|
+
return;
|
|
423
|
+
this.setError(e instanceof Error ? e.message : String(e));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this.render();
|
|
427
|
+
this.signalReady();
|
|
428
|
+
}
|
|
429
|
+
setError(message) {
|
|
430
|
+
this.state = "error";
|
|
431
|
+
this.errorMessage = message;
|
|
432
|
+
this.render();
|
|
433
|
+
this.signalReady();
|
|
434
|
+
}
|
|
435
|
+
signalReady() {
|
|
436
|
+
this.deferred?.resolve();
|
|
437
|
+
this.deferred = null;
|
|
438
|
+
}
|
|
439
|
+
render() {
|
|
440
|
+
if (this.params === null) {
|
|
441
|
+
this.shadowRoot.setHTMLUnsafe("");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (this.state === "error") {
|
|
445
|
+
this.shadowRoot.setHTMLUnsafe(this.component.renderError({
|
|
446
|
+
error: new Error(this.errorMessage),
|
|
447
|
+
params: this.params
|
|
448
|
+
}));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.shadowRoot.setHTMLUnsafe(this.component.renderHTML({
|
|
452
|
+
data: this.state === "ready" ? this.data : null,
|
|
453
|
+
params: this.params,
|
|
454
|
+
context: this.context
|
|
455
|
+
}));
|
|
456
|
+
if (this.state === "ready" && this.component.hydrate) {
|
|
457
|
+
const args = { data: this.data, params: this.params, context: this.context };
|
|
458
|
+
queueMicrotask(() => {
|
|
459
|
+
this.component.hydrate(args);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// dist/src/widget/widget.registry.js
|
|
466
|
+
var WidgetRegistry = class {
|
|
467
|
+
widgets = /* @__PURE__ */ new Map();
|
|
468
|
+
/** Register a widget by its name. */
|
|
469
|
+
add(widget) {
|
|
470
|
+
this.widgets.set(widget.name, widget);
|
|
471
|
+
}
|
|
472
|
+
/** Look up a widget by name. */
|
|
473
|
+
get(name) {
|
|
474
|
+
return this.widgets.get(name);
|
|
475
|
+
}
|
|
476
|
+
/** Iterate all registered widgets. */
|
|
477
|
+
[Symbol.iterator]() {
|
|
478
|
+
return this.widgets.values();
|
|
479
|
+
}
|
|
480
|
+
/** Emit a WidgetsManifest from registered widgets. */
|
|
481
|
+
toManifest() {
|
|
482
|
+
const widgets = [];
|
|
483
|
+
const moduleLoaders = {};
|
|
484
|
+
for (const [name, widget] of this.widgets) {
|
|
485
|
+
const entry = {
|
|
486
|
+
name,
|
|
487
|
+
modulePath: name,
|
|
488
|
+
tagName: `widget-${name}`,
|
|
489
|
+
files: widget.files
|
|
490
|
+
};
|
|
491
|
+
widgets.push(entry);
|
|
492
|
+
moduleLoaders[name] = () => Promise.resolve({ default: widget.constructor });
|
|
493
|
+
}
|
|
494
|
+
return { widgets, moduleLoaders };
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// dist/src/route/route.core.js
|
|
499
|
+
var __rewriteRelativeImportExtension = function(path, preserveJsx) {
|
|
500
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
501
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
502
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return path;
|
|
506
|
+
};
|
|
507
|
+
var DEFAULT_BASE_PATH = { html: "/html", md: "/md", app: "/app" };
|
|
508
|
+
var BLOCKED_PROTOCOLS = /^(javascript|data|vbscript):/i;
|
|
509
|
+
function assertSafeRedirect(url) {
|
|
510
|
+
if (BLOCKED_PROTOCOLS.test(url.trim())) {
|
|
511
|
+
throw new Error(`Unsafe redirect URL blocked: ${url}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
var DEFAULT_ROOT_ROUTE = {
|
|
515
|
+
pattern: "/",
|
|
516
|
+
type: "page",
|
|
517
|
+
modulePath: "__default_root__"
|
|
518
|
+
};
|
|
519
|
+
function toRouteConfig(resolved) {
|
|
520
|
+
const node = resolved.node;
|
|
521
|
+
return {
|
|
522
|
+
pattern: resolved.pattern,
|
|
523
|
+
type: node.redirect ? "redirect" : "page",
|
|
524
|
+
modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
|
|
525
|
+
files: node.files
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
var RouteCore = class {
|
|
529
|
+
resolver;
|
|
530
|
+
/** Registered context provider (if any). Exposed so renderers can apply it to inline contexts. */
|
|
531
|
+
contextProvider;
|
|
532
|
+
listeners = /* @__PURE__ */ new Set();
|
|
533
|
+
moduleCache = /* @__PURE__ */ new Map();
|
|
534
|
+
widgetFileCache = /* @__PURE__ */ new Map();
|
|
535
|
+
moduleLoaders;
|
|
536
|
+
currentRoute = null;
|
|
537
|
+
readFile;
|
|
538
|
+
constructor(resolver, options = {}) {
|
|
539
|
+
this.resolver = resolver;
|
|
540
|
+
this.readFile = options.fileReader ?? ((path) => fetch(path, { headers: { Accept: "text/plain" } }).then((r) => r.text()));
|
|
541
|
+
this.contextProvider = options.extendContext;
|
|
542
|
+
this.moduleLoaders = options.moduleLoaders ?? {};
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Get current route parameters.
|
|
546
|
+
*/
|
|
547
|
+
getParams() {
|
|
548
|
+
return this.currentRoute?.params ?? {};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Add event listener for router events.
|
|
552
|
+
*/
|
|
553
|
+
addEventListener(listener) {
|
|
554
|
+
this.listeners.add(listener);
|
|
555
|
+
return () => this.listeners.delete(listener);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Emit router event to listeners.
|
|
559
|
+
*/
|
|
560
|
+
emit(event) {
|
|
561
|
+
for (const listener of this.listeners) {
|
|
562
|
+
try {
|
|
563
|
+
listener(event);
|
|
564
|
+
} catch (e) {
|
|
565
|
+
console.error("[Router] Event listener error:", e);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Match a URL to a route.
|
|
571
|
+
* Falls back to the default root route for '/'.
|
|
572
|
+
*/
|
|
573
|
+
match(url) {
|
|
574
|
+
const pathname = url.pathname;
|
|
575
|
+
const resolved = this.resolver.match(pathname);
|
|
576
|
+
if (resolved) {
|
|
577
|
+
return {
|
|
578
|
+
route: toRouteConfig(resolved),
|
|
579
|
+
params: resolved.params
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (pathname === "/" || pathname === "") {
|
|
583
|
+
return {
|
|
584
|
+
route: DEFAULT_ROOT_ROUTE,
|
|
585
|
+
params: {}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return void 0;
|
|
589
|
+
}
|
|
590
|
+
/** Get status-specific page (404, 401, 403). */
|
|
591
|
+
getStatusPage(status) {
|
|
592
|
+
const node = this.resolver.findRoute(`/${status}`);
|
|
593
|
+
if (!node)
|
|
594
|
+
return void 0;
|
|
595
|
+
return {
|
|
596
|
+
pattern: `/${status}`,
|
|
597
|
+
type: "page",
|
|
598
|
+
modulePath: node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
|
|
599
|
+
files: node.files
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/** Get global error handler (root errorBoundary). */
|
|
603
|
+
getErrorHandler() {
|
|
604
|
+
const modulePath = this.resolver.findErrorBoundary("/");
|
|
605
|
+
if (!modulePath)
|
|
606
|
+
return void 0;
|
|
607
|
+
return { pattern: "/", type: "error", modulePath };
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Find error boundary for a given pathname.
|
|
611
|
+
* Note: pattern is the input pathname, not the boundary's own pattern.
|
|
612
|
+
* Callers should only rely on modulePath.
|
|
613
|
+
*/
|
|
614
|
+
findErrorBoundary(pathname) {
|
|
615
|
+
const modulePath = this.resolver.findErrorBoundary(pathname);
|
|
616
|
+
if (!modulePath)
|
|
617
|
+
return void 0;
|
|
618
|
+
return { pattern: pathname, modulePath };
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Find a route by its exact pattern.
|
|
622
|
+
* Used for building route hierarchy.
|
|
623
|
+
*/
|
|
624
|
+
findRoute(pattern) {
|
|
625
|
+
const node = this.resolver.findRoute(pattern);
|
|
626
|
+
if (!node)
|
|
627
|
+
return void 0;
|
|
628
|
+
return {
|
|
629
|
+
pattern,
|
|
630
|
+
type: node.redirect ? "redirect" : "page",
|
|
631
|
+
modulePath: node.redirect ?? node.files?.ts ?? node.files?.js ?? node.files?.html ?? node.files?.md ?? "",
|
|
632
|
+
files: node.files
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Build route hierarchy from a pattern.
|
|
637
|
+
* Patterns are always unprefixed (no basePath).
|
|
638
|
+
*
|
|
639
|
+
* e.g., '/projects/:id/tasks'
|
|
640
|
+
* → ['/', '/projects', '/projects/:id', '/projects/:id/tasks']
|
|
641
|
+
*/
|
|
642
|
+
buildRouteHierarchy(pattern) {
|
|
643
|
+
if (pattern === "/") {
|
|
644
|
+
return ["/"];
|
|
645
|
+
}
|
|
646
|
+
const segments = pattern.split("/").filter(Boolean);
|
|
647
|
+
const hierarchy = ["/"];
|
|
648
|
+
let current = "";
|
|
649
|
+
for (const segment of segments) {
|
|
650
|
+
current += "/" + segment;
|
|
651
|
+
hierarchy.push(current);
|
|
652
|
+
}
|
|
653
|
+
return hierarchy;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Normalize URL by removing trailing slashes (except bare '/').
|
|
657
|
+
*/
|
|
658
|
+
normalizeUrl(url) {
|
|
659
|
+
if (url.length > 1 && url.endsWith("/")) {
|
|
660
|
+
return url.slice(0, -1);
|
|
661
|
+
}
|
|
662
|
+
return url;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Convert relative path to absolute path.
|
|
666
|
+
*/
|
|
667
|
+
toAbsolutePath(path) {
|
|
668
|
+
return path.startsWith("/") ? path : "/" + path;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Load a module with caching.
|
|
672
|
+
* Uses pre-bundled loaders when available, falls back to dynamic import.
|
|
673
|
+
*/
|
|
674
|
+
async loadModule(modulePath) {
|
|
675
|
+
if (this.moduleCache.has(modulePath)) {
|
|
676
|
+
return this.moduleCache.get(modulePath);
|
|
677
|
+
}
|
|
678
|
+
let module;
|
|
679
|
+
const loader = this.moduleLoaders[modulePath];
|
|
680
|
+
if (loader) {
|
|
681
|
+
module = await loader();
|
|
682
|
+
} else {
|
|
683
|
+
const absolutePath = this.toAbsolutePath(modulePath);
|
|
684
|
+
module = await import(__rewriteRelativeImportExtension(absolutePath));
|
|
685
|
+
}
|
|
686
|
+
this.moduleCache.set(modulePath, module);
|
|
687
|
+
return module;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Load widget file contents with caching.
|
|
691
|
+
*/
|
|
692
|
+
async loadWidgetFiles(widgetFiles) {
|
|
693
|
+
const load = async (path) => {
|
|
694
|
+
const absPath = this.toAbsolutePath(path);
|
|
695
|
+
const cached = this.widgetFileCache.get(absPath);
|
|
696
|
+
if (cached !== void 0)
|
|
697
|
+
return cached;
|
|
698
|
+
try {
|
|
699
|
+
const content = await this.readFile(absPath);
|
|
700
|
+
this.widgetFileCache.set(absPath, content);
|
|
701
|
+
return content;
|
|
702
|
+
} catch (e) {
|
|
703
|
+
console.warn(`[RouteCore] Failed to load widget file ${path}:`, e instanceof Error ? e.message : e);
|
|
704
|
+
return void 0;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const [html, md, css] = await Promise.all([
|
|
708
|
+
widgetFiles.html ? load(widgetFiles.html) : void 0,
|
|
709
|
+
widgetFiles.md ? load(widgetFiles.md) : void 0,
|
|
710
|
+
widgetFiles.css ? load(widgetFiles.css) : void 0
|
|
711
|
+
]);
|
|
712
|
+
return { html, md, css };
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Build a RouteInfo from a matched route and the resolved URL pathname.
|
|
716
|
+
* Called once per navigation; the result is reused across the route hierarchy.
|
|
717
|
+
*/
|
|
718
|
+
toRouteInfo(matched, url) {
|
|
719
|
+
return {
|
|
720
|
+
url,
|
|
721
|
+
params: matched.params
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get inlined `__files` from a cached module (merged module pattern).
|
|
726
|
+
* Returns undefined if the module isn't cached or has no __files.
|
|
727
|
+
*/
|
|
728
|
+
getModuleFiles(modulePath) {
|
|
729
|
+
const cached = this.moduleCache.get(modulePath);
|
|
730
|
+
if (!cached || typeof cached !== "object")
|
|
731
|
+
return void 0;
|
|
732
|
+
const files = cached.__files;
|
|
733
|
+
if (!files || typeof files !== "object")
|
|
734
|
+
return void 0;
|
|
735
|
+
return files;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Build a ComponentContext by extending RouteInfo with loaded file contents.
|
|
739
|
+
*
|
|
740
|
+
* When the route module is a merged module (contains `__files`), uses
|
|
741
|
+
* inlined content directly. Otherwise falls back to reading companion files.
|
|
742
|
+
*/
|
|
743
|
+
async buildComponentContext(routeInfo, route, signal, isLeaf) {
|
|
744
|
+
const rf = route.files;
|
|
745
|
+
const modulePath = rf?.ts ?? rf?.js;
|
|
746
|
+
const inlined = modulePath ? this.getModuleFiles(modulePath) : void 0;
|
|
747
|
+
let html;
|
|
748
|
+
let md;
|
|
749
|
+
let css;
|
|
750
|
+
if (inlined) {
|
|
751
|
+
html = inlined.html;
|
|
752
|
+
md = inlined.md;
|
|
753
|
+
css = inlined.css;
|
|
754
|
+
} else {
|
|
755
|
+
const fetchFile = (filePath) => this.readFile(this.toAbsolutePath(filePath));
|
|
756
|
+
[html, md, css] = await Promise.all([
|
|
757
|
+
rf?.html ? fetchFile(rf.html) : void 0,
|
|
758
|
+
rf?.md ? fetchFile(rf.md) : void 0,
|
|
759
|
+
rf?.css ? fetchFile(rf.css) : void 0
|
|
760
|
+
]);
|
|
761
|
+
}
|
|
762
|
+
const base = {
|
|
763
|
+
...routeInfo,
|
|
764
|
+
pathname: routeInfo.url.pathname,
|
|
765
|
+
searchParams: routeInfo.url.searchParams,
|
|
766
|
+
files: { html, md, css },
|
|
767
|
+
signal,
|
|
768
|
+
isLeaf
|
|
769
|
+
};
|
|
770
|
+
return this.contextProvider ? this.contextProvider(base) : base;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// dist/src/renderer/spa/thin-client.js
|
|
775
|
+
var EmrouteApp = class {
|
|
776
|
+
server;
|
|
777
|
+
appBase;
|
|
778
|
+
slot = null;
|
|
779
|
+
abortController = null;
|
|
780
|
+
constructor(server, options) {
|
|
781
|
+
const bp = options?.basePath ?? DEFAULT_BASE_PATH;
|
|
782
|
+
this.server = server;
|
|
783
|
+
this.appBase = bp.app;
|
|
784
|
+
}
|
|
785
|
+
async initialize(slotSelector = "router-slot") {
|
|
786
|
+
this.slot = document.querySelector(slotSelector);
|
|
787
|
+
if (!this.slot) {
|
|
788
|
+
console.error("[EmrouteApp] Slot not found:", slotSelector);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (!("navigation" in globalThis)) {
|
|
792
|
+
console.warn("[EmrouteApp] Navigation API not available");
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
this.abortController = new AbortController();
|
|
796
|
+
const { signal } = this.abortController;
|
|
797
|
+
navigation.addEventListener("navigate", (event) => {
|
|
798
|
+
if (!event.canIntercept)
|
|
799
|
+
return;
|
|
800
|
+
if (event.hashChange)
|
|
801
|
+
return;
|
|
802
|
+
if (event.downloadRequest !== null)
|
|
803
|
+
return;
|
|
804
|
+
const url = new URL(event.destination.url);
|
|
805
|
+
if (!this.isAppPath(url.pathname))
|
|
806
|
+
return;
|
|
807
|
+
event.intercept({
|
|
808
|
+
scroll: "manual",
|
|
809
|
+
handler: async () => {
|
|
810
|
+
await this.handleNavigation(url, event.signal);
|
|
811
|
+
event.scroll();
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
}, { signal });
|
|
815
|
+
const ssrRoute = this.slot.getAttribute("data-ssr-route");
|
|
816
|
+
if (ssrRoute && (location.pathname === ssrRoute || location.pathname === ssrRoute + "/")) {
|
|
817
|
+
this.slot.removeAttribute("data-ssr-route");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
await this.handleNavigation(new URL(location.href), this.abortController.signal);
|
|
821
|
+
}
|
|
822
|
+
dispose() {
|
|
823
|
+
this.abortController?.abort();
|
|
824
|
+
this.abortController = null;
|
|
825
|
+
this.slot = null;
|
|
826
|
+
}
|
|
827
|
+
async navigate(url, options = {}) {
|
|
828
|
+
try {
|
|
829
|
+
const { finished } = navigation.navigate(url, {
|
|
830
|
+
state: options.state,
|
|
831
|
+
history: options.replace ? "replace" : "auto"
|
|
832
|
+
});
|
|
833
|
+
await finished;
|
|
834
|
+
} catch (e) {
|
|
835
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
836
|
+
return;
|
|
837
|
+
throw e;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
isAppPath(pathname) {
|
|
841
|
+
return pathname === this.appBase || pathname.startsWith(this.appBase + "/");
|
|
842
|
+
}
|
|
843
|
+
stripAppBase(pathname) {
|
|
844
|
+
if (pathname === this.appBase)
|
|
845
|
+
return "/";
|
|
846
|
+
if (pathname.startsWith(this.appBase + "/"))
|
|
847
|
+
return pathname.slice(this.appBase.length);
|
|
848
|
+
return pathname;
|
|
849
|
+
}
|
|
850
|
+
async handleNavigation(url, signal) {
|
|
851
|
+
if (!this.slot || !this.server.htmlRouter)
|
|
852
|
+
return;
|
|
853
|
+
const routePath = this.stripAppBase(url.pathname);
|
|
854
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
855
|
+
try {
|
|
856
|
+
const { content, title, redirect } = await this.server.htmlRouter.render(routeUrl, signal);
|
|
857
|
+
if (signal.aborted)
|
|
858
|
+
return;
|
|
859
|
+
if (redirect) {
|
|
860
|
+
assertSafeRedirect(redirect);
|
|
861
|
+
const target = redirect.startsWith("/") ? this.appBase + redirect : redirect;
|
|
862
|
+
navigation.navigate(target, { history: "replace" });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (document.startViewTransition) {
|
|
866
|
+
const transition = document.startViewTransition(() => {
|
|
867
|
+
this.slot.setHTMLUnsafe(content);
|
|
868
|
+
});
|
|
869
|
+
signal.addEventListener("abort", () => transition.skipTransition(), { once: true });
|
|
870
|
+
await transition.updateCallbackDone;
|
|
871
|
+
} else {
|
|
872
|
+
this.slot.setHTMLUnsafe(content);
|
|
873
|
+
}
|
|
874
|
+
if (title)
|
|
875
|
+
document.title = title;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (signal.aborted)
|
|
878
|
+
return;
|
|
879
|
+
console.error("[EmrouteApp] Navigation error:", error);
|
|
880
|
+
if (this.slot) {
|
|
881
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
882
|
+
this.slot.setHTMLUnsafe(`<h1>Error</h1><p>${escapeHtml(message)}</p>`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
async function createEmrouteApp(server, options) {
|
|
888
|
+
const g = globalThis;
|
|
889
|
+
if (g.__emroute_app) {
|
|
890
|
+
console.warn("eMroute: App already initialized.");
|
|
891
|
+
return g.__emroute_app;
|
|
892
|
+
}
|
|
893
|
+
const app = new EmrouteApp(server, options);
|
|
894
|
+
await app.initialize();
|
|
895
|
+
g.__emroute_app = app;
|
|
896
|
+
return app;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// dist/src/component/abstract.component.js
|
|
900
|
+
var Component = class {
|
|
901
|
+
/** Host element reference, set by ComponentElement in the browser. */
|
|
902
|
+
element;
|
|
903
|
+
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
904
|
+
files;
|
|
905
|
+
/**
|
|
906
|
+
* When true, SSR serializes the getData() result into the element's
|
|
907
|
+
* light DOM so the client can access it immediately in hydrate()
|
|
908
|
+
* without re-fetching.
|
|
909
|
+
*
|
|
910
|
+
* Default is false — hydrate() receives `data: null`. Most widgets
|
|
911
|
+
* don't need this because the rendered Shadow DOM already contains
|
|
912
|
+
* the visual representation of the data.
|
|
913
|
+
*
|
|
914
|
+
* If you find yourself parsing the shadow DOM in hydrate() trying to
|
|
915
|
+
* reconstruct the original data object, set this to true instead.
|
|
916
|
+
* The server-fetched data will be available as `args.data` in hydrate().
|
|
917
|
+
*/
|
|
918
|
+
exposeSsrData;
|
|
919
|
+
/**
|
|
920
|
+
* Render as HTML for browser context.
|
|
921
|
+
*
|
|
922
|
+
* Default implementation converts renderMarkdown() output to HTML.
|
|
923
|
+
* Override for custom HTML rendering with rich styling/interactivity.
|
|
924
|
+
*/
|
|
925
|
+
renderHTML(args) {
|
|
926
|
+
if (args.data === null) {
|
|
927
|
+
return `<div data-component="${this.name}">Loading...</div>`;
|
|
928
|
+
}
|
|
929
|
+
const markdown = this.renderMarkdown({
|
|
930
|
+
data: args.data,
|
|
931
|
+
params: args.params,
|
|
932
|
+
context: args.context
|
|
933
|
+
});
|
|
934
|
+
return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Render error state.
|
|
938
|
+
*/
|
|
939
|
+
renderError(args) {
|
|
940
|
+
const msg = args.error instanceof Error ? args.error.message : String(args.error);
|
|
941
|
+
return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Render error as markdown.
|
|
945
|
+
*/
|
|
946
|
+
renderMarkdownError(error) {
|
|
947
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
948
|
+
return `> **Error** (\`${this.name}\`): ${msg}`;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
// dist/src/component/page.component.js
|
|
953
|
+
var PageComponent = class extends Component {
|
|
954
|
+
name = "page";
|
|
955
|
+
/** Route pattern this page handles (optional — set by subclasses) */
|
|
956
|
+
pattern;
|
|
957
|
+
/**
|
|
958
|
+
* Fetch or compute page data. Override in subclasses.
|
|
959
|
+
* Default: returns null (no data needed).
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```ts
|
|
963
|
+
* override getData({ params, context }: this['DataArgs']) {
|
|
964
|
+
* return fetch(`/api/${params.id}`, { signal: context?.signal });
|
|
965
|
+
* }
|
|
966
|
+
* ```
|
|
967
|
+
*/
|
|
968
|
+
getData(_args) {
|
|
969
|
+
return Promise.resolve(null);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Render page as HTML.
|
|
973
|
+
*
|
|
974
|
+
* Fallback chain:
|
|
975
|
+
* 1. html file content from context
|
|
976
|
+
* 2. md file content wrapped in `<mark-down>`
|
|
977
|
+
* 3. `<router-slot />` (bare slot for child routes)
|
|
978
|
+
*
|
|
979
|
+
* @example
|
|
980
|
+
* ```ts
|
|
981
|
+
* override renderHTML({ data, params, context }: this['RenderArgs']) {
|
|
982
|
+
* return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
|
|
983
|
+
* }
|
|
984
|
+
* ```
|
|
985
|
+
*/
|
|
986
|
+
renderHTML(args) {
|
|
987
|
+
const files = args.context.files;
|
|
988
|
+
const style = files?.css ? `<style>${files.css}</style>
|
|
989
|
+
` : "";
|
|
990
|
+
if (files?.html) {
|
|
991
|
+
let html = style + files.html;
|
|
992
|
+
if (files.md && html.includes("<mark-down></mark-down>")) {
|
|
993
|
+
html = html.replace("<mark-down></mark-down>", `<mark-down>${escapeHtml(files.md)}</mark-down>`);
|
|
994
|
+
}
|
|
995
|
+
return html;
|
|
996
|
+
}
|
|
997
|
+
if (files?.md) {
|
|
998
|
+
const hasSlot = files.md.includes("```router-slot");
|
|
999
|
+
const slot = args.context.isLeaf || hasSlot ? "" : "\n<router-slot></router-slot>";
|
|
1000
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
|
|
1001
|
+
}
|
|
1002
|
+
return args.context.isLeaf ? "" : "<router-slot></router-slot>";
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Render page as Markdown.
|
|
1006
|
+
*
|
|
1007
|
+
* Fallback chain:
|
|
1008
|
+
* 1. md file content from context
|
|
1009
|
+
* 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
|
|
1010
|
+
*
|
|
1011
|
+
* @example
|
|
1012
|
+
* ```ts
|
|
1013
|
+
* override renderMarkdown({ data, params, context }: this['RenderArgs']) {
|
|
1014
|
+
* return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
|
|
1015
|
+
* }
|
|
1016
|
+
* ```
|
|
1017
|
+
*/
|
|
1018
|
+
renderMarkdown(args) {
|
|
1019
|
+
const files = args.context.files;
|
|
1020
|
+
if (files?.md) {
|
|
1021
|
+
return files.md;
|
|
1022
|
+
}
|
|
1023
|
+
return args.context.isLeaf ? "" : "```router-slot\n```";
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Page title. Override in subclasses.
|
|
1027
|
+
* Default: undefined (no title).
|
|
1028
|
+
*
|
|
1029
|
+
* @example
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* override getTitle({ data, params }: this['RenderArgs']) {
|
|
1032
|
+
* return `Project ${params.id}`;
|
|
1033
|
+
* }
|
|
1034
|
+
* ```
|
|
1035
|
+
*/
|
|
1036
|
+
getTitle(_args) {
|
|
1037
|
+
return void 0;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
var page_component_default = new PageComponent();
|
|
1041
|
+
|
|
1042
|
+
// dist/src/component/widget.component.js
|
|
1043
|
+
var WidgetComponent = class extends Component {
|
|
1044
|
+
/**
|
|
1045
|
+
* Render widget as HTML.
|
|
1046
|
+
*
|
|
1047
|
+
* Fallback chain:
|
|
1048
|
+
* 1. html file content from context
|
|
1049
|
+
* 2. md file content wrapped in `<mark-down>`
|
|
1050
|
+
* 3. base Component default (markdown→HTML conversion)
|
|
1051
|
+
*
|
|
1052
|
+
* @example
|
|
1053
|
+
* ```ts
|
|
1054
|
+
* override renderHTML({ data, params }: this['RenderArgs']) {
|
|
1055
|
+
* return `<span>${params.coin}: $${data?.price}</span>`;
|
|
1056
|
+
* }
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
renderHTML(args) {
|
|
1060
|
+
const files = args.context.files;
|
|
1061
|
+
const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>
|
|
1062
|
+
` : "";
|
|
1063
|
+
if (files?.html) {
|
|
1064
|
+
return style + files.html;
|
|
1065
|
+
}
|
|
1066
|
+
if (files?.md) {
|
|
1067
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>`;
|
|
1068
|
+
}
|
|
1069
|
+
if (style) {
|
|
1070
|
+
return style + super.renderHTML(args);
|
|
1071
|
+
}
|
|
1072
|
+
return super.renderHTML(args);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Render widget as Markdown.
|
|
1076
|
+
*
|
|
1077
|
+
* Fallback chain:
|
|
1078
|
+
* 1. md file content from context
|
|
1079
|
+
* 2. empty string
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```ts
|
|
1083
|
+
* override renderMarkdown({ data, params }: this['RenderArgs']) {
|
|
1084
|
+
* return `**${params.coin}**: $${data?.price}`;
|
|
1085
|
+
* }
|
|
1086
|
+
* ```
|
|
1087
|
+
*/
|
|
1088
|
+
renderMarkdown(args) {
|
|
1089
|
+
const files = args.context.files;
|
|
1090
|
+
if (files?.md) {
|
|
1091
|
+
return files.md;
|
|
1092
|
+
}
|
|
1093
|
+
return "";
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
// dist/src/route/route.trie.js
|
|
1098
|
+
function createNode() {
|
|
1099
|
+
return { static: /* @__PURE__ */ new Map() };
|
|
1100
|
+
}
|
|
1101
|
+
function safeDecode(segment) {
|
|
1102
|
+
try {
|
|
1103
|
+
return decodeURIComponent(segment);
|
|
1104
|
+
} catch {
|
|
1105
|
+
return segment;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function splitSegments(pathname) {
|
|
1109
|
+
return pathname.substring(1).split("/");
|
|
1110
|
+
}
|
|
1111
|
+
function convertNode(source, pattern) {
|
|
1112
|
+
const node = createNode();
|
|
1113
|
+
if (source.files || source.redirect) {
|
|
1114
|
+
node.route = source;
|
|
1115
|
+
node.pattern = pattern;
|
|
1116
|
+
}
|
|
1117
|
+
if (source.errorBoundary) {
|
|
1118
|
+
node.errorBoundary = source.errorBoundary;
|
|
1119
|
+
}
|
|
1120
|
+
if (source.children) {
|
|
1121
|
+
for (const [segment, child] of Object.entries(source.children)) {
|
|
1122
|
+
const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
|
|
1123
|
+
node.static.set(segment, convertNode(child, childPattern));
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (source.dynamic) {
|
|
1127
|
+
const { param, child } = source.dynamic;
|
|
1128
|
+
const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
|
|
1129
|
+
node.dynamic = { param, node: convertNode(child, childPattern) };
|
|
1130
|
+
}
|
|
1131
|
+
if (source.wildcard) {
|
|
1132
|
+
const { param, child } = source.wildcard;
|
|
1133
|
+
const childPattern = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
|
|
1134
|
+
node.wildcard = { param, node: convertNode(child, childPattern) };
|
|
1135
|
+
}
|
|
1136
|
+
return node;
|
|
1137
|
+
}
|
|
1138
|
+
var RouteTrie = class {
|
|
1139
|
+
root;
|
|
1140
|
+
constructor(tree) {
|
|
1141
|
+
this.root = convertNode(tree, "/");
|
|
1142
|
+
}
|
|
1143
|
+
match(pathname) {
|
|
1144
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
1145
|
+
pathname = pathname.slice(0, -1);
|
|
1146
|
+
}
|
|
1147
|
+
if (!pathname.startsWith("/")) {
|
|
1148
|
+
pathname = "/" + pathname;
|
|
1149
|
+
}
|
|
1150
|
+
if (pathname === "/") {
|
|
1151
|
+
if (this.root.route) {
|
|
1152
|
+
return { node: this.root.route, pattern: "/", params: {} };
|
|
1153
|
+
}
|
|
1154
|
+
return void 0;
|
|
1155
|
+
}
|
|
1156
|
+
const segments = splitSegments(pathname);
|
|
1157
|
+
return this.walk(this.root, segments, 0, {});
|
|
1158
|
+
}
|
|
1159
|
+
findErrorBoundary(pathname) {
|
|
1160
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
1161
|
+
pathname = pathname.slice(0, -1);
|
|
1162
|
+
}
|
|
1163
|
+
if (!pathname.startsWith("/")) {
|
|
1164
|
+
pathname = "/" + pathname;
|
|
1165
|
+
}
|
|
1166
|
+
if (pathname === "/")
|
|
1167
|
+
return this.root.errorBoundary;
|
|
1168
|
+
const segments = splitSegments(pathname);
|
|
1169
|
+
return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
|
|
1170
|
+
}
|
|
1171
|
+
findRoute(pattern) {
|
|
1172
|
+
if (pattern === "/") {
|
|
1173
|
+
return this.root.route;
|
|
1174
|
+
}
|
|
1175
|
+
const segments = splitSegments(pattern);
|
|
1176
|
+
let node = this.root;
|
|
1177
|
+
for (const segment of segments) {
|
|
1178
|
+
let child;
|
|
1179
|
+
if (segment.startsWith(":") && segment.endsWith("*")) {
|
|
1180
|
+
child = node.wildcard?.node;
|
|
1181
|
+
} else if (segment.startsWith(":")) {
|
|
1182
|
+
child = node.dynamic?.node;
|
|
1183
|
+
} else {
|
|
1184
|
+
child = node.static.get(segment);
|
|
1185
|
+
}
|
|
1186
|
+
if (!child)
|
|
1187
|
+
return void 0;
|
|
1188
|
+
node = child;
|
|
1189
|
+
}
|
|
1190
|
+
return node.route;
|
|
1191
|
+
}
|
|
1192
|
+
// ── Private matching ──────────────────────────────────────────────────
|
|
1193
|
+
walk(node, segments, index, params) {
|
|
1194
|
+
if (index === segments.length) {
|
|
1195
|
+
if (node.route) {
|
|
1196
|
+
return { node: node.route, pattern: node.pattern, params: { ...params } };
|
|
1197
|
+
}
|
|
1198
|
+
if (node.wildcard?.node.route) {
|
|
1199
|
+
return {
|
|
1200
|
+
node: node.wildcard.node.route,
|
|
1201
|
+
pattern: node.wildcard.node.pattern,
|
|
1202
|
+
params: { ...params, [node.wildcard.param]: "" }
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
return void 0;
|
|
1206
|
+
}
|
|
1207
|
+
const segment = segments[index];
|
|
1208
|
+
const staticChild = node.static.get(segment);
|
|
1209
|
+
if (staticChild) {
|
|
1210
|
+
const result = this.walk(staticChild, segments, index + 1, params);
|
|
1211
|
+
if (result)
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
if (node.dynamic) {
|
|
1215
|
+
const { param, node: dynamicNode } = node.dynamic;
|
|
1216
|
+
params[param] = safeDecode(segment);
|
|
1217
|
+
const result = this.walk(dynamicNode, segments, index + 1, params);
|
|
1218
|
+
if (result)
|
|
1219
|
+
return result;
|
|
1220
|
+
delete params[param];
|
|
1221
|
+
}
|
|
1222
|
+
if (node.wildcard?.node.route) {
|
|
1223
|
+
const { param, node: wildcardNode } = node.wildcard;
|
|
1224
|
+
let rest = safeDecode(segments[index]);
|
|
1225
|
+
for (let i = index + 1; i < segments.length; i++) {
|
|
1226
|
+
rest += "/" + safeDecode(segments[i]);
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
node: wildcardNode.route,
|
|
1230
|
+
pattern: wildcardNode.pattern,
|
|
1231
|
+
params: { ...params, [param]: rest }
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Walk for error boundary. Follows the same priority as match
|
|
1238
|
+
* (static → dynamic → wildcard) without backtracking across branches.
|
|
1239
|
+
* Returns the deepest error boundary module path found along the path.
|
|
1240
|
+
*/
|
|
1241
|
+
walkForBoundary(node, segments, index, deepest) {
|
|
1242
|
+
if (index === segments.length) {
|
|
1243
|
+
return node.errorBoundary ?? deepest;
|
|
1244
|
+
}
|
|
1245
|
+
const segment = segments[index];
|
|
1246
|
+
const staticChild = node.static.get(segment);
|
|
1247
|
+
if (staticChild) {
|
|
1248
|
+
return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
|
|
1249
|
+
}
|
|
1250
|
+
if (node.dynamic) {
|
|
1251
|
+
return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
|
|
1252
|
+
}
|
|
1253
|
+
if (node.wildcard) {
|
|
1254
|
+
return node.wildcard.node.errorBoundary ?? deepest;
|
|
1255
|
+
}
|
|
1256
|
+
return deepest;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
// dist/src/type/logger.type.js
|
|
1261
|
+
var noop = () => {
|
|
1262
|
+
};
|
|
1263
|
+
var logger = { error: noop, warn: noop };
|
|
1264
|
+
function setLogger(impl) {
|
|
1265
|
+
logger.error = impl.error.bind(impl);
|
|
1266
|
+
logger.warn = impl.warn.bind(impl);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// dist/src/overlay/overlay.css.js
|
|
1270
|
+
var overlayCSS = (
|
|
1271
|
+
/* css */
|
|
1272
|
+
`
|
|
1273
|
+
:root {
|
|
1274
|
+
--overlay-backdrop: oklch(0% 0 0 / 0.5);
|
|
1275
|
+
--overlay-surface: oklch(100% 0 0);
|
|
1276
|
+
--overlay-radius: 8px;
|
|
1277
|
+
--overlay-shadow: 0 8px 32px oklch(0% 0 0 / 0.2);
|
|
1278
|
+
--overlay-toast-gap: 8px;
|
|
1279
|
+
--overlay-toast-duration: 5s;
|
|
1280
|
+
--overlay-z: 1000;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/* --- Modal (dialog) --- */
|
|
1284
|
+
|
|
1285
|
+
dialog[data-overlay-modal] {
|
|
1286
|
+
border: none;
|
|
1287
|
+
padding: 0;
|
|
1288
|
+
background: var(--overlay-surface);
|
|
1289
|
+
border-radius: var(--overlay-radius);
|
|
1290
|
+
box-shadow: var(--overlay-shadow);
|
|
1291
|
+
max-width: min(90vw, 560px);
|
|
1292
|
+
max-height: 85vh;
|
|
1293
|
+
overflow: auto;
|
|
1294
|
+
opacity: 1;
|
|
1295
|
+
translate: 0 0;
|
|
1296
|
+
transition:
|
|
1297
|
+
opacity 200ms,
|
|
1298
|
+
translate 200ms;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
dialog[data-overlay-modal][open] {
|
|
1302
|
+
transition:
|
|
1303
|
+
opacity 200ms,
|
|
1304
|
+
translate 200ms,
|
|
1305
|
+
display 200ms allow-discrete,
|
|
1306
|
+
overlay 200ms allow-discrete;
|
|
1307
|
+
|
|
1308
|
+
@starting-style {
|
|
1309
|
+
opacity: 0;
|
|
1310
|
+
translate: 0 20px;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
dialog[data-overlay-modal]::backdrop {
|
|
1315
|
+
background: var(--overlay-backdrop);
|
|
1316
|
+
opacity: 1;
|
|
1317
|
+
transition: opacity 200ms;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
dialog[data-overlay-modal][open]::backdrop {
|
|
1321
|
+
transition:
|
|
1322
|
+
opacity 200ms,
|
|
1323
|
+
display 200ms allow-discrete,
|
|
1324
|
+
overlay 200ms allow-discrete;
|
|
1325
|
+
|
|
1326
|
+
@starting-style {
|
|
1327
|
+
opacity: 0;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
dialog[data-overlay-modal][data-dismissing] {
|
|
1332
|
+
opacity: 0;
|
|
1333
|
+
translate: 0 20px;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
dialog[data-overlay-modal][data-dismissing]::backdrop {
|
|
1337
|
+
opacity: 0;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/* --- Toast container --- */
|
|
1341
|
+
|
|
1342
|
+
[data-overlay-toast-container] {
|
|
1343
|
+
position: fixed;
|
|
1344
|
+
bottom: 16px;
|
|
1345
|
+
right: 16px;
|
|
1346
|
+
z-index: var(--overlay-z);
|
|
1347
|
+
display: flex;
|
|
1348
|
+
flex-direction: column;
|
|
1349
|
+
gap: var(--overlay-toast-gap);
|
|
1350
|
+
pointer-events: none;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/* --- Toast item --- */
|
|
1354
|
+
|
|
1355
|
+
[data-overlay-toast] {
|
|
1356
|
+
pointer-events: auto;
|
|
1357
|
+
background: var(--overlay-surface);
|
|
1358
|
+
border-radius: var(--overlay-radius);
|
|
1359
|
+
box-shadow: var(--overlay-shadow);
|
|
1360
|
+
padding: 12px 16px;
|
|
1361
|
+
animation: overlay-toast-auto var(--overlay-toast-duration, 5s) ease-in-out forwards;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/* Manual toast (timeout: 0): no auto-dismiss, entry transition only */
|
|
1365
|
+
[data-overlay-toast][data-toast-manual] {
|
|
1366
|
+
animation: none;
|
|
1367
|
+
opacity: 1;
|
|
1368
|
+
translate: 0 0;
|
|
1369
|
+
transition:
|
|
1370
|
+
opacity 200ms,
|
|
1371
|
+
translate 200ms;
|
|
1372
|
+
|
|
1373
|
+
@starting-style {
|
|
1374
|
+
opacity: 0;
|
|
1375
|
+
translate: 20px 0;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/* Dismissed toast: CSS exit animation */
|
|
1380
|
+
[data-overlay-toast][data-dismissing] {
|
|
1381
|
+
animation: overlay-toast-exit 200ms ease-in forwards;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
@keyframes overlay-toast-auto {
|
|
1385
|
+
0% { opacity: 0; translate: 20px 0; }
|
|
1386
|
+
10% { opacity: 1; translate: 0 0; }
|
|
1387
|
+
80% { opacity: 1; translate: 0 0; }
|
|
1388
|
+
100% { opacity: 0; translate: 0 0; display: none; }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
@keyframes overlay-toast-exit {
|
|
1392
|
+
to { opacity: 0; translate: 20px 0; display: none; }
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/* --- Popover --- */
|
|
1396
|
+
|
|
1397
|
+
[data-overlay-popover] {
|
|
1398
|
+
border: none;
|
|
1399
|
+
padding: 0;
|
|
1400
|
+
margin: 0;
|
|
1401
|
+
background: var(--overlay-surface);
|
|
1402
|
+
border-radius: var(--overlay-radius);
|
|
1403
|
+
box-shadow: var(--overlay-shadow);
|
|
1404
|
+
opacity: 1;
|
|
1405
|
+
scale: 1;
|
|
1406
|
+
transition:
|
|
1407
|
+
opacity 200ms,
|
|
1408
|
+
scale 200ms;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
[data-overlay-popover]:popover-open {
|
|
1412
|
+
position-anchor: auto;
|
|
1413
|
+
inset: unset;
|
|
1414
|
+
top: anchor(bottom);
|
|
1415
|
+
left: anchor(start);
|
|
1416
|
+
margin-top: 4px;
|
|
1417
|
+
transition:
|
|
1418
|
+
opacity 200ms,
|
|
1419
|
+
scale 200ms,
|
|
1420
|
+
display 200ms allow-discrete,
|
|
1421
|
+
overlay 200ms allow-discrete;
|
|
1422
|
+
|
|
1423
|
+
@starting-style {
|
|
1424
|
+
opacity: 0;
|
|
1425
|
+
scale: 0.95;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
[data-overlay-popover][data-dismissing] {
|
|
1430
|
+
opacity: 0;
|
|
1431
|
+
scale: 0.95;
|
|
1432
|
+
}
|
|
1433
|
+
`
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
// dist/src/overlay/overlay.service.js
|
|
1437
|
+
var ANIMATION_SAFETY_TIMEOUT = 300;
|
|
1438
|
+
function animateDismiss(el, onDone) {
|
|
1439
|
+
el.setAttribute("data-dismissing", "");
|
|
1440
|
+
let done = false;
|
|
1441
|
+
const finish = () => {
|
|
1442
|
+
if (done)
|
|
1443
|
+
return;
|
|
1444
|
+
done = true;
|
|
1445
|
+
onDone();
|
|
1446
|
+
};
|
|
1447
|
+
el.addEventListener("transitionend", finish, { once: true });
|
|
1448
|
+
setTimeout(finish, ANIMATION_SAFETY_TIMEOUT);
|
|
1449
|
+
}
|
|
1450
|
+
function createOverlayService() {
|
|
1451
|
+
let styleInjected = false;
|
|
1452
|
+
let dialog = null;
|
|
1453
|
+
let modalResolve = null;
|
|
1454
|
+
let modalOnClose;
|
|
1455
|
+
let toastContainer = null;
|
|
1456
|
+
let popoverEl = null;
|
|
1457
|
+
let popoverAnchorObserver = null;
|
|
1458
|
+
const supportsAnchor = typeof CSS !== "undefined" && CSS.supports("anchor-name", "--a");
|
|
1459
|
+
function injectCSS() {
|
|
1460
|
+
if (styleInjected)
|
|
1461
|
+
return;
|
|
1462
|
+
styleInjected = true;
|
|
1463
|
+
const style = document.createElement("style");
|
|
1464
|
+
style.textContent = overlayCSS;
|
|
1465
|
+
document.head.appendChild(style);
|
|
1466
|
+
}
|
|
1467
|
+
function ensureDialog() {
|
|
1468
|
+
if (dialog)
|
|
1469
|
+
return dialog;
|
|
1470
|
+
injectCSS();
|
|
1471
|
+
dialog = document.createElement("dialog");
|
|
1472
|
+
dialog.setAttribute("data-overlay-modal", "");
|
|
1473
|
+
document.body.appendChild(dialog);
|
|
1474
|
+
dialog.addEventListener("click", (e) => {
|
|
1475
|
+
if (e.target === dialog) {
|
|
1476
|
+
closeModal(void 0);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
return dialog;
|
|
1480
|
+
}
|
|
1481
|
+
function ensureToastContainer() {
|
|
1482
|
+
if (toastContainer)
|
|
1483
|
+
return toastContainer;
|
|
1484
|
+
injectCSS();
|
|
1485
|
+
toastContainer = document.createElement("div");
|
|
1486
|
+
toastContainer.setAttribute("data-overlay-toast-container", "");
|
|
1487
|
+
document.body.appendChild(toastContainer);
|
|
1488
|
+
return toastContainer;
|
|
1489
|
+
}
|
|
1490
|
+
function ensurePopover() {
|
|
1491
|
+
if (popoverEl)
|
|
1492
|
+
return popoverEl;
|
|
1493
|
+
injectCSS();
|
|
1494
|
+
popoverEl = document.createElement("div");
|
|
1495
|
+
popoverEl.setAttribute("data-overlay-popover", "");
|
|
1496
|
+
popoverEl.setAttribute("popover", "");
|
|
1497
|
+
document.body.appendChild(popoverEl);
|
|
1498
|
+
return popoverEl;
|
|
1499
|
+
}
|
|
1500
|
+
function modal(options) {
|
|
1501
|
+
const d = ensureDialog();
|
|
1502
|
+
d.removeAttribute("data-dismissing");
|
|
1503
|
+
hidePopoverImmediate();
|
|
1504
|
+
if (d.open) {
|
|
1505
|
+
d.close();
|
|
1506
|
+
if (modalResolve) {
|
|
1507
|
+
modalResolve(void 0);
|
|
1508
|
+
modalResolve = null;
|
|
1509
|
+
}
|
|
1510
|
+
if (modalOnClose) {
|
|
1511
|
+
modalOnClose();
|
|
1512
|
+
modalOnClose = void 0;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
d.innerHTML = "";
|
|
1516
|
+
options.render(d);
|
|
1517
|
+
modalOnClose = options.onClose;
|
|
1518
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
1519
|
+
modalResolve = resolve;
|
|
1520
|
+
d.showModal();
|
|
1521
|
+
return promise;
|
|
1522
|
+
}
|
|
1523
|
+
function closeModal(value) {
|
|
1524
|
+
if (!dialog || !dialog.open)
|
|
1525
|
+
return;
|
|
1526
|
+
const resolve = modalResolve;
|
|
1527
|
+
const onClose = modalOnClose;
|
|
1528
|
+
const dialogRef = dialog;
|
|
1529
|
+
modalResolve = null;
|
|
1530
|
+
modalOnClose = void 0;
|
|
1531
|
+
animateDismiss(dialogRef, () => {
|
|
1532
|
+
if (dialogRef && dialogRef.open) {
|
|
1533
|
+
dialogRef.close();
|
|
1534
|
+
if (resolve)
|
|
1535
|
+
resolve(value);
|
|
1536
|
+
if (onClose)
|
|
1537
|
+
onClose();
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
function clearDeadToasts(container) {
|
|
1542
|
+
for (const child of [...container.children]) {
|
|
1543
|
+
const el = child;
|
|
1544
|
+
if (el.hasAttribute("data-dismissing")) {
|
|
1545
|
+
el.remove();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function toast(options) {
|
|
1550
|
+
const container = ensureToastContainer();
|
|
1551
|
+
clearDeadToasts(container);
|
|
1552
|
+
const el = document.createElement("div");
|
|
1553
|
+
el.setAttribute("data-overlay-toast", "");
|
|
1554
|
+
const timeout = options.timeout ?? 0;
|
|
1555
|
+
if (timeout === 0) {
|
|
1556
|
+
el.setAttribute("data-toast-manual", "");
|
|
1557
|
+
} else {
|
|
1558
|
+
el.style.setProperty("--overlay-toast-duration", `${timeout}ms`);
|
|
1559
|
+
}
|
|
1560
|
+
options.render(el);
|
|
1561
|
+
container.appendChild(el);
|
|
1562
|
+
let dismissed = false;
|
|
1563
|
+
const dismiss = () => {
|
|
1564
|
+
if (dismissed)
|
|
1565
|
+
return;
|
|
1566
|
+
dismissed = true;
|
|
1567
|
+
el.setAttribute("data-dismissing", "");
|
|
1568
|
+
};
|
|
1569
|
+
return { dismiss };
|
|
1570
|
+
}
|
|
1571
|
+
function popover(options) {
|
|
1572
|
+
const el = ensurePopover();
|
|
1573
|
+
cleanupPopoverAnchorObserver();
|
|
1574
|
+
try {
|
|
1575
|
+
el.hidePopover();
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
el.removeAttribute("data-dismissing");
|
|
1579
|
+
el.innerHTML = "";
|
|
1580
|
+
options.render(el);
|
|
1581
|
+
if (supportsAnchor) {
|
|
1582
|
+
const anchorName = "--overlay-anchor";
|
|
1583
|
+
options.anchor.style.setProperty("anchor-name", anchorName);
|
|
1584
|
+
el.style.setProperty("position-anchor", anchorName);
|
|
1585
|
+
el.style.removeProperty("top");
|
|
1586
|
+
el.style.removeProperty("left");
|
|
1587
|
+
} else {
|
|
1588
|
+
const rect = options.anchor.getBoundingClientRect();
|
|
1589
|
+
el.style.top = `${rect.bottom + globalThis.scrollY}px`;
|
|
1590
|
+
el.style.left = `${rect.left + globalThis.scrollX}px`;
|
|
1591
|
+
el.style.position = "absolute";
|
|
1592
|
+
}
|
|
1593
|
+
el.showPopover();
|
|
1594
|
+
watchAnchorDisconnect(options.anchor);
|
|
1595
|
+
}
|
|
1596
|
+
function watchAnchorDisconnect(anchor) {
|
|
1597
|
+
cleanupPopoverAnchorObserver();
|
|
1598
|
+
const parent = anchor.parentNode;
|
|
1599
|
+
if (!parent) {
|
|
1600
|
+
closePopover();
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
popoverAnchorObserver = new MutationObserver(() => {
|
|
1604
|
+
if (!document.contains(anchor)) {
|
|
1605
|
+
closePopover();
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
popoverAnchorObserver.observe(parent, { childList: true });
|
|
1609
|
+
}
|
|
1610
|
+
function hidePopoverImmediate() {
|
|
1611
|
+
cleanupPopoverAnchorObserver();
|
|
1612
|
+
if (!popoverEl)
|
|
1613
|
+
return;
|
|
1614
|
+
try {
|
|
1615
|
+
popoverEl.hidePopover();
|
|
1616
|
+
} catch {
|
|
1617
|
+
}
|
|
1618
|
+
popoverEl.removeAttribute("data-dismissing");
|
|
1619
|
+
}
|
|
1620
|
+
function cleanupPopoverAnchorObserver() {
|
|
1621
|
+
if (popoverAnchorObserver) {
|
|
1622
|
+
popoverAnchorObserver.disconnect();
|
|
1623
|
+
popoverAnchorObserver = null;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function closePopover() {
|
|
1627
|
+
cleanupPopoverAnchorObserver();
|
|
1628
|
+
if (!popoverEl)
|
|
1629
|
+
return;
|
|
1630
|
+
let isOpen;
|
|
1631
|
+
try {
|
|
1632
|
+
isOpen = popoverEl.matches(":popover-open");
|
|
1633
|
+
} catch {
|
|
1634
|
+
isOpen = popoverEl.hasAttribute("popover") && popoverEl.style.display !== "none";
|
|
1635
|
+
}
|
|
1636
|
+
if (!isOpen)
|
|
1637
|
+
return;
|
|
1638
|
+
animateDismiss(popoverEl, () => {
|
|
1639
|
+
try {
|
|
1640
|
+
popoverEl.hidePopover();
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
function dismissAll() {
|
|
1646
|
+
if (dialog && dialog.open) {
|
|
1647
|
+
const resolve = modalResolve;
|
|
1648
|
+
const onClose = modalOnClose;
|
|
1649
|
+
modalResolve = null;
|
|
1650
|
+
modalOnClose = void 0;
|
|
1651
|
+
dialog.removeAttribute("data-dismissing");
|
|
1652
|
+
dialog.close();
|
|
1653
|
+
if (resolve)
|
|
1654
|
+
resolve(void 0);
|
|
1655
|
+
if (onClose)
|
|
1656
|
+
onClose();
|
|
1657
|
+
}
|
|
1658
|
+
hidePopoverImmediate();
|
|
1659
|
+
if (toastContainer) {
|
|
1660
|
+
for (const child of toastContainer.children) {
|
|
1661
|
+
child.setAttribute("data-dismissing", "");
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
for (const el of document.querySelectorAll(":popover-open")) {
|
|
1666
|
+
el.hidePopover();
|
|
1667
|
+
}
|
|
1668
|
+
} catch {
|
|
1669
|
+
}
|
|
1670
|
+
for (const el of document.querySelectorAll("dialog[open]")) {
|
|
1671
|
+
if (el !== dialog)
|
|
1672
|
+
el.close();
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
modal,
|
|
1677
|
+
closeModal,
|
|
1678
|
+
toast,
|
|
1679
|
+
popover,
|
|
1680
|
+
closePopover,
|
|
1681
|
+
dismissAll
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// dist/src/widget/page-title.widget.js
|
|
1686
|
+
var PageTitleWidget = class extends WidgetComponent {
|
|
1687
|
+
name = "page-title";
|
|
1688
|
+
getData(args) {
|
|
1689
|
+
return Promise.resolve({ title: args.params.title });
|
|
1690
|
+
}
|
|
1691
|
+
renderHTML(args) {
|
|
1692
|
+
const title = args.data?.title ?? args.params.title;
|
|
1693
|
+
if (title && typeof document !== "undefined") {
|
|
1694
|
+
document.title = title;
|
|
1695
|
+
}
|
|
1696
|
+
return "";
|
|
1697
|
+
}
|
|
1698
|
+
renderMarkdown(_args) {
|
|
1699
|
+
return "";
|
|
1700
|
+
}
|
|
1701
|
+
validateParams(params) {
|
|
1702
|
+
if (!params.title || typeof params.title !== "string") {
|
|
1703
|
+
return 'page-title widget requires a "title" string param';
|
|
1704
|
+
}
|
|
1705
|
+
return void 0;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// dist/src/widget/breadcrumb.widget.js
|
|
1710
|
+
var DEFAULT_HTML_SEPARATOR = " \u203A ";
|
|
1711
|
+
var DEFAULT_MD_SEPARATOR = " > ";
|
|
1712
|
+
var BreadcrumbWidget = class extends WidgetComponent {
|
|
1713
|
+
name = "breadcrumb";
|
|
1714
|
+
getData(args) {
|
|
1715
|
+
const pathname = args.context.pathname || "/";
|
|
1716
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
1717
|
+
const segments = [
|
|
1718
|
+
{ label: "Home", href: "/" }
|
|
1719
|
+
];
|
|
1720
|
+
let accumulated = "";
|
|
1721
|
+
for (const part of parts) {
|
|
1722
|
+
accumulated += "/" + part;
|
|
1723
|
+
segments.push({
|
|
1724
|
+
label: part.charAt(0).toUpperCase() + part.slice(1).replace(/-/g, " "),
|
|
1725
|
+
href: accumulated
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
return Promise.resolve({ segments });
|
|
1729
|
+
}
|
|
1730
|
+
renderHTML(args) {
|
|
1731
|
+
if (!args.data || args.data.segments.length === 0)
|
|
1732
|
+
return "";
|
|
1733
|
+
const sep = args.params.separator ?? DEFAULT_HTML_SEPARATOR;
|
|
1734
|
+
const segments = args.data.segments;
|
|
1735
|
+
const items = segments.map((seg, i) => {
|
|
1736
|
+
const escaped = escapeHtml(seg.label);
|
|
1737
|
+
if (i === segments.length - 1) {
|
|
1738
|
+
return `<span aria-current="page">${escaped}</span>`;
|
|
1739
|
+
}
|
|
1740
|
+
return `<a href="${escapeHtml(seg.href)}">${escaped}</a>`;
|
|
1741
|
+
});
|
|
1742
|
+
return `<nav aria-label="Breadcrumb">${items.join(escapeHtml(sep))}</nav>`;
|
|
1743
|
+
}
|
|
1744
|
+
renderMarkdown(args) {
|
|
1745
|
+
if (!args.data || args.data.segments.length === 0)
|
|
1746
|
+
return "";
|
|
1747
|
+
const sep = args.params.separator ?? DEFAULT_MD_SEPARATOR;
|
|
1748
|
+
return args.data.segments.map((seg, i, arr) => i === arr.length - 1 ? `**${seg.label}**` : `[${seg.label}](${seg.href})`).join(sep);
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// dist/src/renderer/spa/mod.js
|
|
1753
|
+
if (globalThis.customElements) {
|
|
1754
|
+
if (!customElements.get("router-slot"))
|
|
1755
|
+
customElements.define("router-slot", RouterSlot);
|
|
1756
|
+
if (!customElements.get("mark-down"))
|
|
1757
|
+
customElements.define("mark-down", MarkdownElement);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// dist/src/util/widget-resolve.util.js
|
|
1761
|
+
var MAX_WIDGET_DEPTH = 10;
|
|
1762
|
+
async function resolveRecursively(content, parse, resolve, replace, depth = 0) {
|
|
1763
|
+
if (depth >= MAX_WIDGET_DEPTH) {
|
|
1764
|
+
logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). Possible circular dependency or excessive nesting.`);
|
|
1765
|
+
return content;
|
|
1766
|
+
}
|
|
1767
|
+
const widgets = parse(content);
|
|
1768
|
+
if (widgets.length === 0)
|
|
1769
|
+
return content;
|
|
1770
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1771
|
+
await Promise.all(widgets.map(async (widget) => {
|
|
1772
|
+
let rendered = await resolve(widget);
|
|
1773
|
+
rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1);
|
|
1774
|
+
replacements.set(widget, rendered);
|
|
1775
|
+
}));
|
|
1776
|
+
return replace(content, replacements);
|
|
1777
|
+
}
|
|
1778
|
+
function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider) {
|
|
1779
|
+
const tagPattern = /<widget-(?<name>[a-z][a-z0-9-]*)(?<attrs>\s[^>]*)?>(?<content>.*?)<\/widget-\k<name>>/gis;
|
|
1780
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
1781
|
+
const ssrAttrPattern = new RegExp(`\\s${SSR_ATTR}(?:\\s|=|$)`);
|
|
1782
|
+
const parse = (content) => {
|
|
1783
|
+
const matches = content.matchAll(tagPattern).toArray();
|
|
1784
|
+
return matches.filter((match) => {
|
|
1785
|
+
const attrsString = match.groups.attrs || "";
|
|
1786
|
+
return !ssrAttrPattern.test(attrsString);
|
|
1787
|
+
});
|
|
1788
|
+
};
|
|
1789
|
+
const resolve = async (match) => {
|
|
1790
|
+
const widgetName = match.groups.name;
|
|
1791
|
+
const attrsString = match.groups.attrs?.trim() ?? "";
|
|
1792
|
+
const widget = registry.get(widgetName);
|
|
1793
|
+
if (!widget)
|
|
1794
|
+
return match[0];
|
|
1795
|
+
const params = parseAttrsToParams(attrsString);
|
|
1796
|
+
try {
|
|
1797
|
+
let files;
|
|
1798
|
+
if (loadFiles) {
|
|
1799
|
+
files = await loadFiles(widgetName, widget.files);
|
|
1800
|
+
}
|
|
1801
|
+
const baseContext = {
|
|
1802
|
+
...routeInfo,
|
|
1803
|
+
pathname: routeInfo.url.pathname,
|
|
1804
|
+
searchParams: routeInfo.url.searchParams,
|
|
1805
|
+
files
|
|
1806
|
+
};
|
|
1807
|
+
const context = contextProvider ? contextProvider(baseContext) : baseContext;
|
|
1808
|
+
const data = await widget.getData({ params, context });
|
|
1809
|
+
const rendered = widget.renderHTML({ data, params, context });
|
|
1810
|
+
wrappers.set(match, {
|
|
1811
|
+
tagName: `widget-${widgetName}`,
|
|
1812
|
+
attrs: attrsString ? ` ${attrsString}` : "",
|
|
1813
|
+
ssrData: widget.exposeSsrData ? escapeAttr(JSON.stringify(data)) : ""
|
|
1814
|
+
});
|
|
1815
|
+
return rendered;
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e : void 0);
|
|
1818
|
+
return match[0];
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
const replace = (content, replacements) => {
|
|
1822
|
+
let result = content;
|
|
1823
|
+
const entries = [...replacements.entries()].sort((a, b) => b[0].index - a[0].index);
|
|
1824
|
+
for (const [match, innerHtml] of entries) {
|
|
1825
|
+
const start = match.index;
|
|
1826
|
+
const end = start + match[0].length;
|
|
1827
|
+
const wrap = wrappers.get(match);
|
|
1828
|
+
const lightDomData = wrap?.ssrData ? wrap.ssrData : "";
|
|
1829
|
+
const replacement = wrap ? `<${wrap.tagName}${wrap.attrs} ${SSR_ATTR}><template shadowrootmode="open">${innerHtml}</template>${lightDomData}</${wrap.tagName}>` : innerHtml;
|
|
1830
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
1831
|
+
}
|
|
1832
|
+
return result;
|
|
1833
|
+
};
|
|
1834
|
+
return resolveRecursively(html, parse, resolve, replace);
|
|
1835
|
+
}
|
|
1836
|
+
function parseAttrsToParams(attrsString) {
|
|
1837
|
+
const params = {};
|
|
1838
|
+
if (!attrsString)
|
|
1839
|
+
return params;
|
|
1840
|
+
const attrPattern = /(?<attr>[a-z][a-z0-9-]*)(?:="(?<dq>[^"]*)"|='(?<sq>[^']*)'|=(?<uq>[^\s>]+))?/gi;
|
|
1841
|
+
for (const match of attrsString.matchAll(attrPattern)) {
|
|
1842
|
+
const { attr: attrName, dq, sq, uq } = match.groups;
|
|
1843
|
+
if (attrName === SSR_ATTR || attrName === LAZY_ATTR)
|
|
1844
|
+
continue;
|
|
1845
|
+
const key = attrName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1846
|
+
const rawValue = dq ?? sq ?? uq;
|
|
1847
|
+
if (rawValue === void 0) {
|
|
1848
|
+
params[key] = "";
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const raw = rawValue.replaceAll("&", "&").replaceAll("'", "'").replaceAll(""", '"');
|
|
1852
|
+
try {
|
|
1853
|
+
params[key] = JSON.parse(raw);
|
|
1854
|
+
} catch {
|
|
1855
|
+
params[key] = raw;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
return params;
|
|
1859
|
+
}
|
|
1860
|
+
function escapeAttr(value) {
|
|
1861
|
+
return value.replaceAll("&", "&").replaceAll("'", "'");
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// dist/src/renderer/ssr/ssr.renderer.js
|
|
1865
|
+
var SsrRenderer = class _SsrRenderer {
|
|
1866
|
+
core;
|
|
1867
|
+
widgets;
|
|
1868
|
+
widgetFiles;
|
|
1869
|
+
constructor(resolver, options = {}) {
|
|
1870
|
+
this.core = new RouteCore(resolver, options);
|
|
1871
|
+
this.widgets = options.widgets ?? null;
|
|
1872
|
+
this.widgetFiles = options.widgetFiles ?? {};
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Render a URL to a content string.
|
|
1876
|
+
*/
|
|
1877
|
+
async render(url, signal) {
|
|
1878
|
+
const matched = this.core.match(url);
|
|
1879
|
+
if (!matched) {
|
|
1880
|
+
const statusPage = this.core.getStatusPage(404);
|
|
1881
|
+
if (statusPage) {
|
|
1882
|
+
try {
|
|
1883
|
+
const ri = { url, params: {} };
|
|
1884
|
+
const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
|
|
1885
|
+
return { content: this.stripSlots(result.content), status: 404, title: result.title };
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return { content: this.renderStatusPage(404, url), status: 404 };
|
|
1891
|
+
}
|
|
1892
|
+
if (matched.route.type === "redirect") {
|
|
1893
|
+
const module = await this.core.loadModule(matched.route.modulePath);
|
|
1894
|
+
const redirectConfig = module.default;
|
|
1895
|
+
assertSafeRedirect(redirectConfig.to);
|
|
1896
|
+
return {
|
|
1897
|
+
content: this.renderRedirect(redirectConfig.to),
|
|
1898
|
+
status: redirectConfig.status ?? 301,
|
|
1899
|
+
redirect: redirectConfig.to
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
const routeInfo = this.core.toRouteInfo(matched, url);
|
|
1903
|
+
try {
|
|
1904
|
+
const { content, title } = await this.renderPage(routeInfo, matched, signal);
|
|
1905
|
+
return { content, status: 200, title };
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
if (error instanceof Response) {
|
|
1908
|
+
const statusPage = this.core.getStatusPage(error.status);
|
|
1909
|
+
if (statusPage) {
|
|
1910
|
+
try {
|
|
1911
|
+
const ri = { url, params: {} };
|
|
1912
|
+
const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
|
|
1913
|
+
return {
|
|
1914
|
+
content: this.stripSlots(result.content),
|
|
1915
|
+
status: error.status,
|
|
1916
|
+
title: result.title
|
|
1917
|
+
};
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return { content: this.renderStatusPage(error.status, url), status: error.status };
|
|
1923
|
+
}
|
|
1924
|
+
logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error : void 0);
|
|
1925
|
+
const boundary = this.core.findErrorBoundary(url.pathname);
|
|
1926
|
+
if (boundary) {
|
|
1927
|
+
const result = await this.tryRenderErrorModule(boundary.modulePath, url, "boundary");
|
|
1928
|
+
if (result)
|
|
1929
|
+
return result;
|
|
1930
|
+
}
|
|
1931
|
+
const errorHandler = this.core.getErrorHandler();
|
|
1932
|
+
if (errorHandler) {
|
|
1933
|
+
const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, "handler");
|
|
1934
|
+
if (result)
|
|
1935
|
+
return result;
|
|
1936
|
+
}
|
|
1937
|
+
return { content: this.renderErrorPage(error, url), status: 500 };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Render a matched page by composing the route hierarchy.
|
|
1942
|
+
*/
|
|
1943
|
+
async renderPage(routeInfo, matched, signal) {
|
|
1944
|
+
const hierarchy = this.core.buildRouteHierarchy(matched.route.pattern);
|
|
1945
|
+
const segments = [];
|
|
1946
|
+
for (let i = 0; i < hierarchy.length; i++) {
|
|
1947
|
+
const routePattern = hierarchy[i];
|
|
1948
|
+
let route = this.core.findRoute(routePattern);
|
|
1949
|
+
if (!route && routePattern === "/") {
|
|
1950
|
+
route = DEFAULT_ROOT_ROUTE;
|
|
1951
|
+
}
|
|
1952
|
+
if (!route)
|
|
1953
|
+
continue;
|
|
1954
|
+
if (route === matched.route && routePattern !== matched.route.pattern)
|
|
1955
|
+
continue;
|
|
1956
|
+
segments.push({ route, isLeaf: i === hierarchy.length - 1 });
|
|
1957
|
+
}
|
|
1958
|
+
const results = await Promise.all(segments.map(({ route, isLeaf }) => this.renderRouteContent(routeInfo, route, isLeaf, signal)));
|
|
1959
|
+
let result = "";
|
|
1960
|
+
let pageTitle;
|
|
1961
|
+
let lastRenderedPattern = "";
|
|
1962
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1963
|
+
const { content, title } = results[i];
|
|
1964
|
+
if (title) {
|
|
1965
|
+
pageTitle = title;
|
|
1966
|
+
}
|
|
1967
|
+
if (result === "") {
|
|
1968
|
+
result = content;
|
|
1969
|
+
} else {
|
|
1970
|
+
const injected = this.injectSlot(result, content, lastRenderedPattern);
|
|
1971
|
+
if (injected === result) {
|
|
1972
|
+
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.`);
|
|
1973
|
+
}
|
|
1974
|
+
result = injected;
|
|
1975
|
+
}
|
|
1976
|
+
lastRenderedPattern = segments[i].route.pattern;
|
|
1977
|
+
}
|
|
1978
|
+
result = this.stripSlots(result);
|
|
1979
|
+
return { content: result, title: pageTitle };
|
|
1980
|
+
}
|
|
1981
|
+
/** Load component, build context, get data, render content, get title. */
|
|
1982
|
+
async loadRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1983
|
+
const files = route.files ?? {};
|
|
1984
|
+
const tsModule = files.ts ?? files.js;
|
|
1985
|
+
const component = tsModule ? (await this.core.loadModule(tsModule)).default : page_component_default;
|
|
1986
|
+
const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
|
|
1987
|
+
const data = await component.getData({ params: routeInfo.params, signal, context });
|
|
1988
|
+
const content = this.renderContent(component, { data, params: routeInfo.params, context });
|
|
1989
|
+
const title = component.getTitle({ data, params: routeInfo.params, context });
|
|
1990
|
+
return { content, title };
|
|
1991
|
+
}
|
|
1992
|
+
/** Render a component for error boundary/handler with minimal context. */
|
|
1993
|
+
renderComponent(component, data, context) {
|
|
1994
|
+
return this.renderContent(component, { data, params: {}, context });
|
|
1995
|
+
}
|
|
1996
|
+
static EMPTY_URL = new URL("http://error");
|
|
1997
|
+
/** Try to load and render an error boundary or handler module. Returns null on failure. */
|
|
1998
|
+
async tryRenderErrorModule(modulePath, url, kind) {
|
|
1999
|
+
try {
|
|
2000
|
+
const module = await this.core.loadModule(modulePath);
|
|
2001
|
+
const component = module.default;
|
|
2002
|
+
const minCtx = {
|
|
2003
|
+
url: _SsrRenderer.EMPTY_URL,
|
|
2004
|
+
params: {},
|
|
2005
|
+
pathname: "",
|
|
2006
|
+
searchParams: new URLSearchParams()
|
|
2007
|
+
};
|
|
2008
|
+
const data = await component.getData({ params: {}, context: minCtx });
|
|
2009
|
+
const content = this.renderComponent(component, data, minCtx);
|
|
2010
|
+
return { content, status: 500 };
|
|
2011
|
+
} catch (e) {
|
|
2012
|
+
logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
// dist/src/renderer/ssr/html.renderer.js
|
|
2019
|
+
var SsrHtmlRouter = class extends SsrRenderer {
|
|
2020
|
+
label = "SSR HTML";
|
|
2021
|
+
markdownRenderer;
|
|
2022
|
+
markdownReady = null;
|
|
2023
|
+
constructor(resolver, options = {}) {
|
|
2024
|
+
super(resolver, options);
|
|
2025
|
+
this.markdownRenderer = options.markdownRenderer ?? null;
|
|
2026
|
+
if (this.markdownRenderer?.init) {
|
|
2027
|
+
this.markdownReady = this.markdownRenderer.init();
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
injectSlot(parent, child, parentPattern) {
|
|
2031
|
+
const escaped = parentPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2032
|
+
return parent.replace(new RegExp(`<router-slot\\b[^>]*\\bpattern="${escaped}"[^>]*></router-slot>`), child);
|
|
2033
|
+
}
|
|
2034
|
+
stripSlots(result) {
|
|
2035
|
+
return result.replace(/<router-slot[^>]*><\/router-slot>/g, "");
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Render a single route's content.
|
|
2039
|
+
*/
|
|
2040
|
+
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
2041
|
+
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
2042
|
+
return { content: `<router-slot pattern="${route.pattern}"></router-slot>` };
|
|
2043
|
+
}
|
|
2044
|
+
const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
|
|
2045
|
+
let content = rawContent;
|
|
2046
|
+
content = await this.expandMarkdown(content);
|
|
2047
|
+
content = this.attributeSlots(content, route.pattern);
|
|
2048
|
+
if (this.widgets) {
|
|
2049
|
+
content = await resolveWidgetTags(content, this.widgets, routeInfo, (name, declared) => {
|
|
2050
|
+
const files = this.widgetFiles[name] ?? declared;
|
|
2051
|
+
return files ? this.core.loadWidgetFiles(files) : Promise.resolve({});
|
|
2052
|
+
}, this.core.contextProvider);
|
|
2053
|
+
}
|
|
2054
|
+
return { content, title };
|
|
2055
|
+
}
|
|
2056
|
+
renderContent(component, args) {
|
|
2057
|
+
return component.renderHTML(args);
|
|
2058
|
+
}
|
|
2059
|
+
renderRedirect(to) {
|
|
2060
|
+
return `<meta http-equiv="refresh" content="0;url=${escapeHtml(to)}">`;
|
|
2061
|
+
}
|
|
2062
|
+
renderStatusPage(status, url) {
|
|
2063
|
+
return `
|
|
2064
|
+
<h1>${STATUS_MESSAGES[status] ?? "Error"}</h1>
|
|
2065
|
+
<p>Path: ${escapeHtml(url.pathname)}</p>
|
|
2066
|
+
`;
|
|
2067
|
+
}
|
|
2068
|
+
renderErrorPage(error, url) {
|
|
2069
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2070
|
+
return `
|
|
2071
|
+
<h1>Error</h1>
|
|
2072
|
+
<p>Path: ${escapeHtml(url.pathname)}</p>
|
|
2073
|
+
<p>${escapeHtml(message)}</p>
|
|
2074
|
+
`;
|
|
2075
|
+
}
|
|
2076
|
+
/** Add pattern attribute to bare <router-slot> tags. */
|
|
2077
|
+
attributeSlots(content, routePattern) {
|
|
2078
|
+
return content.replace(/<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g, `<router-slot pattern="${routePattern}"$1></router-slot>`);
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Expand <mark-down> tags by rendering markdown to HTML server-side.
|
|
2082
|
+
* Leaves content unchanged if no markdown renderer is configured.
|
|
2083
|
+
*/
|
|
2084
|
+
async expandMarkdown(content) {
|
|
2085
|
+
if (!this.markdownRenderer)
|
|
2086
|
+
return content;
|
|
2087
|
+
if (!content.includes("<mark-down>"))
|
|
2088
|
+
return content;
|
|
2089
|
+
if (this.markdownReady) {
|
|
2090
|
+
await this.markdownReady;
|
|
2091
|
+
}
|
|
2092
|
+
const renderer = this.markdownRenderer;
|
|
2093
|
+
const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
|
|
2094
|
+
return content.replace(pattern, (_match, escaped) => {
|
|
2095
|
+
const markdown = unescapeHtml(escaped);
|
|
2096
|
+
const rendered = renderer.render(markdown);
|
|
2097
|
+
return rendered;
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
// dist/src/widget/widget.parser.js
|
|
2103
|
+
var WIDGET_PATTERN = /```widget:(?<name>[a-z][a-z0-9-]*)\n(?<params>.*?)```/gs;
|
|
2104
|
+
function parseWidgetBlocks(markdown) {
|
|
2105
|
+
const blocks = [];
|
|
2106
|
+
for (const match of markdown.matchAll(WIDGET_PATTERN)) {
|
|
2107
|
+
const fullMatch = match[0];
|
|
2108
|
+
const { name: widgetName, params: paramsRaw } = match.groups;
|
|
2109
|
+
const paramsJson = paramsRaw.trim();
|
|
2110
|
+
const startIndex = match.index;
|
|
2111
|
+
const block = {
|
|
2112
|
+
fullMatch,
|
|
2113
|
+
widgetName,
|
|
2114
|
+
params: null,
|
|
2115
|
+
startIndex,
|
|
2116
|
+
endIndex: startIndex + fullMatch.length
|
|
2117
|
+
};
|
|
2118
|
+
if (paramsJson) {
|
|
2119
|
+
try {
|
|
2120
|
+
const parsed = JSON.parse(paramsJson);
|
|
2121
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2122
|
+
block.params = parsed;
|
|
2123
|
+
} else {
|
|
2124
|
+
block.parseError = "Params must be a JSON object";
|
|
2125
|
+
}
|
|
2126
|
+
} catch (e) {
|
|
2127
|
+
block.parseError = `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`;
|
|
2128
|
+
}
|
|
2129
|
+
} else {
|
|
2130
|
+
block.params = {};
|
|
2131
|
+
}
|
|
2132
|
+
blocks.push(block);
|
|
2133
|
+
}
|
|
2134
|
+
return blocks;
|
|
2135
|
+
}
|
|
2136
|
+
function replaceWidgetBlocks(markdown, replacements) {
|
|
2137
|
+
const sortedBlocks = [...replacements.entries()].sort(([a], [b]) => b.startIndex - a.startIndex);
|
|
2138
|
+
let result = markdown;
|
|
2139
|
+
for (const [block, replacement] of sortedBlocks) {
|
|
2140
|
+
result = result.slice(0, block.startIndex) + replacement + result.slice(block.endIndex);
|
|
2141
|
+
}
|
|
2142
|
+
return result;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// dist/src/renderer/ssr/md.renderer.js
|
|
2146
|
+
var BARE_SLOT_BLOCK = "```router-slot\n```";
|
|
2147
|
+
function routerSlotBlock(pattern) {
|
|
2148
|
+
return `\`\`\`router-slot
|
|
2149
|
+
{"pattern":"${pattern}"}
|
|
2150
|
+
\`\`\``;
|
|
2151
|
+
}
|
|
2152
|
+
var SsrMdRouter = class extends SsrRenderer {
|
|
2153
|
+
label = "SSR MD";
|
|
2154
|
+
constructor(resolver, options = {}) {
|
|
2155
|
+
super(resolver, options);
|
|
2156
|
+
}
|
|
2157
|
+
injectSlot(parent, child, parentPattern) {
|
|
2158
|
+
return parent.replace(routerSlotBlock(parentPattern), child);
|
|
2159
|
+
}
|
|
2160
|
+
stripSlots(result) {
|
|
2161
|
+
return result.replace(/```router-slot\n(?:\{[^}]*\}\n)?```/g, "").trim();
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Render a single route's content to Markdown.
|
|
2165
|
+
*/
|
|
2166
|
+
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
2167
|
+
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
2168
|
+
return { content: routerSlotBlock(route.pattern) };
|
|
2169
|
+
}
|
|
2170
|
+
const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
|
|
2171
|
+
let content = rawContent;
|
|
2172
|
+
content = content.replaceAll(BARE_SLOT_BLOCK, routerSlotBlock(route.pattern));
|
|
2173
|
+
if (this.widgets) {
|
|
2174
|
+
content = await this.resolveWidgets(content, routeInfo);
|
|
2175
|
+
}
|
|
2176
|
+
return { content, title };
|
|
2177
|
+
}
|
|
2178
|
+
renderContent(component, args) {
|
|
2179
|
+
return component.renderMarkdown(args);
|
|
2180
|
+
}
|
|
2181
|
+
renderRedirect(to) {
|
|
2182
|
+
return `Redirect to: ${to}`;
|
|
2183
|
+
}
|
|
2184
|
+
renderStatusPage(status, url) {
|
|
2185
|
+
return `# ${STATUS_MESSAGES[status] ?? "Error"}
|
|
2186
|
+
|
|
2187
|
+
Path: \`${url.pathname}\``;
|
|
2188
|
+
}
|
|
2189
|
+
renderErrorPage(_error, url) {
|
|
2190
|
+
return `# Internal Server Error
|
|
2191
|
+
|
|
2192
|
+
Path: \`${url.pathname}\``;
|
|
2193
|
+
}
|
|
2194
|
+
/**
|
|
2195
|
+
* Resolve fenced widget blocks in markdown content.
|
|
2196
|
+
* Replaces ```widget:name blocks with rendered markdown output.
|
|
2197
|
+
*/
|
|
2198
|
+
resolveWidgets(content, routeInfo) {
|
|
2199
|
+
return resolveRecursively(content, parseWidgetBlocks, async (block) => {
|
|
2200
|
+
if (block.parseError || !block.params) {
|
|
2201
|
+
return `> **Error** (\`${block.widgetName}\`): ${block.parseError}`;
|
|
2202
|
+
}
|
|
2203
|
+
const widget = this.widgets.get(block.widgetName);
|
|
2204
|
+
if (!widget) {
|
|
2205
|
+
return `> **Error**: Unknown widget \`${block.widgetName}\``;
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
let files;
|
|
2209
|
+
const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
|
|
2210
|
+
if (filePaths) {
|
|
2211
|
+
files = await this.core.loadWidgetFiles(filePaths);
|
|
2212
|
+
}
|
|
2213
|
+
const baseContext = {
|
|
2214
|
+
...routeInfo,
|
|
2215
|
+
pathname: routeInfo.url.pathname,
|
|
2216
|
+
searchParams: routeInfo.url.searchParams,
|
|
2217
|
+
files
|
|
2218
|
+
};
|
|
2219
|
+
const context = this.core.contextProvider ? this.core.contextProvider(baseContext) : baseContext;
|
|
2220
|
+
const data = await widget.getData({ params: block.params, context });
|
|
2221
|
+
return widget.renderMarkdown({ data, params: block.params, context });
|
|
2222
|
+
} catch (e) {
|
|
2223
|
+
return widget.renderMarkdownError(e);
|
|
2224
|
+
}
|
|
2225
|
+
}, replaceWidgetBlocks);
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// dist/src/util/md.util.js
|
|
2230
|
+
function rewriteMdLinks(markdown, base, skipPrefixes) {
|
|
2231
|
+
const prefix = base + "/";
|
|
2232
|
+
const skip = skipPrefixes.map((p) => p.slice(1) + "/").join("|");
|
|
2233
|
+
const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, "g");
|
|
2234
|
+
const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, "g");
|
|
2235
|
+
const lines = markdown.split("\n");
|
|
2236
|
+
let inCodeBlock = false;
|
|
2237
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2238
|
+
if (lines[i].startsWith("```")) {
|
|
2239
|
+
inCodeBlock = !inCodeBlock;
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
if (inCodeBlock)
|
|
2243
|
+
continue;
|
|
2244
|
+
lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
|
|
2245
|
+
lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
|
|
2246
|
+
}
|
|
2247
|
+
return lines.join("\n");
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// dist/src/route/route-tree.util.js
|
|
2251
|
+
function resolveTargetNode(node, name, isRoot) {
|
|
2252
|
+
if (name === "index") {
|
|
2253
|
+
if (isRoot)
|
|
2254
|
+
return node;
|
|
2255
|
+
node.wildcard ??= { param: "rest", child: {} };
|
|
2256
|
+
return node.wildcard.child;
|
|
2257
|
+
}
|
|
2258
|
+
if (name.startsWith("[") && name.endsWith("]")) {
|
|
2259
|
+
const param = name.slice(1, -1);
|
|
2260
|
+
node.dynamic ??= { param, child: {} };
|
|
2261
|
+
return node.dynamic.child;
|
|
2262
|
+
}
|
|
2263
|
+
node.children ??= {};
|
|
2264
|
+
node.children[name] ??= {};
|
|
2265
|
+
return node.children[name];
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// dist/runtime/abstract.runtime.js
|
|
2269
|
+
var DEFAULT_ROUTES_DIR = "/routes";
|
|
2270
|
+
var DEFAULT_WIDGETS_DIR = "/widgets";
|
|
2271
|
+
var ROUTES_MANIFEST_PATH = "/routes.manifest.json";
|
|
2272
|
+
var WIDGETS_MANIFEST_PATH = "/widgets.manifest.json";
|
|
2273
|
+
var Runtime = class {
|
|
2274
|
+
config;
|
|
2275
|
+
constructor(config = {}) {
|
|
2276
|
+
this.config = config;
|
|
2277
|
+
this.config = config;
|
|
2278
|
+
}
|
|
2279
|
+
/** Write. Defaults to PUT; pass `{ method: "DELETE" }` etc. to override. */
|
|
2280
|
+
command(resource, options) {
|
|
2281
|
+
return this.handle(resource, { method: "PUT", ...options });
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Dynamically import a module from this runtime's storage.
|
|
2285
|
+
* Used by the server for SSR imports of `.page.ts` and `.widget.ts` files.
|
|
2286
|
+
*/
|
|
2287
|
+
loadModule(_path) {
|
|
2288
|
+
throw new Error(`loadModule not implemented for ${this.constructor.name}`);
|
|
2289
|
+
}
|
|
2290
|
+
// ── Manifest resolution ─────────────────────────────────────────────
|
|
2291
|
+
routesManifestCache = null;
|
|
2292
|
+
widgetsManifestCache = null;
|
|
2293
|
+
/** Clear cached manifests so the next query triggers a fresh scan. */
|
|
2294
|
+
invalidateManifests() {
|
|
2295
|
+
this.routesManifestCache = null;
|
|
2296
|
+
this.widgetsManifestCache = null;
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* Resolve the routes manifest. Called when the concrete runtime returns
|
|
2300
|
+
* 404 for ROUTES_MANIFEST_PATH. Scans `config.routesDir` (or default).
|
|
2301
|
+
*/
|
|
2302
|
+
async resolveRoutesManifest() {
|
|
2303
|
+
if (this.routesManifestCache)
|
|
2304
|
+
return this.routesManifestCache.clone();
|
|
2305
|
+
const routesDir = this.config.routesDir ?? DEFAULT_ROUTES_DIR;
|
|
2306
|
+
const dirResponse = await this.query(routesDir + "/");
|
|
2307
|
+
if (dirResponse.status === 404) {
|
|
2308
|
+
return new Response("Not Found", { status: 404 });
|
|
2309
|
+
}
|
|
2310
|
+
const tree = await this.scanRoutes(routesDir);
|
|
2311
|
+
this.routesManifestCache = Response.json(tree);
|
|
2312
|
+
return this.routesManifestCache.clone();
|
|
2313
|
+
}
|
|
2314
|
+
/**
|
|
2315
|
+
* Resolve the widgets manifest. Called when the concrete runtime returns
|
|
2316
|
+
* 404 for WIDGETS_MANIFEST_PATH. Scans `config.widgetsDir` (or default).
|
|
2317
|
+
*/
|
|
2318
|
+
async resolveWidgetsManifest() {
|
|
2319
|
+
if (this.widgetsManifestCache)
|
|
2320
|
+
return this.widgetsManifestCache.clone();
|
|
2321
|
+
const widgetsDir = this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR;
|
|
2322
|
+
const dirResponse = await this.query(widgetsDir + "/");
|
|
2323
|
+
if (dirResponse.status === 404) {
|
|
2324
|
+
return new Response("Not Found", { status: 404 });
|
|
2325
|
+
}
|
|
2326
|
+
const entries = await this.scanWidgets(widgetsDir, widgetsDir.replace(/^\//, ""));
|
|
2327
|
+
this.widgetsManifestCache = Response.json(entries);
|
|
2328
|
+
return this.widgetsManifestCache.clone();
|
|
2329
|
+
}
|
|
2330
|
+
// ── Scanning ──────────────────────────────────────────────────────────
|
|
2331
|
+
async *walkDirectory(dir) {
|
|
2332
|
+
const trailingDir = dir.endsWith("/") ? dir : dir + "/";
|
|
2333
|
+
const response = await this.query(trailingDir);
|
|
2334
|
+
const entries = await response.json();
|
|
2335
|
+
for (const entry of entries) {
|
|
2336
|
+
const path = `${trailingDir}${entry}`;
|
|
2337
|
+
if (entry.endsWith("/")) {
|
|
2338
|
+
yield* this.walkDirectory(path);
|
|
2339
|
+
} else {
|
|
2340
|
+
yield path;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Scan a routes directory and build a RouteNode tree.
|
|
2346
|
+
* The filesystem structure maps directly to the tree — no intermediate array.
|
|
2347
|
+
*/
|
|
2348
|
+
async scanRoutes(routesDir) {
|
|
2349
|
+
const root = {};
|
|
2350
|
+
const allFiles = [];
|
|
2351
|
+
for await (const file of this.walkDirectory(routesDir)) {
|
|
2352
|
+
allFiles.push(file);
|
|
2353
|
+
}
|
|
2354
|
+
for (const filePath of allFiles) {
|
|
2355
|
+
const relativePath = filePath.replace(`${routesDir}/`, "");
|
|
2356
|
+
const parts = relativePath.split("/");
|
|
2357
|
+
const filename = parts[parts.length - 1];
|
|
2358
|
+
const dirSegments = parts.slice(0, -1);
|
|
2359
|
+
const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
|
|
2360
|
+
if (!match)
|
|
2361
|
+
continue;
|
|
2362
|
+
const [, name, kind, ext] = match;
|
|
2363
|
+
let node = root;
|
|
2364
|
+
for (const dir of dirSegments) {
|
|
2365
|
+
if (dir.startsWith("[") && dir.endsWith("]")) {
|
|
2366
|
+
const param = dir.slice(1, -1);
|
|
2367
|
+
node.dynamic ??= { param, child: {} };
|
|
2368
|
+
node = node.dynamic.child;
|
|
2369
|
+
} else {
|
|
2370
|
+
node.children ??= {};
|
|
2371
|
+
node.children[dir] ??= {};
|
|
2372
|
+
node = node.children[dir];
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
if (kind === "error") {
|
|
2376
|
+
node.errorBoundary = filePath;
|
|
2377
|
+
continue;
|
|
2378
|
+
}
|
|
2379
|
+
const target = resolveTargetNode(node, name, dirSegments.length === 0);
|
|
2380
|
+
if (kind === "redirect") {
|
|
2381
|
+
target.redirect = filePath;
|
|
2382
|
+
} else {
|
|
2383
|
+
target.files ??= {};
|
|
2384
|
+
target.files[ext] = filePath;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return root;
|
|
2388
|
+
}
|
|
2389
|
+
async scanWidgets(widgetsDir, pathPrefix) {
|
|
2390
|
+
const COMPANION_EXTENSIONS = ["html", "md", "css"];
|
|
2391
|
+
const entries = [];
|
|
2392
|
+
const trailingDir = widgetsDir.endsWith("/") ? widgetsDir : widgetsDir + "/";
|
|
2393
|
+
const response = await this.query(trailingDir);
|
|
2394
|
+
const listing = await response.json();
|
|
2395
|
+
for (const item of listing) {
|
|
2396
|
+
if (!item.endsWith("/"))
|
|
2397
|
+
continue;
|
|
2398
|
+
const name = item.slice(0, -1);
|
|
2399
|
+
let moduleFile = `${name}.widget.ts`;
|
|
2400
|
+
let modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
2401
|
+
if ((await this.query(modulePath)).status === 404) {
|
|
2402
|
+
moduleFile = `${name}.widget.js`;
|
|
2403
|
+
modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
2404
|
+
if ((await this.query(modulePath)).status === 404)
|
|
2405
|
+
continue;
|
|
2406
|
+
}
|
|
2407
|
+
const prefix = pathPrefix ? `${pathPrefix}/` : "";
|
|
2408
|
+
const entry = {
|
|
2409
|
+
name,
|
|
2410
|
+
modulePath: `${prefix}${name}/${moduleFile}`,
|
|
2411
|
+
tagName: `widget-${name}`
|
|
2412
|
+
};
|
|
2413
|
+
const files = {};
|
|
2414
|
+
let hasFiles = false;
|
|
2415
|
+
for (const ext of COMPANION_EXTENSIONS) {
|
|
2416
|
+
const companionFile = `${name}.widget.${ext}`;
|
|
2417
|
+
const companionPath = `${trailingDir}${name}/${companionFile}`;
|
|
2418
|
+
if ((await this.query(companionPath)).status !== 404) {
|
|
2419
|
+
files[ext] = `${prefix}${name}/${companionFile}`;
|
|
2420
|
+
hasFiles = true;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
if (hasFiles)
|
|
2424
|
+
entry.files = files;
|
|
2425
|
+
entries.push(entry);
|
|
2426
|
+
}
|
|
2427
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2428
|
+
return entries;
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
// dist/server/emroute.server.js
|
|
2433
|
+
function createModuleLoaders(tree, runtime) {
|
|
2434
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2435
|
+
function walk(node) {
|
|
2436
|
+
const modulePath = node.files?.ts ?? node.files?.js;
|
|
2437
|
+
if (modulePath)
|
|
2438
|
+
paths.add(modulePath);
|
|
2439
|
+
if (node.redirect)
|
|
2440
|
+
paths.add(node.redirect);
|
|
2441
|
+
if (node.errorBoundary)
|
|
2442
|
+
paths.add(node.errorBoundary);
|
|
2443
|
+
if (node.children) {
|
|
2444
|
+
for (const child of Object.values(node.children))
|
|
2445
|
+
walk(child);
|
|
2446
|
+
}
|
|
2447
|
+
if (node.dynamic)
|
|
2448
|
+
walk(node.dynamic.child);
|
|
2449
|
+
if (node.wildcard)
|
|
2450
|
+
walk(node.wildcard.child);
|
|
2451
|
+
}
|
|
2452
|
+
walk(tree);
|
|
2453
|
+
const loaders = {};
|
|
2454
|
+
for (const path of paths) {
|
|
2455
|
+
loaders[path] = () => runtime.loadModule(path);
|
|
2456
|
+
}
|
|
2457
|
+
return loaders;
|
|
2458
|
+
}
|
|
2459
|
+
function extractWidgetExport(mod) {
|
|
2460
|
+
for (const value of Object.values(mod)) {
|
|
2461
|
+
if (!value)
|
|
2462
|
+
continue;
|
|
2463
|
+
if (typeof value === "object" && "getData" in value) {
|
|
2464
|
+
return value;
|
|
2465
|
+
}
|
|
2466
|
+
if (typeof value === "function" && value.prototype?.getData) {
|
|
2467
|
+
return new value();
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
return null;
|
|
2471
|
+
}
|
|
2472
|
+
async function importWidgets(entries, runtime, manual) {
|
|
2473
|
+
const registry = new WidgetRegistry();
|
|
2474
|
+
const widgetFiles = {};
|
|
2475
|
+
for (const entry of entries) {
|
|
2476
|
+
try {
|
|
2477
|
+
const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
|
|
2478
|
+
const mod = await runtime.loadModule(runtimePath);
|
|
2479
|
+
const instance = extractWidgetExport(mod);
|
|
2480
|
+
if (!instance)
|
|
2481
|
+
continue;
|
|
2482
|
+
registry.add(instance);
|
|
2483
|
+
const inlined = mod.__files;
|
|
2484
|
+
if (inlined && typeof inlined === "object") {
|
|
2485
|
+
widgetFiles[entry.name] = inlined;
|
|
2486
|
+
} else if (entry.files) {
|
|
2487
|
+
widgetFiles[entry.name] = entry.files;
|
|
2488
|
+
}
|
|
2489
|
+
} catch (e) {
|
|
2490
|
+
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
2491
|
+
if (entry.files)
|
|
2492
|
+
widgetFiles[entry.name] = entry.files;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
if (manual) {
|
|
2496
|
+
for (const widget of manual) {
|
|
2497
|
+
registry.add(widget);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return { registry, widgetFiles };
|
|
2501
|
+
}
|
|
2502
|
+
function buildHtmlShell(title, htmlBase) {
|
|
2503
|
+
const baseTag = htmlBase ? `
|
|
2504
|
+
<base href="${escapeHtml(htmlBase)}/">` : "";
|
|
2505
|
+
return `<!DOCTYPE html>
|
|
2506
|
+
<html>
|
|
2507
|
+
<head>${baseTag}
|
|
2508
|
+
<meta charset="utf-8">
|
|
2509
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2510
|
+
<title>${escapeHtml(title)}</title>
|
|
2511
|
+
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
2512
|
+
</head>
|
|
2513
|
+
<body>
|
|
2514
|
+
<router-slot></router-slot>
|
|
2515
|
+
</body>
|
|
2516
|
+
</html>`;
|
|
2517
|
+
}
|
|
2518
|
+
function injectSsrContent(html, content, title, ssrRoute) {
|
|
2519
|
+
const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
|
|
2520
|
+
if (!slotPattern.test(html))
|
|
2521
|
+
return html;
|
|
2522
|
+
const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
|
|
2523
|
+
html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
|
|
2524
|
+
if (title) {
|
|
2525
|
+
html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
2526
|
+
}
|
|
2527
|
+
return html;
|
|
2528
|
+
}
|
|
2529
|
+
async function resolveShell(runtime, title, htmlBase) {
|
|
2530
|
+
const response = await runtime.query("/index.html");
|
|
2531
|
+
if (response.status !== 404)
|
|
2532
|
+
return await response.text();
|
|
2533
|
+
return buildHtmlShell(title, htmlBase);
|
|
2534
|
+
}
|
|
2535
|
+
async function createEmrouteServer(config, runtime) {
|
|
2536
|
+
const { spa = "root" } = config;
|
|
2537
|
+
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
2538
|
+
let routeTree;
|
|
2539
|
+
if (config.routeTree) {
|
|
2540
|
+
routeTree = config.routeTree;
|
|
2541
|
+
} else {
|
|
2542
|
+
const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
|
|
2543
|
+
if (manifestResponse.status === 404) {
|
|
2544
|
+
throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. Provide routeTree in config or ensure the runtime produces it.`);
|
|
2545
|
+
}
|
|
2546
|
+
routeTree = await manifestResponse.json();
|
|
2547
|
+
}
|
|
2548
|
+
const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
|
|
2549
|
+
const resolver = new RouteTrie(routeTree);
|
|
2550
|
+
let widgets = config.widgets;
|
|
2551
|
+
let widgetFiles = {};
|
|
2552
|
+
let discoveredWidgetEntries = [];
|
|
2553
|
+
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
2554
|
+
if (widgetsResponse.status !== 404) {
|
|
2555
|
+
discoveredWidgetEntries = await widgetsResponse.json();
|
|
2556
|
+
if (config.widgets) {
|
|
2557
|
+
widgets = config.widgets;
|
|
2558
|
+
for (const entry of discoveredWidgetEntries) {
|
|
2559
|
+
if (entry.files)
|
|
2560
|
+
widgetFiles[entry.name] = entry.files;
|
|
2561
|
+
}
|
|
2562
|
+
} else {
|
|
2563
|
+
const imported = await importWidgets(discoveredWidgetEntries, runtime);
|
|
2564
|
+
widgets = imported.registry;
|
|
2565
|
+
widgetFiles = imported.widgetFiles;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
let ssrHtmlRouter = null;
|
|
2569
|
+
let ssrMdRouter = null;
|
|
2570
|
+
function buildSsrRouters() {
|
|
2571
|
+
if (spa === "only") {
|
|
2572
|
+
ssrHtmlRouter = null;
|
|
2573
|
+
ssrMdRouter = null;
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
ssrHtmlRouter = new SsrHtmlRouter(resolver, {
|
|
2577
|
+
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
2578
|
+
moduleLoaders,
|
|
2579
|
+
markdownRenderer: config.markdownRenderer,
|
|
2580
|
+
extendContext: config.extendContext,
|
|
2581
|
+
widgets,
|
|
2582
|
+
widgetFiles
|
|
2583
|
+
});
|
|
2584
|
+
ssrMdRouter = new SsrMdRouter(resolver, {
|
|
2585
|
+
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
2586
|
+
moduleLoaders,
|
|
2587
|
+
extendContext: config.extendContext,
|
|
2588
|
+
widgets,
|
|
2589
|
+
widgetFiles
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
buildSsrRouters();
|
|
2593
|
+
const title = config.title ?? "emroute";
|
|
2594
|
+
let shell = await resolveShell(runtime, title, htmlBase);
|
|
2595
|
+
if ((await runtime.query("/main.css")).status !== 404) {
|
|
2596
|
+
shell = shell.replace("</head>", ' <link rel="stylesheet" href="/main.css">\n</head>');
|
|
2597
|
+
}
|
|
2598
|
+
async function handleRequest(req) {
|
|
2599
|
+
const url = new URL(req.url);
|
|
2600
|
+
const pathname = url.pathname;
|
|
2601
|
+
const mdPrefix = mdBase + "/";
|
|
2602
|
+
const htmlPrefix = htmlBase + "/";
|
|
2603
|
+
const appPrefix = appBase + "/";
|
|
2604
|
+
if (ssrMdRouter && (pathname.startsWith(mdPrefix) || pathname === mdBase)) {
|
|
2605
|
+
const routePath = pathname === mdBase ? "/" : pathname.slice(mdBase.length);
|
|
2606
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2607
|
+
const canonical = mdBase + routePath.slice(0, -1) + (url.search || "");
|
|
2608
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
2609
|
+
}
|
|
2610
|
+
try {
|
|
2611
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2612
|
+
const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
|
|
2613
|
+
if (redirect) {
|
|
2614
|
+
const target = redirect.startsWith("/") ? mdBase + redirect : redirect;
|
|
2615
|
+
return Response.redirect(new URL(target, url.origin), status);
|
|
2616
|
+
}
|
|
2617
|
+
return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
|
|
2618
|
+
status,
|
|
2619
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
|
|
2620
|
+
});
|
|
2621
|
+
} catch (e) {
|
|
2622
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2623
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (ssrHtmlRouter && (pathname.startsWith(htmlPrefix) || pathname === htmlBase)) {
|
|
2627
|
+
const routePath = pathname === htmlBase ? "/" : pathname.slice(htmlBase.length);
|
|
2628
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2629
|
+
const canonical = htmlBase + routePath.slice(0, -1) + (url.search || "");
|
|
2630
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
2631
|
+
}
|
|
2632
|
+
try {
|
|
2633
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2634
|
+
const result = await ssrHtmlRouter.render(routeUrl, req.signal);
|
|
2635
|
+
if (result.redirect) {
|
|
2636
|
+
const target = result.redirect.startsWith("/") ? htmlBase + result.redirect : result.redirect;
|
|
2637
|
+
return Response.redirect(new URL(target, url.origin), result.status);
|
|
2638
|
+
}
|
|
2639
|
+
const ssrTitle = result.title ?? title;
|
|
2640
|
+
const html = injectSsrContent(shell, result.content, ssrTitle, pathname);
|
|
2641
|
+
return new Response(html, {
|
|
2642
|
+
status: result.status,
|
|
2643
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2644
|
+
});
|
|
2645
|
+
} catch (e) {
|
|
2646
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2647
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (pathname.startsWith(appPrefix) || pathname === appBase) {
|
|
2651
|
+
return new Response(shell, {
|
|
2652
|
+
status: 200,
|
|
2653
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
if (pathname.startsWith(htmlPrefix) || pathname === htmlBase || pathname.startsWith(mdPrefix) || pathname === mdBase) {
|
|
2657
|
+
return new Response(shell, {
|
|
2658
|
+
status: 200,
|
|
2659
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
const lastSegment = pathname.split("/").pop() ?? "";
|
|
2663
|
+
if (lastSegment.includes(".")) {
|
|
2664
|
+
const fileResponse = await runtime.handle(pathname);
|
|
2665
|
+
if (fileResponse.status === 200)
|
|
2666
|
+
return fileResponse;
|
|
2667
|
+
return null;
|
|
2668
|
+
}
|
|
2669
|
+
const base = spa === "root" || spa === "only" ? appBase : htmlBase;
|
|
2670
|
+
const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
|
|
2671
|
+
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
2672
|
+
}
|
|
2673
|
+
return {
|
|
2674
|
+
handleRequest,
|
|
2675
|
+
get htmlRouter() {
|
|
2676
|
+
return ssrHtmlRouter;
|
|
2677
|
+
},
|
|
2678
|
+
get mdRouter() {
|
|
2679
|
+
return ssrMdRouter;
|
|
2680
|
+
},
|
|
2681
|
+
get routeTree() {
|
|
2682
|
+
return routeTree;
|
|
2683
|
+
},
|
|
2684
|
+
get widgetEntries() {
|
|
2685
|
+
return discoveredWidgetEntries;
|
|
2686
|
+
},
|
|
2687
|
+
get shell() {
|
|
2688
|
+
return shell;
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// dist/runtime/fetch.runtime.js
|
|
2694
|
+
var __rewriteRelativeImportExtension2 = function(path, preserveJsx) {
|
|
2695
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
2696
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
2697
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
return path;
|
|
2701
|
+
};
|
|
2702
|
+
var FetchRuntime = class extends Runtime {
|
|
2703
|
+
origin;
|
|
2704
|
+
/**
|
|
2705
|
+
* @param origin — Server origin, e.g. `'http://localhost:4100'` or `location.origin`.
|
|
2706
|
+
*/
|
|
2707
|
+
constructor(origin, config = {}) {
|
|
2708
|
+
super(config);
|
|
2709
|
+
this.origin = origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
2710
|
+
}
|
|
2711
|
+
handle(resource, init) {
|
|
2712
|
+
const url = this.toUrl(resource);
|
|
2713
|
+
return fetch(url, init);
|
|
2714
|
+
}
|
|
2715
|
+
query(resource, options) {
|
|
2716
|
+
if (options?.as === "text") {
|
|
2717
|
+
return fetch(this.toUrl(resource)).then((r) => r.text());
|
|
2718
|
+
}
|
|
2719
|
+
return this.handle(resource, options);
|
|
2720
|
+
}
|
|
2721
|
+
async loadModule(path) {
|
|
2722
|
+
const url = `${this.origin}${path}`;
|
|
2723
|
+
const response = await fetch(url);
|
|
2724
|
+
const js = await response.text();
|
|
2725
|
+
const blob = new Blob([js], { type: "application/javascript" });
|
|
2726
|
+
return import(__rewriteRelativeImportExtension2(URL.createObjectURL(blob)));
|
|
2727
|
+
}
|
|
2728
|
+
toUrl(resource) {
|
|
2729
|
+
if (typeof resource === "string")
|
|
2730
|
+
return `${this.origin}${resource}`;
|
|
2731
|
+
if (resource instanceof URL)
|
|
2732
|
+
return `${this.origin}${resource.pathname}${resource.search}`;
|
|
2733
|
+
return `${this.origin}${new URL(resource.url).pathname}`;
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
export {
|
|
2737
|
+
BreadcrumbWidget,
|
|
2738
|
+
Component,
|
|
2739
|
+
ComponentElement,
|
|
2740
|
+
DEFAULT_BASE_PATH,
|
|
2741
|
+
EmrouteApp,
|
|
2742
|
+
FetchRuntime,
|
|
2743
|
+
MarkdownElement,
|
|
2744
|
+
PageComponent,
|
|
2745
|
+
PageTitleWidget,
|
|
2746
|
+
RouteTrie,
|
|
2747
|
+
RouterSlot,
|
|
2748
|
+
WidgetComponent,
|
|
2749
|
+
WidgetRegistry,
|
|
2750
|
+
createEmrouteApp,
|
|
2751
|
+
createEmrouteServer,
|
|
2752
|
+
createOverlayService,
|
|
2753
|
+
escapeHtml,
|
|
2754
|
+
scopeWidgetCss,
|
|
2755
|
+
setLogger
|
|
2756
|
+
};
|
|
2757
|
+
//# sourceMappingURL=emroute.js.map
|