@emkodev/emroute 1.6.6-beta.4 → 1.7.0
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 +1802 -1679
- package/dist/emroute.js.map +3 -3
- package/dist/runtime/abstract.runtime.d.ts +5 -0
- package/dist/runtime/abstract.runtime.js +7 -0
- package/dist/runtime/abstract.runtime.js.map +1 -1
- package/dist/runtime/bun/fs/bun-fs.runtime.d.ts +1 -0
- package/dist/runtime/bun/fs/bun-fs.runtime.js +4 -0
- package/dist/runtime/bun/fs/bun-fs.runtime.js.map +1 -1
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.d.ts +1 -0
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js +4 -0
- package/dist/runtime/bun/sqlite/bun-sqlite.runtime.js.map +1 -1
- package/dist/runtime/sitemap.generator.js +3 -3
- package/dist/runtime/sitemap.generator.js.map +1 -1
- package/dist/server/build.util.d.ts +9 -2
- package/dist/server/build.util.js +134 -23
- package/dist/server/build.util.js.map +1 -1
- package/dist/server/codegen.util.d.ts +11 -9
- package/dist/server/codegen.util.js +21 -53
- package/dist/server/codegen.util.js.map +1 -1
- package/dist/src/element/component.element.d.ts +11 -0
- package/dist/src/element/component.element.js +65 -0
- package/dist/src/element/component.element.js.map +1 -1
- package/dist/src/element/markdown.element.d.ts +2 -0
- package/dist/src/element/markdown.element.js +4 -0
- package/dist/src/element/markdown.element.js.map +1 -1
- package/dist/src/renderer/spa/mod.d.ts +1 -1
- package/dist/src/renderer/spa/mod.js +1 -1
- package/dist/src/renderer/spa/mod.js.map +1 -1
- package/dist/src/renderer/spa/thin-client.d.ts +16 -0
- package/dist/src/renderer/spa/thin-client.js +79 -0
- package/dist/src/renderer/spa/thin-client.js.map +1 -1
- package/package.json +1 -1
- package/runtime/abstract.runtime.ts +8 -0
- package/runtime/bun/fs/bun-fs.runtime.ts +4 -0
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +5 -0
- package/runtime/sitemap.generator.ts +3 -3
- package/server/build.util.ts +155 -25
- package/server/codegen.util.ts +19 -66
- package/src/element/component.element.ts +75 -0
- package/src/element/markdown.element.ts +5 -0
- package/src/renderer/spa/mod.ts +1 -1
- package/src/renderer/spa/thin-client.ts +97 -0
package/dist/emroute.js
CHANGED
|
@@ -138,6 +138,10 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
|
138
138
|
_MarkdownElement.renderer = renderer;
|
|
139
139
|
_MarkdownElement.rendererInitPromise = renderer.init ? renderer.init() : null;
|
|
140
140
|
}
|
|
141
|
+
/** Get the current renderer (if set). Used by bootEmrouteApp to pass through to createEmrouteServer. */
|
|
142
|
+
static getConfiguredRenderer() {
|
|
143
|
+
return _MarkdownElement.renderer;
|
|
144
|
+
}
|
|
141
145
|
/**
|
|
142
146
|
* Get the current renderer, waiting for init if needed.
|
|
143
147
|
*/
|
|
@@ -204,6 +208,10 @@ var MarkdownElement = class _MarkdownElement extends HTMLElementBase {
|
|
|
204
208
|
var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
205
209
|
/** Shared file content cache — deduplicates fetches across all widget instances. */
|
|
206
210
|
static fileCache = /* @__PURE__ */ new Map();
|
|
211
|
+
/** Lazy module loaders keyed by tag name — set by registerLazy(). */
|
|
212
|
+
static lazyLoaders = /* @__PURE__ */ new Map();
|
|
213
|
+
/** Cached module promises for lazy-loaded widgets — avoids re-fetching. */
|
|
214
|
+
static lazyModules = /* @__PURE__ */ new Map();
|
|
207
215
|
/** App-level context provider set once during router initialization. */
|
|
208
216
|
static extendContext;
|
|
209
217
|
/** Register (or clear) the context provider that enriches every widget's ComponentContext. */
|
|
@@ -265,6 +273,32 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
265
273
|
};
|
|
266
274
|
customElements.define(tagName, BoundElement);
|
|
267
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Register a widget lazily: define the custom element immediately (so SSR
|
|
278
|
+
* content via Declarative Shadow DOM is adopted), but defer loading the
|
|
279
|
+
* module until connectedCallback fires. Once loaded, the real component
|
|
280
|
+
* replaces the placeholder and hydration proceeds normally.
|
|
281
|
+
*/
|
|
282
|
+
static registerLazy(name, files, loader) {
|
|
283
|
+
const tagName = `widget-${name}`;
|
|
284
|
+
if (!globalThis.customElements || customElements.get(tagName))
|
|
285
|
+
return;
|
|
286
|
+
_ComponentElement.lazyLoaders.set(tagName, loader);
|
|
287
|
+
const placeholder = {
|
|
288
|
+
name,
|
|
289
|
+
getData: () => Promise.resolve(null),
|
|
290
|
+
renderHTML: () => "",
|
|
291
|
+
renderMarkdown: () => "",
|
|
292
|
+
renderError: () => "",
|
|
293
|
+
renderMarkdownError: () => ""
|
|
294
|
+
};
|
|
295
|
+
const BoundElement = class extends _ComponentElement {
|
|
296
|
+
constructor() {
|
|
297
|
+
super(placeholder, files);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
customElements.define(tagName, BoundElement);
|
|
301
|
+
}
|
|
268
302
|
/**
|
|
269
303
|
* Promise that resolves when component is ready (data loaded and rendered).
|
|
270
304
|
* Used by router to wait for async components.
|
|
@@ -277,6 +311,35 @@ var ComponentElement = class _ComponentElement extends HTMLElementBase {
|
|
|
277
311
|
return this.deferred.promise;
|
|
278
312
|
}
|
|
279
313
|
async connectedCallback() {
|
|
314
|
+
const tagName = this.tagName.toLowerCase();
|
|
315
|
+
const lazyLoader = _ComponentElement.lazyLoaders.get(tagName);
|
|
316
|
+
if (lazyLoader) {
|
|
317
|
+
try {
|
|
318
|
+
let modulePromise = _ComponentElement.lazyModules.get(tagName);
|
|
319
|
+
if (!modulePromise) {
|
|
320
|
+
modulePromise = lazyLoader();
|
|
321
|
+
_ComponentElement.lazyModules.set(tagName, modulePromise);
|
|
322
|
+
}
|
|
323
|
+
const mod = await modulePromise;
|
|
324
|
+
for (const exp of Object.values(mod)) {
|
|
325
|
+
if (exp && typeof exp === "object" && "getData" in exp) {
|
|
326
|
+
const WidgetClass = exp.constructor;
|
|
327
|
+
this.component = new WidgetClass();
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
if (typeof exp === "function" && exp.prototype?.getData) {
|
|
331
|
+
this.component = new exp();
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
if (this.hasAttribute(SSR_ATTR)) {
|
|
337
|
+
this.removeAttribute(SSR_ATTR);
|
|
338
|
+
this.signalReady();
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
280
343
|
this.component.element = this;
|
|
281
344
|
this.style.contentVisibility = "auto";
|
|
282
345
|
this.abortController = new AbortController();
|
|
@@ -771,172 +834,323 @@ var RouteCore = class {
|
|
|
771
834
|
}
|
|
772
835
|
};
|
|
773
836
|
|
|
774
|
-
// dist/src/
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
this.appBase = bp.app;
|
|
837
|
+
// dist/src/route/route.trie.js
|
|
838
|
+
function createNode() {
|
|
839
|
+
return { static: /* @__PURE__ */ new Map() };
|
|
840
|
+
}
|
|
841
|
+
function safeDecode(segment) {
|
|
842
|
+
try {
|
|
843
|
+
return decodeURIComponent(segment);
|
|
844
|
+
} catch {
|
|
845
|
+
return segment;
|
|
784
846
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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);
|
|
847
|
+
}
|
|
848
|
+
function splitSegments(pathname) {
|
|
849
|
+
return pathname.substring(1).split("/");
|
|
850
|
+
}
|
|
851
|
+
function convertNode(source, pattern) {
|
|
852
|
+
const node = createNode();
|
|
853
|
+
if (source.files || source.redirect) {
|
|
854
|
+
node.route = source;
|
|
855
|
+
node.pattern = pattern;
|
|
821
856
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
this.abortController = null;
|
|
825
|
-
this.slot = null;
|
|
857
|
+
if (source.errorBoundary) {
|
|
858
|
+
node.errorBoundary = source.errorBoundary;
|
|
826
859
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
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;
|
|
860
|
+
if (source.children) {
|
|
861
|
+
for (const [segment, child] of Object.entries(source.children)) {
|
|
862
|
+
const childPattern = pattern === "/" ? `/${segment}` : `${pattern}/${segment}`;
|
|
863
|
+
node.static.set(segment, convertNode(child, childPattern));
|
|
838
864
|
}
|
|
839
865
|
}
|
|
840
|
-
|
|
841
|
-
|
|
866
|
+
if (source.dynamic) {
|
|
867
|
+
const { param, child } = source.dynamic;
|
|
868
|
+
const childPattern = pattern === "/" ? `/:${param}` : `${pattern}/:${param}`;
|
|
869
|
+
node.dynamic = { param, node: convertNode(child, childPattern) };
|
|
842
870
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
return pathname.slice(this.appBase.length);
|
|
848
|
-
return pathname;
|
|
871
|
+
if (source.wildcard) {
|
|
872
|
+
const { param, child } = source.wildcard;
|
|
873
|
+
const childPattern = pattern === "/" ? `/:${param}*` : `${pattern}/:${param}*`;
|
|
874
|
+
node.wildcard = { param, node: convertNode(child, childPattern) };
|
|
849
875
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
if (
|
|
866
|
-
|
|
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>`);
|
|
876
|
+
return node;
|
|
877
|
+
}
|
|
878
|
+
var RouteTrie = class {
|
|
879
|
+
root;
|
|
880
|
+
constructor(tree) {
|
|
881
|
+
this.root = convertNode(tree, "/");
|
|
882
|
+
}
|
|
883
|
+
match(pathname) {
|
|
884
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
885
|
+
pathname = pathname.slice(0, -1);
|
|
886
|
+
}
|
|
887
|
+
if (!pathname.startsWith("/")) {
|
|
888
|
+
pathname = "/" + pathname;
|
|
889
|
+
}
|
|
890
|
+
if (pathname === "/") {
|
|
891
|
+
if (this.root.route) {
|
|
892
|
+
return { node: this.root.route, pattern: "/", params: {} };
|
|
883
893
|
}
|
|
894
|
+
return void 0;
|
|
884
895
|
}
|
|
896
|
+
const segments = splitSegments(pathname);
|
|
897
|
+
return this.walk(this.root, segments, 0, {});
|
|
885
898
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
899
|
+
findErrorBoundary(pathname) {
|
|
900
|
+
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
901
|
+
pathname = pathname.slice(0, -1);
|
|
902
|
+
}
|
|
903
|
+
if (!pathname.startsWith("/")) {
|
|
904
|
+
pathname = "/" + pathname;
|
|
905
|
+
}
|
|
906
|
+
if (pathname === "/")
|
|
907
|
+
return this.root.errorBoundary;
|
|
908
|
+
const segments = splitSegments(pathname);
|
|
909
|
+
return this.walkForBoundary(this.root, segments, 0, this.root.errorBoundary);
|
|
892
910
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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>`;
|
|
911
|
+
findRoute(pattern) {
|
|
912
|
+
if (pattern === "/") {
|
|
913
|
+
return this.root.route;
|
|
928
914
|
}
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
915
|
+
const segments = splitSegments(pattern);
|
|
916
|
+
let node = this.root;
|
|
917
|
+
for (const segment of segments) {
|
|
918
|
+
let child;
|
|
919
|
+
if (segment.startsWith(":") && segment.endsWith("*")) {
|
|
920
|
+
child = node.wildcard?.node;
|
|
921
|
+
} else if (segment.startsWith(":")) {
|
|
922
|
+
child = node.dynamic?.node;
|
|
923
|
+
} else {
|
|
924
|
+
child = node.static.get(segment);
|
|
925
|
+
}
|
|
926
|
+
if (!child)
|
|
927
|
+
return void 0;
|
|
928
|
+
node = child;
|
|
929
|
+
}
|
|
930
|
+
return node.route;
|
|
935
931
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
932
|
+
// ── Private matching ──────────────────────────────────────────────────
|
|
933
|
+
walk(node, segments, index, params) {
|
|
934
|
+
if (index === segments.length) {
|
|
935
|
+
if (node.route) {
|
|
936
|
+
return { node: node.route, pattern: node.pattern, params: { ...params } };
|
|
937
|
+
}
|
|
938
|
+
if (node.wildcard?.node.route) {
|
|
939
|
+
return {
|
|
940
|
+
node: node.wildcard.node.route,
|
|
941
|
+
pattern: node.wildcard.node.pattern,
|
|
942
|
+
params: { ...params, [node.wildcard.param]: "" }
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return void 0;
|
|
946
|
+
}
|
|
947
|
+
const segment = segments[index];
|
|
948
|
+
const staticChild = node.static.get(segment);
|
|
949
|
+
if (staticChild) {
|
|
950
|
+
const result = this.walk(staticChild, segments, index + 1, params);
|
|
951
|
+
if (result)
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
if (node.dynamic) {
|
|
955
|
+
const { param, node: dynamicNode } = node.dynamic;
|
|
956
|
+
params[param] = safeDecode(segment);
|
|
957
|
+
const result = this.walk(dynamicNode, segments, index + 1, params);
|
|
958
|
+
if (result)
|
|
959
|
+
return result;
|
|
960
|
+
delete params[param];
|
|
961
|
+
}
|
|
962
|
+
if (node.wildcard?.node.route) {
|
|
963
|
+
const { param, node: wildcardNode } = node.wildcard;
|
|
964
|
+
let rest = safeDecode(segments[index]);
|
|
965
|
+
for (let i = index + 1; i < segments.length; i++) {
|
|
966
|
+
rest += "/" + safeDecode(segments[i]);
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
node: wildcardNode.route,
|
|
970
|
+
pattern: wildcardNode.pattern,
|
|
971
|
+
params: { ...params, [param]: rest }
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
return void 0;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Walk for error boundary. Follows the same priority as match
|
|
978
|
+
* (static → dynamic → wildcard) without backtracking across branches.
|
|
979
|
+
* Returns the deepest error boundary module path found along the path.
|
|
980
|
+
*/
|
|
981
|
+
walkForBoundary(node, segments, index, deepest) {
|
|
982
|
+
if (index === segments.length) {
|
|
983
|
+
return node.errorBoundary ?? deepest;
|
|
984
|
+
}
|
|
985
|
+
const segment = segments[index];
|
|
986
|
+
const staticChild = node.static.get(segment);
|
|
987
|
+
if (staticChild) {
|
|
988
|
+
return this.walkForBoundary(staticChild, segments, index + 1, staticChild.errorBoundary ?? deepest);
|
|
989
|
+
}
|
|
990
|
+
if (node.dynamic) {
|
|
991
|
+
return this.walkForBoundary(node.dynamic.node, segments, index + 1, node.dynamic.node.errorBoundary ?? deepest);
|
|
992
|
+
}
|
|
993
|
+
if (node.wildcard) {
|
|
994
|
+
return node.wildcard.node.errorBoundary ?? deepest;
|
|
995
|
+
}
|
|
996
|
+
return deepest;
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
// dist/src/type/logger.type.js
|
|
1001
|
+
var noop = () => {
|
|
1002
|
+
};
|
|
1003
|
+
var logger = { error: noop, warn: noop };
|
|
1004
|
+
function setLogger(impl) {
|
|
1005
|
+
logger.error = impl.error.bind(impl);
|
|
1006
|
+
logger.warn = impl.warn.bind(impl);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// dist/src/util/widget-resolve.util.js
|
|
1010
|
+
var MAX_WIDGET_DEPTH = 10;
|
|
1011
|
+
async function resolveRecursively(content, parse, resolve, replace, depth = 0) {
|
|
1012
|
+
if (depth >= MAX_WIDGET_DEPTH) {
|
|
1013
|
+
logger.warn(`Widget nesting depth limit reached (${MAX_WIDGET_DEPTH}). Possible circular dependency or excessive nesting.`);
|
|
1014
|
+
return content;
|
|
1015
|
+
}
|
|
1016
|
+
const widgets = parse(content);
|
|
1017
|
+
if (widgets.length === 0)
|
|
1018
|
+
return content;
|
|
1019
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1020
|
+
await Promise.all(widgets.map(async (widget) => {
|
|
1021
|
+
let rendered = await resolve(widget);
|
|
1022
|
+
rendered = await resolveRecursively(rendered, parse, resolve, replace, depth + 1);
|
|
1023
|
+
replacements.set(widget, rendered);
|
|
1024
|
+
}));
|
|
1025
|
+
return replace(content, replacements);
|
|
1026
|
+
}
|
|
1027
|
+
function resolveWidgetTags(html, registry, routeInfo, loadFiles, contextProvider) {
|
|
1028
|
+
const tagPattern = /<widget-(?<name>[a-z][a-z0-9-]*)(?<attrs>\s[^>]*)?>(?<content>.*?)<\/widget-\k<name>>/gis;
|
|
1029
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
1030
|
+
const ssrAttrPattern = new RegExp(`\\s${SSR_ATTR}(?:\\s|=|$)`);
|
|
1031
|
+
const parse = (content) => {
|
|
1032
|
+
const matches = content.matchAll(tagPattern).toArray();
|
|
1033
|
+
return matches.filter((match) => {
|
|
1034
|
+
const attrsString = match.groups.attrs || "";
|
|
1035
|
+
return !ssrAttrPattern.test(attrsString);
|
|
1036
|
+
});
|
|
1037
|
+
};
|
|
1038
|
+
const resolve = async (match) => {
|
|
1039
|
+
const widgetName = match.groups.name;
|
|
1040
|
+
const attrsString = match.groups.attrs?.trim() ?? "";
|
|
1041
|
+
const widget = registry.get(widgetName);
|
|
1042
|
+
if (!widget)
|
|
1043
|
+
return match[0];
|
|
1044
|
+
const params = parseAttrsToParams(attrsString);
|
|
1045
|
+
try {
|
|
1046
|
+
let files;
|
|
1047
|
+
if (loadFiles) {
|
|
1048
|
+
files = await loadFiles(widgetName, widget.files);
|
|
1049
|
+
}
|
|
1050
|
+
const baseContext = {
|
|
1051
|
+
...routeInfo,
|
|
1052
|
+
pathname: routeInfo.url.pathname,
|
|
1053
|
+
searchParams: routeInfo.url.searchParams,
|
|
1054
|
+
files
|
|
1055
|
+
};
|
|
1056
|
+
const context = contextProvider ? contextProvider(baseContext) : baseContext;
|
|
1057
|
+
const data = await widget.getData({ params, context });
|
|
1058
|
+
const rendered = widget.renderHTML({ data, params, context });
|
|
1059
|
+
wrappers.set(match, {
|
|
1060
|
+
tagName: `widget-${widgetName}`,
|
|
1061
|
+
attrs: attrsString ? ` ${attrsString}` : "",
|
|
1062
|
+
ssrData: widget.exposeSsrData ? escapeAttr(JSON.stringify(data)) : ""
|
|
1063
|
+
});
|
|
1064
|
+
return rendered;
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
logger.error(`[SSR HTML] Widget "${widgetName}" render failed`, e instanceof Error ? e : void 0);
|
|
1067
|
+
return match[0];
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
const replace = (content, replacements) => {
|
|
1071
|
+
let result = content;
|
|
1072
|
+
const entries = [...replacements.entries()].sort((a, b) => b[0].index - a[0].index);
|
|
1073
|
+
for (const [match, innerHtml] of entries) {
|
|
1074
|
+
const start = match.index;
|
|
1075
|
+
const end = start + match[0].length;
|
|
1076
|
+
const wrap = wrappers.get(match);
|
|
1077
|
+
const lightDomData = wrap?.ssrData ? wrap.ssrData : "";
|
|
1078
|
+
const replacement = wrap ? `<${wrap.tagName}${wrap.attrs} ${SSR_ATTR}><template shadowrootmode="open">${innerHtml}</template>${lightDomData}</${wrap.tagName}>` : innerHtml;
|
|
1079
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
1080
|
+
}
|
|
1081
|
+
return result;
|
|
1082
|
+
};
|
|
1083
|
+
return resolveRecursively(html, parse, resolve, replace);
|
|
1084
|
+
}
|
|
1085
|
+
function parseAttrsToParams(attrsString) {
|
|
1086
|
+
const params = {};
|
|
1087
|
+
if (!attrsString)
|
|
1088
|
+
return params;
|
|
1089
|
+
const attrPattern = /(?<attr>[a-z][a-z0-9-]*)(?:="(?<dq>[^"]*)"|='(?<sq>[^']*)'|=(?<uq>[^\s>]+))?/gi;
|
|
1090
|
+
for (const match of attrsString.matchAll(attrPattern)) {
|
|
1091
|
+
const { attr: attrName, dq, sq, uq } = match.groups;
|
|
1092
|
+
if (attrName === SSR_ATTR || attrName === LAZY_ATTR)
|
|
1093
|
+
continue;
|
|
1094
|
+
const key = attrName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1095
|
+
const rawValue = dq ?? sq ?? uq;
|
|
1096
|
+
if (rawValue === void 0) {
|
|
1097
|
+
params[key] = "";
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
const raw = rawValue.replaceAll("&", "&").replaceAll("'", "'").replaceAll(""", '"');
|
|
1101
|
+
try {
|
|
1102
|
+
params[key] = JSON.parse(raw);
|
|
1103
|
+
} catch {
|
|
1104
|
+
params[key] = raw;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return params;
|
|
1108
|
+
}
|
|
1109
|
+
function escapeAttr(value) {
|
|
1110
|
+
return value.replaceAll("&", "&").replaceAll("'", "'");
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// dist/src/component/abstract.component.js
|
|
1114
|
+
var Component = class {
|
|
1115
|
+
/** Host element reference, set by ComponentElement in the browser. */
|
|
1116
|
+
element;
|
|
1117
|
+
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
1118
|
+
files;
|
|
1119
|
+
/**
|
|
1120
|
+
* When true, SSR serializes the getData() result into the element's
|
|
1121
|
+
* light DOM so the client can access it immediately in hydrate()
|
|
1122
|
+
* without re-fetching.
|
|
1123
|
+
*
|
|
1124
|
+
* Default is false — hydrate() receives `data: null`. Most widgets
|
|
1125
|
+
* don't need this because the rendered Shadow DOM already contains
|
|
1126
|
+
* the visual representation of the data.
|
|
1127
|
+
*
|
|
1128
|
+
* If you find yourself parsing the shadow DOM in hydrate() trying to
|
|
1129
|
+
* reconstruct the original data object, set this to true instead.
|
|
1130
|
+
* The server-fetched data will be available as `args.data` in hydrate().
|
|
1131
|
+
*/
|
|
1132
|
+
exposeSsrData;
|
|
1133
|
+
/**
|
|
1134
|
+
* Render as HTML for browser context.
|
|
1135
|
+
*
|
|
1136
|
+
* Default implementation converts renderMarkdown() output to HTML.
|
|
1137
|
+
* Override for custom HTML rendering with rich styling/interactivity.
|
|
1138
|
+
*/
|
|
1139
|
+
renderHTML(args) {
|
|
1140
|
+
if (args.data === null) {
|
|
1141
|
+
return `<div data-component="${this.name}">Loading...</div>`;
|
|
1142
|
+
}
|
|
1143
|
+
const markdown = this.renderMarkdown({
|
|
1144
|
+
data: args.data,
|
|
1145
|
+
params: args.params,
|
|
1146
|
+
context: args.context
|
|
1147
|
+
});
|
|
1148
|
+
return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Render error state.
|
|
1152
|
+
*/
|
|
1153
|
+
renderError(args) {
|
|
940
1154
|
const msg = args.error instanceof Error ? args.error.message : String(args.error);
|
|
941
1155
|
return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
|
|
942
1156
|
}
|
|
@@ -1039,1700 +1253,1608 @@ var PageComponent = class extends Component {
|
|
|
1039
1253
|
};
|
|
1040
1254
|
var page_component_default = new PageComponent();
|
|
1041
1255
|
|
|
1042
|
-
// dist/src/
|
|
1043
|
-
var
|
|
1256
|
+
// dist/src/renderer/ssr/ssr.renderer.js
|
|
1257
|
+
var SsrRenderer = class _SsrRenderer {
|
|
1258
|
+
core;
|
|
1259
|
+
widgets;
|
|
1260
|
+
widgetFiles;
|
|
1261
|
+
constructor(resolver, options = {}) {
|
|
1262
|
+
this.core = new RouteCore(resolver, options);
|
|
1263
|
+
this.widgets = options.widgets ?? null;
|
|
1264
|
+
this.widgetFiles = options.widgetFiles ?? {};
|
|
1265
|
+
}
|
|
1044
1266
|
/**
|
|
1045
|
-
* Render
|
|
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
|
-
* ```
|
|
1267
|
+
* Render a URL to a content string.
|
|
1058
1268
|
*/
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1269
|
+
async render(url, signal) {
|
|
1270
|
+
const matched = this.core.match(url);
|
|
1271
|
+
if (!matched) {
|
|
1272
|
+
const statusPage = this.core.getStatusPage(404);
|
|
1273
|
+
if (statusPage) {
|
|
1274
|
+
try {
|
|
1275
|
+
const ri = { url, params: {} };
|
|
1276
|
+
const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
|
|
1277
|
+
return { content: this.stripSlots(result.content), status: 404, title: result.title };
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
logger.error(`[${this.label}] Failed to render 404 status page for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return { content: this.renderStatusPage(404, url), status: 404 };
|
|
1068
1283
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1284
|
+
if (matched.route.type === "redirect") {
|
|
1285
|
+
const module = await this.core.loadModule(matched.route.modulePath);
|
|
1286
|
+
const redirectConfig = module.default;
|
|
1287
|
+
assertSafeRedirect(redirectConfig.to);
|
|
1288
|
+
return {
|
|
1289
|
+
content: this.renderRedirect(redirectConfig.to),
|
|
1290
|
+
status: redirectConfig.status ?? 301,
|
|
1291
|
+
redirect: redirectConfig.to
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const routeInfo = this.core.toRouteInfo(matched, url);
|
|
1295
|
+
try {
|
|
1296
|
+
const { content, title } = await this.renderPage(routeInfo, matched, signal);
|
|
1297
|
+
return { content, status: 200, title };
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (error instanceof Response) {
|
|
1300
|
+
const statusPage = this.core.getStatusPage(error.status);
|
|
1301
|
+
if (statusPage) {
|
|
1302
|
+
try {
|
|
1303
|
+
const ri = { url, params: {} };
|
|
1304
|
+
const result = await this.renderRouteContent(ri, statusPage, void 0, signal);
|
|
1305
|
+
return {
|
|
1306
|
+
content: this.stripSlots(result.content),
|
|
1307
|
+
status: error.status,
|
|
1308
|
+
title: result.title
|
|
1309
|
+
};
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
logger.error(`[${this.label}] Failed to render ${error.status} status page for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
return { content: this.renderStatusPage(error.status, url), status: error.status };
|
|
1315
|
+
}
|
|
1316
|
+
logger.error(`[${this.label}] Error rendering ${url.pathname}:`, error instanceof Error ? error : void 0);
|
|
1317
|
+
const boundary = this.core.findErrorBoundary(url.pathname);
|
|
1318
|
+
if (boundary) {
|
|
1319
|
+
const result = await this.tryRenderErrorModule(boundary.modulePath, url, "boundary");
|
|
1320
|
+
if (result)
|
|
1321
|
+
return result;
|
|
1322
|
+
}
|
|
1323
|
+
const errorHandler = this.core.getErrorHandler();
|
|
1324
|
+
if (errorHandler) {
|
|
1325
|
+
const result = await this.tryRenderErrorModule(errorHandler.modulePath, url, "handler");
|
|
1326
|
+
if (result)
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
return { content: this.renderErrorPage(error, url), status: 500 };
|
|
1071
1330
|
}
|
|
1072
|
-
return super.renderHTML(args);
|
|
1073
1331
|
}
|
|
1074
1332
|
/**
|
|
1075
|
-
* Render
|
|
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
|
-
* ```
|
|
1333
|
+
* Render a matched page by composing the route hierarchy.
|
|
1087
1334
|
*/
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1335
|
+
async renderPage(routeInfo, matched, signal) {
|
|
1336
|
+
const hierarchy = this.core.buildRouteHierarchy(matched.route.pattern);
|
|
1337
|
+
const segments = [];
|
|
1338
|
+
for (let i = 0; i < hierarchy.length; i++) {
|
|
1339
|
+
const routePattern = hierarchy[i];
|
|
1340
|
+
let route = this.core.findRoute(routePattern);
|
|
1341
|
+
if (!route && routePattern === "/") {
|
|
1342
|
+
route = DEFAULT_ROOT_ROUTE;
|
|
1343
|
+
}
|
|
1344
|
+
if (!route)
|
|
1345
|
+
continue;
|
|
1346
|
+
if (route === matched.route && routePattern !== matched.route.pattern)
|
|
1347
|
+
continue;
|
|
1348
|
+
segments.push({ route, isLeaf: i === hierarchy.length - 1 });
|
|
1092
1349
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1350
|
+
const results = await Promise.all(segments.map(({ route, isLeaf }) => this.renderRouteContent(routeInfo, route, isLeaf, signal)));
|
|
1351
|
+
let result = "";
|
|
1352
|
+
let pageTitle;
|
|
1353
|
+
let lastRenderedPattern = "";
|
|
1354
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1355
|
+
const { content, title } = results[i];
|
|
1356
|
+
if (title) {
|
|
1357
|
+
pageTitle = title;
|
|
1358
|
+
}
|
|
1359
|
+
if (result === "") {
|
|
1360
|
+
result = content;
|
|
1361
|
+
} else {
|
|
1362
|
+
const injected = this.injectSlot(result, content, lastRenderedPattern);
|
|
1363
|
+
if (injected === result) {
|
|
1364
|
+
logger.warn(`[${this.label}] Route "${lastRenderedPattern}" has no <router-slot> for child route "${hierarchy[i]}" to render into. Add <router-slot></router-slot> to the parent template.`);
|
|
1365
|
+
}
|
|
1366
|
+
result = injected;
|
|
1367
|
+
}
|
|
1368
|
+
lastRenderedPattern = segments[i].route.pattern;
|
|
1369
|
+
}
|
|
1370
|
+
result = this.stripSlots(result);
|
|
1371
|
+
return { content: result, title: pageTitle };
|
|
1106
1372
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1373
|
+
/** Load component, build context, get data, render content, get title. */
|
|
1374
|
+
async loadRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1375
|
+
const files = route.files ?? {};
|
|
1376
|
+
const tsModule = files.ts ?? files.js;
|
|
1377
|
+
const component = tsModule ? (await this.core.loadModule(tsModule)).default : page_component_default;
|
|
1378
|
+
const context = await this.core.buildComponentContext(routeInfo, route, signal, isLeaf);
|
|
1379
|
+
const data = await component.getData({ params: routeInfo.params, signal, context });
|
|
1380
|
+
const content = this.renderContent(component, { data, params: routeInfo.params, context });
|
|
1381
|
+
const title = component.getTitle({ data, params: routeInfo.params, context });
|
|
1382
|
+
return { content, title };
|
|
1116
1383
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1384
|
+
/** Render a component for error boundary/handler with minimal context. */
|
|
1385
|
+
renderComponent(component, data, context) {
|
|
1386
|
+
return this.renderContent(component, { data, params: {}, context });
|
|
1119
1387
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1388
|
+
static EMPTY_URL = new URL("http://error");
|
|
1389
|
+
/** Try to load and render an error boundary or handler module. Returns null on failure. */
|
|
1390
|
+
async tryRenderErrorModule(modulePath, url, kind) {
|
|
1391
|
+
try {
|
|
1392
|
+
const module = await this.core.loadModule(modulePath);
|
|
1393
|
+
const component = module.default;
|
|
1394
|
+
const minCtx = {
|
|
1395
|
+
url: _SsrRenderer.EMPTY_URL,
|
|
1396
|
+
params: {},
|
|
1397
|
+
pathname: "",
|
|
1398
|
+
searchParams: new URLSearchParams()
|
|
1399
|
+
};
|
|
1400
|
+
const data = await component.getData({ params: {}, context: minCtx });
|
|
1401
|
+
const content = this.renderComponent(component, data, minCtx);
|
|
1402
|
+
return { content, status: 500 };
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
logger.error(`[${this.label}] Error ${kind} failed for ${url.pathname}`, e instanceof Error ? e : void 0);
|
|
1405
|
+
return null;
|
|
1124
1406
|
}
|
|
1125
1407
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// dist/src/renderer/ssr/html.renderer.js
|
|
1411
|
+
var SsrHtmlRouter = class extends SsrRenderer {
|
|
1412
|
+
label = "SSR HTML";
|
|
1413
|
+
markdownRenderer;
|
|
1414
|
+
markdownReady = null;
|
|
1415
|
+
constructor(resolver, options = {}) {
|
|
1416
|
+
super(resolver, options);
|
|
1417
|
+
this.markdownRenderer = options.markdownRenderer ?? null;
|
|
1418
|
+
if (this.markdownRenderer?.init) {
|
|
1419
|
+
this.markdownReady = this.markdownRenderer.init();
|
|
1420
|
+
}
|
|
1130
1421
|
}
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1134
|
-
node.wildcard = { param, node: convertNode(child, childPattern) };
|
|
1422
|
+
injectSlot(parent, child, parentPattern) {
|
|
1423
|
+
const escaped = parentPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1424
|
+
return parent.replace(new RegExp(`<router-slot\\b[^>]*\\bpattern="${escaped}"[^>]*></router-slot>`), child);
|
|
1135
1425
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
var RouteTrie = class {
|
|
1139
|
-
root;
|
|
1140
|
-
constructor(tree) {
|
|
1141
|
-
this.root = convertNode(tree, "/");
|
|
1426
|
+
stripSlots(result) {
|
|
1427
|
+
return result.replace(/<router-slot[^>]*><\/router-slot>/g, "");
|
|
1142
1428
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
if (
|
|
1148
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Render a single route's content.
|
|
1431
|
+
*/
|
|
1432
|
+
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1433
|
+
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
1434
|
+
return { content: `<router-slot pattern="${route.pattern}"></router-slot>` };
|
|
1149
1435
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1436
|
+
const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
|
|
1437
|
+
let content = rawContent;
|
|
1438
|
+
content = await this.expandMarkdown(content);
|
|
1439
|
+
content = this.attributeSlots(content, route.pattern);
|
|
1440
|
+
if (this.widgets) {
|
|
1441
|
+
content = await resolveWidgetTags(content, this.widgets, routeInfo, (name, declared) => {
|
|
1442
|
+
const files = this.widgetFiles[name] ?? declared;
|
|
1443
|
+
return files ? this.core.loadWidgetFiles(files) : Promise.resolve({});
|
|
1444
|
+
}, this.core.contextProvider);
|
|
1155
1445
|
}
|
|
1156
|
-
|
|
1157
|
-
return this.walk(this.root, segments, 0, {});
|
|
1446
|
+
return { content, title };
|
|
1158
1447
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
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);
|
|
1448
|
+
renderContent(component, args) {
|
|
1449
|
+
return component.renderHTML(args);
|
|
1170
1450
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
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;
|
|
1451
|
+
renderRedirect(to) {
|
|
1452
|
+
return `<meta http-equiv="refresh" content="0;url=${escapeHtml(to)}">`;
|
|
1191
1453
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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;
|
|
1454
|
+
renderStatusPage(status, url) {
|
|
1455
|
+
return `
|
|
1456
|
+
<h1>${STATUS_MESSAGES[status] ?? "Error"}</h1>
|
|
1457
|
+
<p>Path: ${escapeHtml(url.pathname)}</p>
|
|
1458
|
+
`;
|
|
1459
|
+
}
|
|
1460
|
+
renderErrorPage(error, url) {
|
|
1461
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1462
|
+
return `
|
|
1463
|
+
<h1>Error</h1>
|
|
1464
|
+
<p>Path: ${escapeHtml(url.pathname)}</p>
|
|
1465
|
+
<p>${escapeHtml(message)}</p>
|
|
1466
|
+
`;
|
|
1467
|
+
}
|
|
1468
|
+
/** Add pattern attribute to bare <router-slot> tags. */
|
|
1469
|
+
attributeSlots(content, routePattern) {
|
|
1470
|
+
return content.replace(/<router-slot(?![^>]*\bpattern=)([^>]*)><\/router-slot>/g, `<router-slot pattern="${routePattern}"$1></router-slot>`);
|
|
1235
1471
|
}
|
|
1236
1472
|
/**
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
* Returns the deepest error boundary module path found along the path.
|
|
1473
|
+
* Expand <mark-down> tags by rendering markdown to HTML server-side.
|
|
1474
|
+
* Leaves content unchanged if no markdown renderer is configured.
|
|
1240
1475
|
*/
|
|
1241
|
-
|
|
1242
|
-
if (
|
|
1243
|
-
return
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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;
|
|
1476
|
+
async expandMarkdown(content) {
|
|
1477
|
+
if (!this.markdownRenderer)
|
|
1478
|
+
return content;
|
|
1479
|
+
if (!content.includes("<mark-down>"))
|
|
1480
|
+
return content;
|
|
1481
|
+
if (this.markdownReady) {
|
|
1482
|
+
await this.markdownReady;
|
|
1255
1483
|
}
|
|
1256
|
-
|
|
1484
|
+
const renderer = this.markdownRenderer;
|
|
1485
|
+
const pattern = /<mark-down>([\s\S]*?)<\/mark-down>/g;
|
|
1486
|
+
return content.replace(pattern, (_match, escaped) => {
|
|
1487
|
+
const markdown = unescapeHtml(escaped);
|
|
1488
|
+
const rendered = renderer.render(markdown);
|
|
1489
|
+
return rendered;
|
|
1490
|
+
});
|
|
1257
1491
|
}
|
|
1258
1492
|
};
|
|
1259
1493
|
|
|
1260
|
-
// dist/src/
|
|
1261
|
-
var
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
overflow: auto;
|
|
1294
|
-
opacity: 1;
|
|
1295
|
-
translate: 0 0;
|
|
1296
|
-
transition:
|
|
1297
|
-
opacity 200ms,
|
|
1298
|
-
translate 200ms;
|
|
1494
|
+
// dist/src/widget/widget.parser.js
|
|
1495
|
+
var WIDGET_PATTERN = /```widget:(?<name>[a-z][a-z0-9-]*)\n(?<params>.*?)```/gs;
|
|
1496
|
+
function parseWidgetBlocks(markdown) {
|
|
1497
|
+
const blocks = [];
|
|
1498
|
+
for (const match of markdown.matchAll(WIDGET_PATTERN)) {
|
|
1499
|
+
const fullMatch = match[0];
|
|
1500
|
+
const { name: widgetName, params: paramsRaw } = match.groups;
|
|
1501
|
+
const paramsJson = paramsRaw.trim();
|
|
1502
|
+
const startIndex = match.index;
|
|
1503
|
+
const block = {
|
|
1504
|
+
fullMatch,
|
|
1505
|
+
widgetName,
|
|
1506
|
+
params: null,
|
|
1507
|
+
startIndex,
|
|
1508
|
+
endIndex: startIndex + fullMatch.length
|
|
1509
|
+
};
|
|
1510
|
+
if (paramsJson) {
|
|
1511
|
+
try {
|
|
1512
|
+
const parsed = JSON.parse(paramsJson);
|
|
1513
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1514
|
+
block.params = parsed;
|
|
1515
|
+
} else {
|
|
1516
|
+
block.parseError = "Params must be a JSON object";
|
|
1517
|
+
}
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
block.parseError = `Invalid JSON: ${e instanceof Error ? e.message : String(e)}`;
|
|
1520
|
+
}
|
|
1521
|
+
} else {
|
|
1522
|
+
block.params = {};
|
|
1523
|
+
}
|
|
1524
|
+
blocks.push(block);
|
|
1525
|
+
}
|
|
1526
|
+
return blocks;
|
|
1299
1527
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
display 200ms allow-discrete,
|
|
1306
|
-
overlay 200ms allow-discrete;
|
|
1307
|
-
|
|
1308
|
-
@starting-style {
|
|
1309
|
-
opacity: 0;
|
|
1310
|
-
translate: 0 20px;
|
|
1528
|
+
function replaceWidgetBlocks(markdown, replacements) {
|
|
1529
|
+
const sortedBlocks = [...replacements.entries()].sort(([a], [b]) => b.startIndex - a.startIndex);
|
|
1530
|
+
let result = markdown;
|
|
1531
|
+
for (const [block, replacement] of sortedBlocks) {
|
|
1532
|
+
result = result.slice(0, block.startIndex) + replacement + result.slice(block.endIndex);
|
|
1311
1533
|
}
|
|
1534
|
+
return result;
|
|
1312
1535
|
}
|
|
1313
1536
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1537
|
+
// dist/src/renderer/ssr/md.renderer.js
|
|
1538
|
+
var BARE_SLOT_BLOCK = "```router-slot\n```";
|
|
1539
|
+
function routerSlotBlock(pattern) {
|
|
1540
|
+
return `\`\`\`router-slot
|
|
1541
|
+
{"pattern":"${pattern}"}
|
|
1542
|
+
\`\`\``;
|
|
1318
1543
|
}
|
|
1544
|
+
var SsrMdRouter = class extends SsrRenderer {
|
|
1545
|
+
label = "SSR MD";
|
|
1546
|
+
constructor(resolver, options = {}) {
|
|
1547
|
+
super(resolver, options);
|
|
1548
|
+
}
|
|
1549
|
+
injectSlot(parent, child, parentPattern) {
|
|
1550
|
+
return parent.replace(routerSlotBlock(parentPattern), child);
|
|
1551
|
+
}
|
|
1552
|
+
stripSlots(result) {
|
|
1553
|
+
return result.replace(/```router-slot\n(?:\{[^}]*\}\n)?```/g, "").trim();
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Render a single route's content to Markdown.
|
|
1557
|
+
*/
|
|
1558
|
+
async renderRouteContent(routeInfo, route, isLeaf, signal) {
|
|
1559
|
+
if (route.modulePath === DEFAULT_ROOT_ROUTE.modulePath) {
|
|
1560
|
+
return { content: routerSlotBlock(route.pattern) };
|
|
1561
|
+
}
|
|
1562
|
+
const { content: rawContent, title } = await this.loadRouteContent(routeInfo, route, isLeaf, signal);
|
|
1563
|
+
let content = rawContent;
|
|
1564
|
+
content = content.replaceAll(BARE_SLOT_BLOCK, routerSlotBlock(route.pattern));
|
|
1565
|
+
if (this.widgets) {
|
|
1566
|
+
content = await this.resolveWidgets(content, routeInfo);
|
|
1567
|
+
}
|
|
1568
|
+
return { content, title };
|
|
1569
|
+
}
|
|
1570
|
+
renderContent(component, args) {
|
|
1571
|
+
return component.renderMarkdown(args);
|
|
1572
|
+
}
|
|
1573
|
+
renderRedirect(to) {
|
|
1574
|
+
return `Redirect to: ${to}`;
|
|
1575
|
+
}
|
|
1576
|
+
renderStatusPage(status, url) {
|
|
1577
|
+
return `# ${STATUS_MESSAGES[status] ?? "Error"}
|
|
1319
1578
|
|
|
1320
|
-
|
|
1321
|
-
transition:
|
|
1322
|
-
opacity 200ms,
|
|
1323
|
-
display 200ms allow-discrete,
|
|
1324
|
-
overlay 200ms allow-discrete;
|
|
1325
|
-
|
|
1326
|
-
@starting-style {
|
|
1327
|
-
opacity: 0;
|
|
1579
|
+
Path: \`${url.pathname}\``;
|
|
1328
1580
|
}
|
|
1329
|
-
|
|
1581
|
+
renderErrorPage(_error, url) {
|
|
1582
|
+
return `# Internal Server Error
|
|
1330
1583
|
|
|
1331
|
-
|
|
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;
|
|
1584
|
+
Path: \`${url.pathname}\``;
|
|
1376
1585
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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;
|
|
1586
|
+
/**
|
|
1587
|
+
* Resolve fenced widget blocks in markdown content.
|
|
1588
|
+
* Replaces ```widget:name blocks with rendered markdown output.
|
|
1589
|
+
*/
|
|
1590
|
+
resolveWidgets(content, routeInfo) {
|
|
1591
|
+
return resolveRecursively(content, parseWidgetBlocks, async (block) => {
|
|
1592
|
+
if (block.parseError || !block.params) {
|
|
1593
|
+
return `> **Error** (\`${block.widgetName}\`): ${block.parseError}`;
|
|
1594
|
+
}
|
|
1595
|
+
const widget = this.widgets.get(block.widgetName);
|
|
1596
|
+
if (!widget) {
|
|
1597
|
+
return `> **Error**: Unknown widget \`${block.widgetName}\``;
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
let files;
|
|
1601
|
+
const filePaths = this.widgetFiles[block.widgetName] ?? widget.files;
|
|
1602
|
+
if (filePaths) {
|
|
1603
|
+
files = await this.core.loadWidgetFiles(filePaths);
|
|
1604
|
+
}
|
|
1605
|
+
const baseContext = {
|
|
1606
|
+
...routeInfo,
|
|
1607
|
+
pathname: routeInfo.url.pathname,
|
|
1608
|
+
searchParams: routeInfo.url.searchParams,
|
|
1609
|
+
files
|
|
1610
|
+
};
|
|
1611
|
+
const context = this.core.contextProvider ? this.core.contextProvider(baseContext) : baseContext;
|
|
1612
|
+
const data = await widget.getData({ params: block.params, context });
|
|
1613
|
+
return widget.renderMarkdown({ data, params: block.params, context });
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
return widget.renderMarkdownError(e);
|
|
1616
|
+
}
|
|
1617
|
+
}, replaceWidgetBlocks);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1422
1620
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1621
|
+
// dist/src/util/md.util.js
|
|
1622
|
+
function rewriteMdLinks(markdown, base, skipPrefixes) {
|
|
1623
|
+
const prefix = base + "/";
|
|
1624
|
+
const skip = skipPrefixes.map((p) => p.slice(1) + "/").join("|");
|
|
1625
|
+
const inlineRe = new RegExp(`\\]\\(\\/(?!${skip})`, "g");
|
|
1626
|
+
const refRe = new RegExp(`^(\\[[^\\]]+\\]:\\s+)\\/(?!${skip})`, "g");
|
|
1627
|
+
const lines = markdown.split("\n");
|
|
1628
|
+
let inCodeBlock = false;
|
|
1629
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1630
|
+
if (lines[i].startsWith("```")) {
|
|
1631
|
+
inCodeBlock = !inCodeBlock;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
if (inCodeBlock)
|
|
1635
|
+
continue;
|
|
1636
|
+
lines[i] = lines[i].replaceAll(inlineRe, `](${prefix}`);
|
|
1637
|
+
lines[i] = lines[i].replaceAll(refRe, `$1${prefix}`);
|
|
1426
1638
|
}
|
|
1639
|
+
return lines.join("\n");
|
|
1427
1640
|
}
|
|
1428
1641
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1642
|
+
// dist/src/route/route-tree.util.js
|
|
1643
|
+
function resolveTargetNode(node, name, isRoot) {
|
|
1644
|
+
if (name === "index") {
|
|
1645
|
+
if (isRoot)
|
|
1646
|
+
return node;
|
|
1647
|
+
node.wildcard ??= { param: "rest", child: {} };
|
|
1648
|
+
return node.wildcard.child;
|
|
1649
|
+
}
|
|
1650
|
+
if (name.startsWith("[") && name.endsWith("]")) {
|
|
1651
|
+
const param = name.slice(1, -1);
|
|
1652
|
+
node.dynamic ??= { param, child: {} };
|
|
1653
|
+
return node.dynamic.child;
|
|
1654
|
+
}
|
|
1655
|
+
node.children ??= {};
|
|
1656
|
+
node.children[name] ??= {};
|
|
1657
|
+
return node.children[name];
|
|
1432
1658
|
}
|
|
1433
|
-
`
|
|
1434
|
-
);
|
|
1435
1659
|
|
|
1436
|
-
// dist/
|
|
1437
|
-
var
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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);
|
|
1660
|
+
// dist/runtime/abstract.runtime.js
|
|
1661
|
+
var DEFAULT_ROUTES_DIR = "/routes";
|
|
1662
|
+
var DEFAULT_WIDGETS_DIR = "/widgets";
|
|
1663
|
+
var ROUTES_MANIFEST_PATH = "/routes.manifest.json";
|
|
1664
|
+
var WIDGETS_MANIFEST_PATH = "/widgets.manifest.json";
|
|
1665
|
+
var Runtime = class {
|
|
1666
|
+
config;
|
|
1667
|
+
constructor(config = {}) {
|
|
1668
|
+
this.config = config;
|
|
1669
|
+
this.config = config;
|
|
1466
1670
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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;
|
|
1671
|
+
/** Write. Defaults to PUT; pass `{ method: "DELETE" }` etc. to override. */
|
|
1672
|
+
command(resource, options) {
|
|
1673
|
+
return this.handle(resource, { method: "PUT", ...options });
|
|
1480
1674
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
document.body.appendChild(toastContainer);
|
|
1488
|
-
return toastContainer;
|
|
1675
|
+
/**
|
|
1676
|
+
* Dynamically import a module from this runtime's storage.
|
|
1677
|
+
* Used by the server for SSR imports of `.page.ts` and `.widget.ts` files.
|
|
1678
|
+
*/
|
|
1679
|
+
loadModule(_path) {
|
|
1680
|
+
throw new Error(`loadModule not implemented for ${this.constructor.name}`);
|
|
1489
1681
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
popoverEl.setAttribute("popover", "");
|
|
1497
|
-
document.body.appendChild(popoverEl);
|
|
1498
|
-
return popoverEl;
|
|
1682
|
+
/**
|
|
1683
|
+
* Transpile TypeScript source to JavaScript.
|
|
1684
|
+
* Used by the build step to produce browser-loadable .js modules.
|
|
1685
|
+
*/
|
|
1686
|
+
transpile(_source) {
|
|
1687
|
+
throw new Error(`transpile not implemented for ${this.constructor.name}`);
|
|
1499
1688
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
}
|
|
1689
|
+
// ── Manifest resolution ─────────────────────────────────────────────
|
|
1690
|
+
routesManifestCache = null;
|
|
1691
|
+
widgetsManifestCache = null;
|
|
1692
|
+
/** Clear cached manifests so the next query triggers a fresh scan. */
|
|
1693
|
+
invalidateManifests() {
|
|
1694
|
+
this.routesManifestCache = null;
|
|
1695
|
+
this.widgetsManifestCache = null;
|
|
1548
1696
|
}
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1697
|
+
/**
|
|
1698
|
+
* Resolve the routes manifest. Called when the concrete runtime returns
|
|
1699
|
+
* 404 for ROUTES_MANIFEST_PATH. Scans `config.routesDir` (or default).
|
|
1700
|
+
*/
|
|
1701
|
+
async resolveRoutesManifest() {
|
|
1702
|
+
if (this.routesManifestCache)
|
|
1703
|
+
return this.routesManifestCache.clone();
|
|
1704
|
+
const routesDir = this.config.routesDir ?? DEFAULT_ROUTES_DIR;
|
|
1705
|
+
const dirResponse = await this.query(routesDir + "/");
|
|
1706
|
+
if (dirResponse.status === 404) {
|
|
1707
|
+
return new Response("Not Found", { status: 404 });
|
|
1559
1708
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
const dismiss = () => {
|
|
1564
|
-
if (dismissed)
|
|
1565
|
-
return;
|
|
1566
|
-
dismissed = true;
|
|
1567
|
-
el.setAttribute("data-dismissing", "");
|
|
1568
|
-
};
|
|
1569
|
-
return { dismiss };
|
|
1709
|
+
const tree = await this.scanRoutes(routesDir);
|
|
1710
|
+
this.routesManifestCache = Response.json(tree);
|
|
1711
|
+
return this.routesManifestCache.clone();
|
|
1570
1712
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
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";
|
|
1713
|
+
/**
|
|
1714
|
+
* Resolve the widgets manifest. Called when the concrete runtime returns
|
|
1715
|
+
* 404 for WIDGETS_MANIFEST_PATH. Scans `config.widgetsDir` (or default).
|
|
1716
|
+
*/
|
|
1717
|
+
async resolveWidgetsManifest() {
|
|
1718
|
+
if (this.widgetsManifestCache)
|
|
1719
|
+
return this.widgetsManifestCache.clone();
|
|
1720
|
+
const widgetsDir = this.config.widgetsDir ?? DEFAULT_WIDGETS_DIR;
|
|
1721
|
+
const dirResponse = await this.query(widgetsDir + "/");
|
|
1722
|
+
if (dirResponse.status === 404) {
|
|
1723
|
+
return new Response("Not Found", { status: 404 });
|
|
1592
1724
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1725
|
+
const entries = await this.scanWidgets(widgetsDir, widgetsDir.replace(/^\//, ""));
|
|
1726
|
+
this.widgetsManifestCache = Response.json(entries);
|
|
1727
|
+
return this.widgetsManifestCache.clone();
|
|
1595
1728
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1729
|
+
// ── Scanning ──────────────────────────────────────────────────────────
|
|
1730
|
+
async *walkDirectory(dir) {
|
|
1731
|
+
const trailingDir = dir.endsWith("/") ? dir : dir + "/";
|
|
1732
|
+
const response = await this.query(trailingDir);
|
|
1733
|
+
const entries = await response.json();
|
|
1734
|
+
for (const entry of entries) {
|
|
1735
|
+
const path = `${trailingDir}${entry}`;
|
|
1736
|
+
if (entry.endsWith("/")) {
|
|
1737
|
+
yield* this.walkDirectory(path);
|
|
1738
|
+
} else {
|
|
1739
|
+
yield path;
|
|
1606
1740
|
}
|
|
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
1741
|
}
|
|
1625
1742
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1743
|
+
/**
|
|
1744
|
+
* Scan a routes directory and build a RouteNode tree.
|
|
1745
|
+
* The filesystem structure maps directly to the tree — no intermediate array.
|
|
1746
|
+
*/
|
|
1747
|
+
async scanRoutes(routesDir) {
|
|
1748
|
+
const root = {};
|
|
1749
|
+
const allFiles = [];
|
|
1750
|
+
for await (const file of this.walkDirectory(routesDir)) {
|
|
1751
|
+
allFiles.push(file);
|
|
1635
1752
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1753
|
+
for (const filePath of allFiles) {
|
|
1754
|
+
const relativePath = filePath.replace(`${routesDir}/`, "");
|
|
1755
|
+
const parts = relativePath.split("/");
|
|
1756
|
+
const filename = parts[parts.length - 1];
|
|
1757
|
+
const dirSegments = parts.slice(0, -1);
|
|
1758
|
+
const match = filename.match(/^(.+?)\.(page|error|redirect)\.(ts|js|html|md|css)$/);
|
|
1759
|
+
if (!match)
|
|
1760
|
+
continue;
|
|
1761
|
+
const [, name, kind, ext] = match;
|
|
1762
|
+
let node = root;
|
|
1763
|
+
for (const dir of dirSegments) {
|
|
1764
|
+
if (dir.startsWith("[") && dir.endsWith("]")) {
|
|
1765
|
+
const param = dir.slice(1, -1);
|
|
1766
|
+
node.dynamic ??= { param, child: {} };
|
|
1767
|
+
node = node.dynamic.child;
|
|
1768
|
+
} else {
|
|
1769
|
+
node.children ??= {};
|
|
1770
|
+
node.children[dir] ??= {};
|
|
1771
|
+
node = node.children[dir];
|
|
1772
|
+
}
|
|
1642
1773
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
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", "");
|
|
1774
|
+
if (kind === "error") {
|
|
1775
|
+
node.errorBoundary = filePath;
|
|
1776
|
+
continue;
|
|
1662
1777
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1778
|
+
const target = resolveTargetNode(node, name, dirSegments.length === 0);
|
|
1779
|
+
if (kind === "redirect") {
|
|
1780
|
+
target.redirect = filePath;
|
|
1781
|
+
} else {
|
|
1782
|
+
target.files ??= {};
|
|
1783
|
+
target.files[ext] = filePath;
|
|
1667
1784
|
}
|
|
1668
|
-
} catch {
|
|
1669
1785
|
}
|
|
1670
|
-
|
|
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 });
|
|
1786
|
+
return root;
|
|
1690
1787
|
}
|
|
1691
|
-
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1788
|
+
async scanWidgets(widgetsDir, pathPrefix) {
|
|
1789
|
+
const COMPANION_EXTENSIONS = ["html", "md", "css"];
|
|
1790
|
+
const entries = [];
|
|
1791
|
+
const trailingDir = widgetsDir.endsWith("/") ? widgetsDir : widgetsDir + "/";
|
|
1792
|
+
const response = await this.query(trailingDir);
|
|
1793
|
+
const listing = await response.json();
|
|
1794
|
+
for (const item of listing) {
|
|
1795
|
+
if (!item.endsWith("/"))
|
|
1796
|
+
continue;
|
|
1797
|
+
const name = item.slice(0, -1);
|
|
1798
|
+
let moduleFile = `${name}.widget.ts`;
|
|
1799
|
+
let modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
1800
|
+
if ((await this.query(modulePath)).status === 404) {
|
|
1801
|
+
moduleFile = `${name}.widget.js`;
|
|
1802
|
+
modulePath = `${trailingDir}${name}/${moduleFile}`;
|
|
1803
|
+
if ((await this.query(modulePath)).status === 404)
|
|
1804
|
+
continue;
|
|
1805
|
+
}
|
|
1806
|
+
const prefix = pathPrefix ? `${pathPrefix}/` : "";
|
|
1807
|
+
const entry = {
|
|
1808
|
+
name,
|
|
1809
|
+
modulePath: `${prefix}${name}/${moduleFile}`,
|
|
1810
|
+
tagName: `widget-${name}`
|
|
1811
|
+
};
|
|
1812
|
+
const files = {};
|
|
1813
|
+
let hasFiles = false;
|
|
1814
|
+
for (const ext of COMPANION_EXTENSIONS) {
|
|
1815
|
+
const companionFile = `${name}.widget.${ext}`;
|
|
1816
|
+
const companionPath = `${trailingDir}${name}/${companionFile}`;
|
|
1817
|
+
if ((await this.query(companionPath)).status !== 404) {
|
|
1818
|
+
files[ext] = `${prefix}${name}/${companionFile}`;
|
|
1819
|
+
hasFiles = true;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (hasFiles)
|
|
1823
|
+
entry.files = files;
|
|
1824
|
+
entries.push(entry);
|
|
1704
1825
|
}
|
|
1705
|
-
|
|
1826
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1827
|
+
return entries;
|
|
1706
1828
|
}
|
|
1707
1829
|
};
|
|
1708
1830
|
|
|
1709
|
-
// dist/
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
segments.push({
|
|
1724
|
-
label: part.charAt(0).toUpperCase() + part.slice(1).replace(/-/g, " "),
|
|
1725
|
-
href: accumulated
|
|
1726
|
-
});
|
|
1831
|
+
// dist/server/emroute.server.js
|
|
1832
|
+
function createModuleLoaders(tree, runtime) {
|
|
1833
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1834
|
+
function walk(node) {
|
|
1835
|
+
const modulePath = node.files?.ts ?? node.files?.js;
|
|
1836
|
+
if (modulePath)
|
|
1837
|
+
paths.add(modulePath);
|
|
1838
|
+
if (node.redirect)
|
|
1839
|
+
paths.add(node.redirect);
|
|
1840
|
+
if (node.errorBoundary)
|
|
1841
|
+
paths.add(node.errorBoundary);
|
|
1842
|
+
if (node.children) {
|
|
1843
|
+
for (const child of Object.values(node.children))
|
|
1844
|
+
walk(child);
|
|
1727
1845
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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>`;
|
|
1846
|
+
if (node.dynamic)
|
|
1847
|
+
walk(node.dynamic.child);
|
|
1848
|
+
if (node.wildcard)
|
|
1849
|
+
walk(node.wildcard.child);
|
|
1743
1850
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
return args.data.segments.map((seg, i, arr) => i === arr.length - 1 ? `**${seg.label}**` : `[${seg.label}](${seg.href})`).join(sep);
|
|
1851
|
+
walk(tree);
|
|
1852
|
+
const loaders = {};
|
|
1853
|
+
for (const path of paths) {
|
|
1854
|
+
loaders[path] = () => runtime.loadModule(path);
|
|
1749
1855
|
}
|
|
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);
|
|
1856
|
+
return loaders;
|
|
1758
1857
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1858
|
+
function extractWidgetExport(mod) {
|
|
1859
|
+
for (const value of Object.values(mod)) {
|
|
1860
|
+
if (!value)
|
|
1861
|
+
continue;
|
|
1862
|
+
if (typeof value === "object" && "getData" in value) {
|
|
1863
|
+
return value;
|
|
1864
|
+
}
|
|
1865
|
+
if (typeof value === "function" && value.prototype?.getData) {
|
|
1866
|
+
return new value();
|
|
1867
|
+
}
|
|
1766
1868
|
}
|
|
1767
|
-
|
|
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);
|
|
1869
|
+
return null;
|
|
1777
1870
|
}
|
|
1778
|
-
function
|
|
1779
|
-
const
|
|
1780
|
-
const
|
|
1781
|
-
const
|
|
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);
|
|
1871
|
+
async function importWidgets(entries, runtime, manual) {
|
|
1872
|
+
const registry = new WidgetRegistry();
|
|
1873
|
+
const widgetFiles = {};
|
|
1874
|
+
for (const entry of entries) {
|
|
1796
1875
|
try {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1876
|
+
const runtimePath = entry.modulePath.startsWith("/") ? entry.modulePath : `/${entry.modulePath}`;
|
|
1877
|
+
const mod = await runtime.loadModule(runtimePath);
|
|
1878
|
+
const instance = extractWidgetExport(mod);
|
|
1879
|
+
if (!instance)
|
|
1880
|
+
continue;
|
|
1881
|
+
registry.add(instance);
|
|
1882
|
+
const inlined = mod.__files;
|
|
1883
|
+
if (inlined && typeof inlined === "object") {
|
|
1884
|
+
widgetFiles[entry.name] = inlined;
|
|
1885
|
+
} else if (entry.files) {
|
|
1886
|
+
widgetFiles[entry.name] = entry.files;
|
|
1800
1887
|
}
|
|
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
1888
|
} catch (e) {
|
|
1817
|
-
|
|
1818
|
-
|
|
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;
|
|
1889
|
+
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
1890
|
+
if (entry.files)
|
|
1891
|
+
widgetFiles[entry.name] = entry.files;
|
|
1850
1892
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
params[key] = raw;
|
|
1893
|
+
}
|
|
1894
|
+
if (manual) {
|
|
1895
|
+
for (const widget of manual) {
|
|
1896
|
+
registry.add(widget);
|
|
1856
1897
|
}
|
|
1857
1898
|
}
|
|
1858
|
-
return
|
|
1859
|
-
}
|
|
1860
|
-
function escapeAttr(value) {
|
|
1861
|
-
return value.replaceAll("&", "&").replaceAll("'", "'");
|
|
1899
|
+
return { registry, widgetFiles };
|
|
1862
1900
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1901
|
+
function buildHtmlShell(title, htmlBase) {
|
|
1902
|
+
const baseTag = htmlBase ? `
|
|
1903
|
+
<base href="${escapeHtml(htmlBase)}/">` : "";
|
|
1904
|
+
return `<!DOCTYPE html>
|
|
1905
|
+
<html>
|
|
1906
|
+
<head>${baseTag}
|
|
1907
|
+
<meta charset="utf-8">
|
|
1908
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1909
|
+
<title>${escapeHtml(title)}</title>
|
|
1910
|
+
<style>@view-transition { navigation: auto; } router-slot { display: contents; }</style>
|
|
1911
|
+
</head>
|
|
1912
|
+
<body>
|
|
1913
|
+
<router-slot></router-slot>
|
|
1914
|
+
</body>
|
|
1915
|
+
</html>`;
|
|
1916
|
+
}
|
|
1917
|
+
function injectSsrContent(html, content, title, ssrRoute) {
|
|
1918
|
+
const slotPattern = /<router-slot\b[^>]*>.*?<\/router-slot>/s;
|
|
1919
|
+
if (!slotPattern.test(html))
|
|
1920
|
+
return html;
|
|
1921
|
+
const ssrAttr = ssrRoute ? ` data-ssr-route="${ssrRoute}"` : "";
|
|
1922
|
+
html = html.replace(slotPattern, `<router-slot${ssrAttr}>${content}</router-slot>`);
|
|
1923
|
+
if (title) {
|
|
1924
|
+
html = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(title)}</title>`);
|
|
1873
1925
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1926
|
+
return html;
|
|
1927
|
+
}
|
|
1928
|
+
async function resolveShell(runtime, title, htmlBase) {
|
|
1929
|
+
const response = await runtime.query("/index.html");
|
|
1930
|
+
if (response.status !== 404)
|
|
1931
|
+
return await response.text();
|
|
1932
|
+
return buildHtmlShell(title, htmlBase);
|
|
1933
|
+
}
|
|
1934
|
+
async function createEmrouteServer(config, runtime) {
|
|
1935
|
+
const { spa = "root" } = config;
|
|
1936
|
+
const { html: htmlBase, md: mdBase, app: appBase } = config.basePath ?? DEFAULT_BASE_PATH;
|
|
1937
|
+
let routeTree;
|
|
1938
|
+
if (config.routeTree) {
|
|
1939
|
+
routeTree = config.routeTree;
|
|
1940
|
+
} else {
|
|
1941
|
+
const manifestResponse = await runtime.query(ROUTES_MANIFEST_PATH);
|
|
1942
|
+
if (manifestResponse.status === 404) {
|
|
1943
|
+
throw new Error(`[emroute] ${ROUTES_MANIFEST_PATH} not found in runtime. Provide routeTree in config or ensure the runtime produces it.`);
|
|
1944
|
+
}
|
|
1945
|
+
routeTree = await manifestResponse.json();
|
|
1946
|
+
}
|
|
1947
|
+
const moduleLoaders = config.moduleLoaders ?? createModuleLoaders(routeTree, runtime);
|
|
1948
|
+
const resolver = new RouteTrie(routeTree);
|
|
1949
|
+
let widgets = config.widgets;
|
|
1950
|
+
let widgetFiles = {};
|
|
1951
|
+
let discoveredWidgetEntries = [];
|
|
1952
|
+
const widgetsResponse = await runtime.query(WIDGETS_MANIFEST_PATH);
|
|
1953
|
+
if (widgetsResponse.status !== 404) {
|
|
1954
|
+
discoveredWidgetEntries = await widgetsResponse.json();
|
|
1955
|
+
if (config.widgets) {
|
|
1956
|
+
widgets = config.widgets;
|
|
1957
|
+
for (const entry of discoveredWidgetEntries) {
|
|
1958
|
+
if (entry.files)
|
|
1959
|
+
widgetFiles[entry.name] = entry.files;
|
|
1889
1960
|
}
|
|
1890
|
-
|
|
1961
|
+
} else {
|
|
1962
|
+
const imported = await importWidgets(discoveredWidgetEntries, runtime);
|
|
1963
|
+
widgets = imported.registry;
|
|
1964
|
+
widgetFiles = imported.widgetFiles;
|
|
1891
1965
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
};
|
|
1966
|
+
}
|
|
1967
|
+
let ssrHtmlRouter = null;
|
|
1968
|
+
let ssrMdRouter = null;
|
|
1969
|
+
function buildSsrRouters() {
|
|
1970
|
+
if (spa === "only") {
|
|
1971
|
+
ssrHtmlRouter = null;
|
|
1972
|
+
ssrMdRouter = null;
|
|
1973
|
+
return;
|
|
1901
1974
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1975
|
+
ssrHtmlRouter = new SsrHtmlRouter(resolver, {
|
|
1976
|
+
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
1977
|
+
moduleLoaders,
|
|
1978
|
+
markdownRenderer: config.markdownRenderer,
|
|
1979
|
+
extendContext: config.extendContext,
|
|
1980
|
+
widgets,
|
|
1981
|
+
widgetFiles
|
|
1982
|
+
});
|
|
1983
|
+
ssrMdRouter = new SsrMdRouter(resolver, {
|
|
1984
|
+
fileReader: (path) => runtime.query(path, { as: "text" }),
|
|
1985
|
+
moduleLoaders,
|
|
1986
|
+
extendContext: config.extendContext,
|
|
1987
|
+
widgets,
|
|
1988
|
+
widgetFiles
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
buildSsrRouters();
|
|
1992
|
+
const title = config.title ?? "emroute";
|
|
1993
|
+
let shell = await resolveShell(runtime, title, htmlBase);
|
|
1994
|
+
if ((await runtime.query("/main.css")).status !== 404) {
|
|
1995
|
+
shell = shell.replace("</head>", ' <link rel="stylesheet" href="/main.css">\n</head>');
|
|
1996
|
+
}
|
|
1997
|
+
async function handleRequest(req) {
|
|
1998
|
+
const url = new URL(req.url);
|
|
1999
|
+
const pathname = url.pathname;
|
|
2000
|
+
const mdPrefix = mdBase + "/";
|
|
2001
|
+
const htmlPrefix = htmlBase + "/";
|
|
2002
|
+
const appPrefix = appBase + "/";
|
|
2003
|
+
if (ssrMdRouter && (pathname.startsWith(mdPrefix) || pathname === mdBase)) {
|
|
2004
|
+
const routePath = pathname === mdBase ? "/" : pathname.slice(mdBase.length);
|
|
2005
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2006
|
+
const canonical = mdBase + routePath.slice(0, -1) + (url.search || "");
|
|
2007
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
2008
|
+
}
|
|
2009
|
+
try {
|
|
2010
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2011
|
+
const { content, status, redirect } = await ssrMdRouter.render(routeUrl, req.signal);
|
|
2012
|
+
if (redirect) {
|
|
2013
|
+
const target = redirect.startsWith("/") ? mdBase + redirect : redirect;
|
|
2014
|
+
return Response.redirect(new URL(target, url.origin), status);
|
|
1921
2015
|
}
|
|
1922
|
-
return
|
|
2016
|
+
return new Response(rewriteMdLinks(content, mdBase, [mdBase, htmlBase]), {
|
|
2017
|
+
status,
|
|
2018
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8; variant=CommonMark" }
|
|
2019
|
+
});
|
|
2020
|
+
} catch (e) {
|
|
2021
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2022
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1923
2023
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2024
|
+
}
|
|
2025
|
+
if (ssrHtmlRouter && (pathname.startsWith(htmlPrefix) || pathname === htmlBase)) {
|
|
2026
|
+
const routePath = pathname === htmlBase ? "/" : pathname.slice(htmlBase.length);
|
|
2027
|
+
if (routePath.length > 1 && routePath.endsWith("/")) {
|
|
2028
|
+
const canonical = htmlBase + routePath.slice(0, -1) + (url.search || "");
|
|
2029
|
+
return Response.redirect(new URL(canonical, url.origin), 301);
|
|
1930
2030
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const result = await
|
|
1934
|
-
if (result)
|
|
1935
|
-
|
|
2031
|
+
try {
|
|
2032
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2033
|
+
const result = await ssrHtmlRouter.render(routeUrl, req.signal);
|
|
2034
|
+
if (result.redirect) {
|
|
2035
|
+
const target = result.redirect.startsWith("/") ? htmlBase + result.redirect : result.redirect;
|
|
2036
|
+
return Response.redirect(new URL(target, url.origin), result.status);
|
|
2037
|
+
}
|
|
2038
|
+
const ssrTitle = result.title ?? title;
|
|
2039
|
+
const html = injectSsrContent(shell, result.content, ssrTitle, pathname);
|
|
2040
|
+
return new Response(html, {
|
|
2041
|
+
status: result.status,
|
|
2042
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2043
|
+
});
|
|
2044
|
+
} catch (e) {
|
|
2045
|
+
console.error(`[emroute] Error rendering ${pathname}:`, e);
|
|
2046
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
1936
2047
|
}
|
|
1937
|
-
return { content: this.renderErrorPage(error, url), status: 500 };
|
|
1938
2048
|
}
|
|
2049
|
+
if (pathname.startsWith(appPrefix) || pathname === appBase) {
|
|
2050
|
+
return new Response(shell, {
|
|
2051
|
+
status: 200,
|
|
2052
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
if (pathname.startsWith(htmlPrefix) || pathname === htmlBase || pathname.startsWith(mdPrefix) || pathname === mdBase) {
|
|
2056
|
+
return new Response(shell, {
|
|
2057
|
+
status: 200,
|
|
2058
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
const lastSegment = pathname.split("/").pop() ?? "";
|
|
2062
|
+
if (lastSegment.includes(".")) {
|
|
2063
|
+
const fileResponse = await runtime.handle(pathname);
|
|
2064
|
+
if (fileResponse.status === 200)
|
|
2065
|
+
return fileResponse;
|
|
2066
|
+
return null;
|
|
2067
|
+
}
|
|
2068
|
+
const base = spa === "root" || spa === "only" ? appBase : htmlBase;
|
|
2069
|
+
const bare = pathname === "/" ? "" : pathname.slice(1).replace(/\/$/, "");
|
|
2070
|
+
return Response.redirect(new URL(`${base}/${bare}`, url.origin), 302);
|
|
2071
|
+
}
|
|
2072
|
+
return {
|
|
2073
|
+
handleRequest,
|
|
2074
|
+
get htmlRouter() {
|
|
2075
|
+
return ssrHtmlRouter;
|
|
2076
|
+
},
|
|
2077
|
+
get mdRouter() {
|
|
2078
|
+
return ssrMdRouter;
|
|
2079
|
+
},
|
|
2080
|
+
get routeTree() {
|
|
2081
|
+
return routeTree;
|
|
2082
|
+
},
|
|
2083
|
+
get widgetEntries() {
|
|
2084
|
+
return discoveredWidgetEntries;
|
|
2085
|
+
},
|
|
2086
|
+
get shell() {
|
|
2087
|
+
return shell;
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// dist/runtime/fetch.runtime.js
|
|
2093
|
+
var __rewriteRelativeImportExtension2 = function(path, preserveJsx) {
|
|
2094
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
2095
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
2096
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
2097
|
+
});
|
|
1939
2098
|
}
|
|
2099
|
+
return path;
|
|
2100
|
+
};
|
|
2101
|
+
var FetchRuntime = class extends Runtime {
|
|
2102
|
+
origin;
|
|
1940
2103
|
/**
|
|
1941
|
-
*
|
|
2104
|
+
* @param origin — Server origin, e.g. `'http://localhost:4100'` or `location.origin`.
|
|
1942
2105
|
*/
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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 };
|
|
2106
|
+
constructor(origin, config = {}) {
|
|
2107
|
+
super(config);
|
|
2108
|
+
this.origin = origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
1991
2109
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
return
|
|
2110
|
+
handle(resource, init) {
|
|
2111
|
+
const url = this.toUrl(resource);
|
|
2112
|
+
return fetch(url, init);
|
|
1995
2113
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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;
|
|
2114
|
+
query(resource, options) {
|
|
2115
|
+
if (options?.as === "text") {
|
|
2116
|
+
return fetch(this.toUrl(resource)).then((r) => r.text());
|
|
2014
2117
|
}
|
|
2118
|
+
return this.handle(resource, options);
|
|
2015
2119
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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
|
-
}
|
|
2120
|
+
async loadModule(path) {
|
|
2121
|
+
const url = `${this.origin}${path}`;
|
|
2122
|
+
const response = await fetch(url);
|
|
2123
|
+
const js = await response.text();
|
|
2124
|
+
const blob = new Blob([js], { type: "application/javascript" });
|
|
2125
|
+
return import(__rewriteRelativeImportExtension2(URL.createObjectURL(blob)));
|
|
2029
2126
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2127
|
+
toUrl(resource) {
|
|
2128
|
+
if (typeof resource === "string")
|
|
2129
|
+
return `${this.origin}${resource}`;
|
|
2130
|
+
if (resource instanceof URL)
|
|
2131
|
+
return `${this.origin}${resource.pathname}${resource.search}`;
|
|
2132
|
+
return `${this.origin}${new URL(resource.url).pathname}`;
|
|
2033
2133
|
}
|
|
2034
|
-
|
|
2035
|
-
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
// dist/src/renderer/spa/thin-client.js
|
|
2137
|
+
var EmrouteApp = class {
|
|
2138
|
+
server;
|
|
2139
|
+
appBase;
|
|
2140
|
+
slot = null;
|
|
2141
|
+
abortController = null;
|
|
2142
|
+
constructor(server, options) {
|
|
2143
|
+
const bp = options?.basePath ?? DEFAULT_BASE_PATH;
|
|
2144
|
+
this.server = server;
|
|
2145
|
+
this.appBase = bp.app;
|
|
2036
2146
|
}
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
return { content: `<router-slot pattern="${route.pattern}"></router-slot>` };
|
|
2147
|
+
async initialize(slotSelector = "router-slot") {
|
|
2148
|
+
this.slot = document.querySelector(slotSelector);
|
|
2149
|
+
if (!this.slot) {
|
|
2150
|
+
console.error("[EmrouteApp] Slot not found:", slotSelector);
|
|
2151
|
+
return;
|
|
2043
2152
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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);
|
|
2153
|
+
if (!("navigation" in globalThis)) {
|
|
2154
|
+
console.warn("[EmrouteApp] Navigation API not available");
|
|
2155
|
+
return;
|
|
2053
2156
|
}
|
|
2054
|
-
|
|
2157
|
+
this.abortController = new AbortController();
|
|
2158
|
+
const { signal } = this.abortController;
|
|
2159
|
+
navigation.addEventListener("navigate", (event) => {
|
|
2160
|
+
if (!event.canIntercept)
|
|
2161
|
+
return;
|
|
2162
|
+
if (event.hashChange)
|
|
2163
|
+
return;
|
|
2164
|
+
if (event.downloadRequest !== null)
|
|
2165
|
+
return;
|
|
2166
|
+
const url = new URL(event.destination.url);
|
|
2167
|
+
if (!this.isAppPath(url.pathname))
|
|
2168
|
+
return;
|
|
2169
|
+
event.intercept({
|
|
2170
|
+
scroll: "manual",
|
|
2171
|
+
handler: async () => {
|
|
2172
|
+
await this.handleNavigation(url, event.signal);
|
|
2173
|
+
event.scroll();
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
}, { signal });
|
|
2177
|
+
const ssrRoute = this.slot.getAttribute("data-ssr-route");
|
|
2178
|
+
if (ssrRoute && (location.pathname === ssrRoute || location.pathname === ssrRoute + "/")) {
|
|
2179
|
+
this.slot.removeAttribute("data-ssr-route");
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
await this.handleNavigation(new URL(location.href), this.abortController.signal);
|
|
2055
2183
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2184
|
+
dispose() {
|
|
2185
|
+
this.abortController?.abort();
|
|
2186
|
+
this.abortController = null;
|
|
2187
|
+
this.slot = null;
|
|
2058
2188
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2189
|
+
async navigate(url, options = {}) {
|
|
2190
|
+
try {
|
|
2191
|
+
const { finished } = navigation.navigate(url, {
|
|
2192
|
+
state: options.state,
|
|
2193
|
+
history: options.replace ? "replace" : "auto"
|
|
2194
|
+
});
|
|
2195
|
+
await finished;
|
|
2196
|
+
} catch (e) {
|
|
2197
|
+
if (e instanceof DOMException && e.name === "AbortError")
|
|
2198
|
+
return;
|
|
2199
|
+
throw e;
|
|
2200
|
+
}
|
|
2061
2201
|
}
|
|
2062
|
-
|
|
2063
|
-
return
|
|
2064
|
-
<h1>${STATUS_MESSAGES[status] ?? "Error"}</h1>
|
|
2065
|
-
<p>Path: ${escapeHtml(url.pathname)}</p>
|
|
2066
|
-
`;
|
|
2202
|
+
isAppPath(pathname) {
|
|
2203
|
+
return pathname === this.appBase || pathname.startsWith(this.appBase + "/");
|
|
2067
2204
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
`;
|
|
2205
|
+
stripAppBase(pathname) {
|
|
2206
|
+
if (pathname === this.appBase)
|
|
2207
|
+
return "/";
|
|
2208
|
+
if (pathname.startsWith(this.appBase + "/"))
|
|
2209
|
+
return pathname.slice(this.appBase.length);
|
|
2210
|
+
return pathname;
|
|
2075
2211
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2212
|
+
async handleNavigation(url, signal) {
|
|
2213
|
+
if (!this.slot || !this.server.htmlRouter)
|
|
2214
|
+
return;
|
|
2215
|
+
const routePath = this.stripAppBase(url.pathname);
|
|
2216
|
+
const routeUrl = new URL(routePath + url.search, url.origin);
|
|
2217
|
+
try {
|
|
2218
|
+
const { content, title, redirect } = await this.server.htmlRouter.render(routeUrl, signal);
|
|
2219
|
+
if (signal.aborted)
|
|
2220
|
+
return;
|
|
2221
|
+
if (redirect) {
|
|
2222
|
+
assertSafeRedirect(redirect);
|
|
2223
|
+
const target = redirect.startsWith("/") ? this.appBase + redirect : redirect;
|
|
2224
|
+
navigation.navigate(target, { history: "replace" });
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (document.startViewTransition) {
|
|
2228
|
+
const transition = document.startViewTransition(() => {
|
|
2229
|
+
this.slot.setHTMLUnsafe(content);
|
|
2230
|
+
});
|
|
2231
|
+
signal.addEventListener("abort", () => transition.skipTransition(), { once: true });
|
|
2232
|
+
await transition.updateCallbackDone;
|
|
2233
|
+
} else {
|
|
2234
|
+
this.slot.setHTMLUnsafe(content);
|
|
2235
|
+
}
|
|
2236
|
+
if (title)
|
|
2237
|
+
document.title = title;
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
if (signal.aborted)
|
|
2240
|
+
return;
|
|
2241
|
+
console.error("[EmrouteApp] Navigation error:", error);
|
|
2242
|
+
if (this.slot) {
|
|
2243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2244
|
+
this.slot.setHTMLUnsafe(`<h1>Error</h1><p>${escapeHtml(message)}</p>`);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2099
2247
|
}
|
|
2100
2248
|
};
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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);
|
|
2249
|
+
async function createEmrouteApp(server, options) {
|
|
2250
|
+
const g = globalThis;
|
|
2251
|
+
if (g.__emroute_app) {
|
|
2252
|
+
console.warn("eMroute: App already initialized.");
|
|
2253
|
+
return g.__emroute_app;
|
|
2141
2254
|
}
|
|
2142
|
-
|
|
2255
|
+
const app = new EmrouteApp(server, options);
|
|
2256
|
+
await app.initialize();
|
|
2257
|
+
g.__emroute_app = app;
|
|
2258
|
+
return app;
|
|
2143
2259
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
{
|
|
2150
|
-
|
|
2260
|
+
async function bootEmrouteApp(options) {
|
|
2261
|
+
const origin = options?.origin ?? location.origin;
|
|
2262
|
+
const runtime = new FetchRuntime(origin);
|
|
2263
|
+
const routesResponse = await runtime.handle(ROUTES_MANIFEST_PATH);
|
|
2264
|
+
if (!routesResponse.ok) {
|
|
2265
|
+
throw new Error(`[emroute] Failed to fetch ${ROUTES_MANIFEST_PATH}: ${routesResponse.status}`);
|
|
2266
|
+
}
|
|
2267
|
+
const routeTree = await routesResponse.json();
|
|
2268
|
+
const widgetsResponse = await runtime.handle(WIDGETS_MANIFEST_PATH);
|
|
2269
|
+
const widgetEntries = widgetsResponse.ok ? await widgetsResponse.json() : [];
|
|
2270
|
+
const moduleLoaders = buildLazyLoaders(routeTree, widgetEntries, runtime);
|
|
2271
|
+
const widgets = new WidgetRegistry();
|
|
2272
|
+
for (const entry of widgetEntries) {
|
|
2273
|
+
ComponentElement.registerLazy(entry.name, entry.files, moduleLoaders[entry.modulePath]);
|
|
2274
|
+
}
|
|
2275
|
+
const server = await createEmrouteServer({
|
|
2276
|
+
routeTree,
|
|
2277
|
+
widgets,
|
|
2278
|
+
moduleLoaders,
|
|
2279
|
+
markdownRenderer: MarkdownElement.getConfiguredRenderer() ?? void 0
|
|
2280
|
+
}, runtime);
|
|
2281
|
+
return createEmrouteApp(server, options);
|
|
2151
2282
|
}
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2283
|
+
function buildLazyLoaders(tree, widgetEntries, runtime) {
|
|
2284
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2285
|
+
function walk(node) {
|
|
2286
|
+
const modulePath = node.files?.ts ?? node.files?.js;
|
|
2287
|
+
if (modulePath)
|
|
2288
|
+
paths.add(modulePath);
|
|
2289
|
+
if (node.redirect)
|
|
2290
|
+
paths.add(node.redirect);
|
|
2291
|
+
if (node.errorBoundary)
|
|
2292
|
+
paths.add(node.errorBoundary);
|
|
2293
|
+
if (node.children) {
|
|
2294
|
+
for (const child of Object.values(node.children))
|
|
2295
|
+
walk(child);
|
|
2296
|
+
}
|
|
2297
|
+
if (node.dynamic)
|
|
2298
|
+
walk(node.dynamic.child);
|
|
2299
|
+
if (node.wildcard)
|
|
2300
|
+
walk(node.wildcard.child);
|
|
2159
2301
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2302
|
+
walk(tree);
|
|
2303
|
+
for (const entry of widgetEntries)
|
|
2304
|
+
paths.add(entry.modulePath);
|
|
2305
|
+
const loaders = {};
|
|
2306
|
+
for (const path of paths) {
|
|
2307
|
+
const absolute = path.startsWith("/") ? path : "/" + path;
|
|
2308
|
+
loaders[path] = () => runtime.loadModule(absolute);
|
|
2162
2309
|
}
|
|
2310
|
+
return loaders;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// dist/src/component/widget.component.js
|
|
2314
|
+
var WidgetComponent = class extends Component {
|
|
2163
2315
|
/**
|
|
2164
|
-
* Render
|
|
2316
|
+
* Render widget as HTML.
|
|
2317
|
+
*
|
|
2318
|
+
* Fallback chain:
|
|
2319
|
+
* 1. html file content from context
|
|
2320
|
+
* 2. md file content wrapped in `<mark-down>`
|
|
2321
|
+
* 3. base Component default (markdown→HTML conversion)
|
|
2322
|
+
*
|
|
2323
|
+
* @example
|
|
2324
|
+
* ```ts
|
|
2325
|
+
* override renderHTML({ data, params }: this['RenderArgs']) {
|
|
2326
|
+
* return `<span>${params.coin}: $${data?.price}</span>`;
|
|
2327
|
+
* }
|
|
2328
|
+
* ```
|
|
2165
2329
|
*/
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2330
|
+
renderHTML(args) {
|
|
2331
|
+
const files = args.context.files;
|
|
2332
|
+
const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>
|
|
2333
|
+
` : "";
|
|
2334
|
+
if (files?.html) {
|
|
2335
|
+
return style + files.html;
|
|
2169
2336
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
content = content.replaceAll(BARE_SLOT_BLOCK, routerSlotBlock(route.pattern));
|
|
2173
|
-
if (this.widgets) {
|
|
2174
|
-
content = await this.resolveWidgets(content, routeInfo);
|
|
2337
|
+
if (files?.md) {
|
|
2338
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>`;
|
|
2175
2339
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
return
|
|
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}\``;
|
|
2340
|
+
if (style) {
|
|
2341
|
+
return style + super.renderHTML(args);
|
|
2342
|
+
}
|
|
2343
|
+
return super.renderHTML(args);
|
|
2193
2344
|
}
|
|
2194
2345
|
/**
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2346
|
+
* Render widget as Markdown.
|
|
2347
|
+
*
|
|
2348
|
+
* Fallback chain:
|
|
2349
|
+
* 1. md file content from context
|
|
2350
|
+
* 2. empty string
|
|
2351
|
+
*
|
|
2352
|
+
* @example
|
|
2353
|
+
* ```ts
|
|
2354
|
+
* override renderMarkdown({ data, params }: this['RenderArgs']) {
|
|
2355
|
+
* return `**${params.coin}**: $${data?.price}`;
|
|
2356
|
+
* }
|
|
2357
|
+
* ```
|
|
2197
2358
|
*/
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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);
|
|
2359
|
+
renderMarkdown(args) {
|
|
2360
|
+
const files = args.context.files;
|
|
2361
|
+
if (files?.md) {
|
|
2362
|
+
return files.md;
|
|
2363
|
+
}
|
|
2364
|
+
return "";
|
|
2226
2365
|
}
|
|
2227
2366
|
};
|
|
2228
2367
|
|
|
2229
|
-
// dist/src/
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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");
|
|
2368
|
+
// dist/src/overlay/overlay.css.js
|
|
2369
|
+
var overlayCSS = (
|
|
2370
|
+
/* css */
|
|
2371
|
+
`
|
|
2372
|
+
:root {
|
|
2373
|
+
--overlay-backdrop: oklch(0% 0 0 / 0.5);
|
|
2374
|
+
--overlay-surface: oklch(100% 0 0);
|
|
2375
|
+
--overlay-radius: 8px;
|
|
2376
|
+
--overlay-shadow: 0 8px 32px oklch(0% 0 0 / 0.2);
|
|
2377
|
+
--overlay-toast-gap: 8px;
|
|
2378
|
+
--overlay-toast-duration: 5s;
|
|
2379
|
+
--overlay-z: 1000;
|
|
2248
2380
|
}
|
|
2249
2381
|
|
|
2250
|
-
|
|
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
|
-
}
|
|
2382
|
+
/* --- Modal (dialog) --- */
|
|
2267
2383
|
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
var
|
|
2272
|
-
var
|
|
2273
|
-
var
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
invalidateManifests() {
|
|
2295
|
-
this.routesManifestCache = null;
|
|
2296
|
-
this.widgetsManifestCache = null;
|
|
2384
|
+
dialog[data-overlay-modal] {
|
|
2385
|
+
border: none;
|
|
2386
|
+
padding: 0;
|
|
2387
|
+
background: var(--overlay-surface);
|
|
2388
|
+
border-radius: var(--overlay-radius);
|
|
2389
|
+
box-shadow: var(--overlay-shadow);
|
|
2390
|
+
max-width: min(90vw, 560px);
|
|
2391
|
+
max-height: 85vh;
|
|
2392
|
+
overflow: auto;
|
|
2393
|
+
opacity: 1;
|
|
2394
|
+
translate: 0 0;
|
|
2395
|
+
transition:
|
|
2396
|
+
opacity 200ms,
|
|
2397
|
+
translate 200ms;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
dialog[data-overlay-modal][open] {
|
|
2401
|
+
transition:
|
|
2402
|
+
opacity 200ms,
|
|
2403
|
+
translate 200ms,
|
|
2404
|
+
display 200ms allow-discrete,
|
|
2405
|
+
overlay 200ms allow-discrete;
|
|
2406
|
+
|
|
2407
|
+
@starting-style {
|
|
2408
|
+
opacity: 0;
|
|
2409
|
+
translate: 0 20px;
|
|
2297
2410
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
dialog[data-overlay-modal]::backdrop {
|
|
2414
|
+
background: var(--overlay-backdrop);
|
|
2415
|
+
opacity: 1;
|
|
2416
|
+
transition: opacity 200ms;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
dialog[data-overlay-modal][open]::backdrop {
|
|
2420
|
+
transition:
|
|
2421
|
+
opacity 200ms,
|
|
2422
|
+
display 200ms allow-discrete,
|
|
2423
|
+
overlay 200ms allow-discrete;
|
|
2424
|
+
|
|
2425
|
+
@starting-style {
|
|
2426
|
+
opacity: 0;
|
|
2313
2427
|
}
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
dialog[data-overlay-modal][data-dismissing] {
|
|
2431
|
+
opacity: 0;
|
|
2432
|
+
translate: 0 20px;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
dialog[data-overlay-modal][data-dismissing]::backdrop {
|
|
2436
|
+
opacity: 0;
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
/* --- Toast container --- */
|
|
2440
|
+
|
|
2441
|
+
[data-overlay-toast-container] {
|
|
2442
|
+
position: fixed;
|
|
2443
|
+
bottom: 16px;
|
|
2444
|
+
right: 16px;
|
|
2445
|
+
z-index: var(--overlay-z);
|
|
2446
|
+
display: flex;
|
|
2447
|
+
flex-direction: column;
|
|
2448
|
+
gap: var(--overlay-toast-gap);
|
|
2449
|
+
pointer-events: none;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/* --- Toast item --- */
|
|
2453
|
+
|
|
2454
|
+
[data-overlay-toast] {
|
|
2455
|
+
pointer-events: auto;
|
|
2456
|
+
background: var(--overlay-surface);
|
|
2457
|
+
border-radius: var(--overlay-radius);
|
|
2458
|
+
box-shadow: var(--overlay-shadow);
|
|
2459
|
+
padding: 12px 16px;
|
|
2460
|
+
animation: overlay-toast-auto var(--overlay-toast-duration, 5s) ease-in-out forwards;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/* Manual toast (timeout: 0): no auto-dismiss, entry transition only */
|
|
2464
|
+
[data-overlay-toast][data-toast-manual] {
|
|
2465
|
+
animation: none;
|
|
2466
|
+
opacity: 1;
|
|
2467
|
+
translate: 0 0;
|
|
2468
|
+
transition:
|
|
2469
|
+
opacity 200ms,
|
|
2470
|
+
translate 200ms;
|
|
2471
|
+
|
|
2472
|
+
@starting-style {
|
|
2473
|
+
opacity: 0;
|
|
2474
|
+
translate: 20px 0;
|
|
2329
2475
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/* Dismissed toast: CSS exit animation */
|
|
2479
|
+
[data-overlay-toast][data-dismissing] {
|
|
2480
|
+
animation: overlay-toast-exit 200ms ease-in forwards;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
@keyframes overlay-toast-auto {
|
|
2484
|
+
0% { opacity: 0; translate: 20px 0; }
|
|
2485
|
+
10% { opacity: 1; translate: 0 0; }
|
|
2486
|
+
80% { opacity: 1; translate: 0 0; }
|
|
2487
|
+
100% { opacity: 0; translate: 0 0; display: none; }
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
@keyframes overlay-toast-exit {
|
|
2491
|
+
to { opacity: 0; translate: 20px 0; display: none; }
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/* --- Popover --- */
|
|
2495
|
+
|
|
2496
|
+
[data-overlay-popover] {
|
|
2497
|
+
border: none;
|
|
2498
|
+
padding: 0;
|
|
2499
|
+
margin: 0;
|
|
2500
|
+
background: var(--overlay-surface);
|
|
2501
|
+
border-radius: var(--overlay-radius);
|
|
2502
|
+
box-shadow: var(--overlay-shadow);
|
|
2503
|
+
opacity: 1;
|
|
2504
|
+
scale: 1;
|
|
2505
|
+
transition:
|
|
2506
|
+
opacity 200ms,
|
|
2507
|
+
scale 200ms;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
[data-overlay-popover]:popover-open {
|
|
2511
|
+
position-anchor: auto;
|
|
2512
|
+
inset: unset;
|
|
2513
|
+
top: anchor(bottom);
|
|
2514
|
+
left: anchor(start);
|
|
2515
|
+
margin-top: 4px;
|
|
2516
|
+
transition:
|
|
2517
|
+
opacity 200ms,
|
|
2518
|
+
scale 200ms,
|
|
2519
|
+
display 200ms allow-discrete,
|
|
2520
|
+
overlay 200ms allow-discrete;
|
|
2521
|
+
|
|
2522
|
+
@starting-style {
|
|
2523
|
+
opacity: 0;
|
|
2524
|
+
scale: 0.95;
|
|
2343
2525
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
target.files ??= {};
|
|
2384
|
-
target.files[ext] = filePath;
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
return root;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
[data-overlay-popover][data-dismissing] {
|
|
2529
|
+
opacity: 0;
|
|
2530
|
+
scale: 0.95;
|
|
2531
|
+
}
|
|
2532
|
+
`
|
|
2533
|
+
);
|
|
2534
|
+
|
|
2535
|
+
// dist/src/overlay/overlay.service.js
|
|
2536
|
+
var ANIMATION_SAFETY_TIMEOUT = 300;
|
|
2537
|
+
function animateDismiss(el, onDone) {
|
|
2538
|
+
el.setAttribute("data-dismissing", "");
|
|
2539
|
+
let done = false;
|
|
2540
|
+
const finish = () => {
|
|
2541
|
+
if (done)
|
|
2542
|
+
return;
|
|
2543
|
+
done = true;
|
|
2544
|
+
onDone();
|
|
2545
|
+
};
|
|
2546
|
+
el.addEventListener("transitionend", finish, { once: true });
|
|
2547
|
+
setTimeout(finish, ANIMATION_SAFETY_TIMEOUT);
|
|
2548
|
+
}
|
|
2549
|
+
function createOverlayService() {
|
|
2550
|
+
let styleInjected = false;
|
|
2551
|
+
let dialog = null;
|
|
2552
|
+
let modalResolve = null;
|
|
2553
|
+
let modalOnClose;
|
|
2554
|
+
let toastContainer = null;
|
|
2555
|
+
let popoverEl = null;
|
|
2556
|
+
let popoverAnchorObserver = null;
|
|
2557
|
+
const supportsAnchor = typeof CSS !== "undefined" && CSS.supports("anchor-name", "--a");
|
|
2558
|
+
function injectCSS() {
|
|
2559
|
+
if (styleInjected)
|
|
2560
|
+
return;
|
|
2561
|
+
styleInjected = true;
|
|
2562
|
+
const style = document.createElement("style");
|
|
2563
|
+
style.textContent = overlayCSS;
|
|
2564
|
+
document.head.appendChild(style);
|
|
2388
2565
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
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;
|
|
2566
|
+
function ensureDialog() {
|
|
2567
|
+
if (dialog)
|
|
2568
|
+
return dialog;
|
|
2569
|
+
injectCSS();
|
|
2570
|
+
dialog = document.createElement("dialog");
|
|
2571
|
+
dialog.setAttribute("data-overlay-modal", "");
|
|
2572
|
+
document.body.appendChild(dialog);
|
|
2573
|
+
dialog.addEventListener("click", (e) => {
|
|
2574
|
+
if (e.target === dialog) {
|
|
2575
|
+
closeModal(void 0);
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
return dialog;
|
|
2429
2579
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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);
|
|
2580
|
+
function ensureToastContainer() {
|
|
2581
|
+
if (toastContainer)
|
|
2582
|
+
return toastContainer;
|
|
2583
|
+
injectCSS();
|
|
2584
|
+
toastContainer = document.createElement("div");
|
|
2585
|
+
toastContainer.setAttribute("data-overlay-toast-container", "");
|
|
2586
|
+
document.body.appendChild(toastContainer);
|
|
2587
|
+
return toastContainer;
|
|
2451
2588
|
}
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2589
|
+
function ensurePopover() {
|
|
2590
|
+
if (popoverEl)
|
|
2591
|
+
return popoverEl;
|
|
2592
|
+
injectCSS();
|
|
2593
|
+
popoverEl = document.createElement("div");
|
|
2594
|
+
popoverEl.setAttribute("data-overlay-popover", "");
|
|
2595
|
+
popoverEl.setAttribute("popover", "");
|
|
2596
|
+
document.body.appendChild(popoverEl);
|
|
2597
|
+
return popoverEl;
|
|
2456
2598
|
}
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
if (
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2599
|
+
function modal(options) {
|
|
2600
|
+
const d = ensureDialog();
|
|
2601
|
+
d.removeAttribute("data-dismissing");
|
|
2602
|
+
hidePopoverImmediate();
|
|
2603
|
+
if (d.open) {
|
|
2604
|
+
d.close();
|
|
2605
|
+
if (modalResolve) {
|
|
2606
|
+
modalResolve(void 0);
|
|
2607
|
+
modalResolve = null;
|
|
2608
|
+
}
|
|
2609
|
+
if (modalOnClose) {
|
|
2610
|
+
modalOnClose();
|
|
2611
|
+
modalOnClose = void 0;
|
|
2612
|
+
}
|
|
2468
2613
|
}
|
|
2614
|
+
d.innerHTML = "";
|
|
2615
|
+
options.render(d);
|
|
2616
|
+
modalOnClose = options.onClose;
|
|
2617
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
2618
|
+
modalResolve = resolve;
|
|
2619
|
+
d.showModal();
|
|
2620
|
+
return promise;
|
|
2469
2621
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
widgetFiles[entry.name] = inlined;
|
|
2486
|
-
} else if (entry.files) {
|
|
2487
|
-
widgetFiles[entry.name] = entry.files;
|
|
2622
|
+
function closeModal(value) {
|
|
2623
|
+
if (!dialog || !dialog.open)
|
|
2624
|
+
return;
|
|
2625
|
+
const resolve = modalResolve;
|
|
2626
|
+
const onClose = modalOnClose;
|
|
2627
|
+
const dialogRef = dialog;
|
|
2628
|
+
modalResolve = null;
|
|
2629
|
+
modalOnClose = void 0;
|
|
2630
|
+
animateDismiss(dialogRef, () => {
|
|
2631
|
+
if (dialogRef && dialogRef.open) {
|
|
2632
|
+
dialogRef.close();
|
|
2633
|
+
if (resolve)
|
|
2634
|
+
resolve(value);
|
|
2635
|
+
if (onClose)
|
|
2636
|
+
onClose();
|
|
2488
2637
|
}
|
|
2489
|
-
}
|
|
2490
|
-
console.error(`[emroute] Failed to load widget ${entry.modulePath}:`, e);
|
|
2491
|
-
if (entry.files)
|
|
2492
|
-
widgetFiles[entry.name] = entry.files;
|
|
2493
|
-
}
|
|
2638
|
+
});
|
|
2494
2639
|
}
|
|
2495
|
-
|
|
2496
|
-
for (const
|
|
2497
|
-
|
|
2640
|
+
function clearDeadToasts(container) {
|
|
2641
|
+
for (const child of [...container.children]) {
|
|
2642
|
+
const el = child;
|
|
2643
|
+
if (el.hasAttribute("data-dismissing")) {
|
|
2644
|
+
el.remove();
|
|
2645
|
+
}
|
|
2498
2646
|
}
|
|
2499
2647
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
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.`);
|
|
2648
|
+
function toast(options) {
|
|
2649
|
+
const container = ensureToastContainer();
|
|
2650
|
+
clearDeadToasts(container);
|
|
2651
|
+
const el = document.createElement("div");
|
|
2652
|
+
el.setAttribute("data-overlay-toast", "");
|
|
2653
|
+
const timeout = options.timeout ?? 0;
|
|
2654
|
+
if (timeout === 0) {
|
|
2655
|
+
el.setAttribute("data-toast-manual", "");
|
|
2656
|
+
} else {
|
|
2657
|
+
el.style.setProperty("--overlay-toast-duration", `${timeout}ms`);
|
|
2545
2658
|
}
|
|
2546
|
-
|
|
2659
|
+
options.render(el);
|
|
2660
|
+
container.appendChild(el);
|
|
2661
|
+
let dismissed = false;
|
|
2662
|
+
const dismiss = () => {
|
|
2663
|
+
if (dismissed)
|
|
2664
|
+
return;
|
|
2665
|
+
dismissed = true;
|
|
2666
|
+
el.setAttribute("data-dismissing", "");
|
|
2667
|
+
};
|
|
2668
|
+
return { dismiss };
|
|
2547
2669
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2670
|
+
function popover(options) {
|
|
2671
|
+
const el = ensurePopover();
|
|
2672
|
+
cleanupPopoverAnchorObserver();
|
|
2673
|
+
try {
|
|
2674
|
+
el.hidePopover();
|
|
2675
|
+
} catch {
|
|
2676
|
+
}
|
|
2677
|
+
el.removeAttribute("data-dismissing");
|
|
2678
|
+
el.innerHTML = "";
|
|
2679
|
+
options.render(el);
|
|
2680
|
+
if (supportsAnchor) {
|
|
2681
|
+
const anchorName = "--overlay-anchor";
|
|
2682
|
+
options.anchor.style.setProperty("anchor-name", anchorName);
|
|
2683
|
+
el.style.setProperty("position-anchor", anchorName);
|
|
2684
|
+
el.style.removeProperty("top");
|
|
2685
|
+
el.style.removeProperty("left");
|
|
2562
2686
|
} else {
|
|
2563
|
-
const
|
|
2564
|
-
|
|
2565
|
-
|
|
2687
|
+
const rect = options.anchor.getBoundingClientRect();
|
|
2688
|
+
el.style.top = `${rect.bottom + globalThis.scrollY}px`;
|
|
2689
|
+
el.style.left = `${rect.left + globalThis.scrollX}px`;
|
|
2690
|
+
el.style.position = "absolute";
|
|
2566
2691
|
}
|
|
2692
|
+
el.showPopover();
|
|
2693
|
+
watchAnchorDisconnect(options.anchor);
|
|
2567
2694
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
if (
|
|
2572
|
-
|
|
2573
|
-
ssrMdRouter = null;
|
|
2695
|
+
function watchAnchorDisconnect(anchor) {
|
|
2696
|
+
cleanupPopoverAnchorObserver();
|
|
2697
|
+
const parent = anchor.parentNode;
|
|
2698
|
+
if (!parent) {
|
|
2699
|
+
closePopover();
|
|
2574
2700
|
return;
|
|
2575
2701
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
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
|
|
2702
|
+
popoverAnchorObserver = new MutationObserver(() => {
|
|
2703
|
+
if (!document.contains(anchor)) {
|
|
2704
|
+
closePopover();
|
|
2705
|
+
}
|
|
2590
2706
|
});
|
|
2707
|
+
popoverAnchorObserver.observe(parent, { childList: true });
|
|
2591
2708
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2709
|
+
function hidePopoverImmediate() {
|
|
2710
|
+
cleanupPopoverAnchorObserver();
|
|
2711
|
+
if (!popoverEl)
|
|
2712
|
+
return;
|
|
2713
|
+
try {
|
|
2714
|
+
popoverEl.hidePopover();
|
|
2715
|
+
} catch {
|
|
2716
|
+
}
|
|
2717
|
+
popoverEl.removeAttribute("data-dismissing");
|
|
2597
2718
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
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
|
-
}
|
|
2719
|
+
function cleanupPopoverAnchorObserver() {
|
|
2720
|
+
if (popoverAnchorObserver) {
|
|
2721
|
+
popoverAnchorObserver.disconnect();
|
|
2722
|
+
popoverAnchorObserver = null;
|
|
2625
2723
|
}
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2724
|
+
}
|
|
2725
|
+
function closePopover() {
|
|
2726
|
+
cleanupPopoverAnchorObserver();
|
|
2727
|
+
if (!popoverEl)
|
|
2728
|
+
return;
|
|
2729
|
+
let isOpen;
|
|
2730
|
+
try {
|
|
2731
|
+
isOpen = popoverEl.matches(":popover-open");
|
|
2732
|
+
} catch {
|
|
2733
|
+
isOpen = popoverEl.hasAttribute("popover") && popoverEl.style.display !== "none";
|
|
2734
|
+
}
|
|
2735
|
+
if (!isOpen)
|
|
2736
|
+
return;
|
|
2737
|
+
animateDismiss(popoverEl, () => {
|
|
2632
2738
|
try {
|
|
2633
|
-
|
|
2634
|
-
|
|
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 });
|
|
2739
|
+
popoverEl.hidePopover();
|
|
2740
|
+
} catch {
|
|
2648
2741
|
}
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
function dismissAll() {
|
|
2745
|
+
if (dialog && dialog.open) {
|
|
2746
|
+
const resolve = modalResolve;
|
|
2747
|
+
const onClose = modalOnClose;
|
|
2748
|
+
modalResolve = null;
|
|
2749
|
+
modalOnClose = void 0;
|
|
2750
|
+
dialog.removeAttribute("data-dismissing");
|
|
2751
|
+
dialog.close();
|
|
2752
|
+
if (resolve)
|
|
2753
|
+
resolve(void 0);
|
|
2754
|
+
if (onClose)
|
|
2755
|
+
onClose();
|
|
2649
2756
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
}
|
|
2757
|
+
hidePopoverImmediate();
|
|
2758
|
+
if (toastContainer) {
|
|
2759
|
+
for (const child of toastContainer.children) {
|
|
2760
|
+
child.setAttribute("data-dismissing", "");
|
|
2761
|
+
}
|
|
2655
2762
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2763
|
+
try {
|
|
2764
|
+
for (const el of document.querySelectorAll(":popover-open")) {
|
|
2765
|
+
el.hidePopover();
|
|
2766
|
+
}
|
|
2767
|
+
} catch {
|
|
2661
2768
|
}
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
if (fileResponse.status === 200)
|
|
2666
|
-
return fileResponse;
|
|
2667
|
-
return null;
|
|
2769
|
+
for (const el of document.querySelectorAll("dialog[open]")) {
|
|
2770
|
+
if (el !== dialog)
|
|
2771
|
+
el.close();
|
|
2668
2772
|
}
|
|
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
2773
|
}
|
|
2673
2774
|
return {
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
},
|
|
2681
|
-
get routeTree() {
|
|
2682
|
-
return routeTree;
|
|
2683
|
-
},
|
|
2684
|
-
get widgetEntries() {
|
|
2685
|
-
return discoveredWidgetEntries;
|
|
2686
|
-
},
|
|
2687
|
-
get shell() {
|
|
2688
|
-
return shell;
|
|
2689
|
-
}
|
|
2775
|
+
modal,
|
|
2776
|
+
closeModal,
|
|
2777
|
+
toast,
|
|
2778
|
+
popover,
|
|
2779
|
+
closePopover,
|
|
2780
|
+
dismissAll
|
|
2690
2781
|
};
|
|
2691
2782
|
}
|
|
2692
2783
|
|
|
2693
|
-
// dist/
|
|
2694
|
-
var
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
});
|
|
2784
|
+
// dist/src/widget/page-title.widget.js
|
|
2785
|
+
var PageTitleWidget = class extends WidgetComponent {
|
|
2786
|
+
name = "page-title";
|
|
2787
|
+
getData(args) {
|
|
2788
|
+
return Promise.resolve({ title: args.params.title });
|
|
2699
2789
|
}
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
*/
|
|
2707
|
-
constructor(origin, config = {}) {
|
|
2708
|
-
super(config);
|
|
2709
|
-
this.origin = origin.endsWith("/") ? origin.slice(0, -1) : origin;
|
|
2790
|
+
renderHTML(args) {
|
|
2791
|
+
const title = args.data?.title ?? args.params.title;
|
|
2792
|
+
if (title && typeof document !== "undefined") {
|
|
2793
|
+
document.title = title;
|
|
2794
|
+
}
|
|
2795
|
+
return "";
|
|
2710
2796
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
return fetch(url, init);
|
|
2797
|
+
renderMarkdown(_args) {
|
|
2798
|
+
return "";
|
|
2714
2799
|
}
|
|
2715
|
-
|
|
2716
|
-
if (
|
|
2717
|
-
return
|
|
2800
|
+
validateParams(params) {
|
|
2801
|
+
if (!params.title || typeof params.title !== "string") {
|
|
2802
|
+
return 'page-title widget requires a "title" string param';
|
|
2718
2803
|
}
|
|
2719
|
-
return
|
|
2804
|
+
return void 0;
|
|
2720
2805
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
// dist/src/widget/breadcrumb.widget.js
|
|
2809
|
+
var DEFAULT_HTML_SEPARATOR = " \u203A ";
|
|
2810
|
+
var DEFAULT_MD_SEPARATOR = " > ";
|
|
2811
|
+
var BreadcrumbWidget = class extends WidgetComponent {
|
|
2812
|
+
name = "breadcrumb";
|
|
2813
|
+
getData(args) {
|
|
2814
|
+
const pathname = args.context.pathname || "/";
|
|
2815
|
+
const parts = pathname.split("/").filter(Boolean);
|
|
2816
|
+
const segments = [
|
|
2817
|
+
{ label: "Home", href: "/" }
|
|
2818
|
+
];
|
|
2819
|
+
let accumulated = "";
|
|
2820
|
+
for (const part of parts) {
|
|
2821
|
+
accumulated += "/" + part;
|
|
2822
|
+
segments.push({
|
|
2823
|
+
label: part.charAt(0).toUpperCase() + part.slice(1).replace(/-/g, " "),
|
|
2824
|
+
href: accumulated
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
return Promise.resolve({ segments });
|
|
2727
2828
|
}
|
|
2728
|
-
|
|
2729
|
-
if (
|
|
2730
|
-
return
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2829
|
+
renderHTML(args) {
|
|
2830
|
+
if (!args.data || args.data.segments.length === 0)
|
|
2831
|
+
return "";
|
|
2832
|
+
const sep = args.params.separator ?? DEFAULT_HTML_SEPARATOR;
|
|
2833
|
+
const segments = args.data.segments;
|
|
2834
|
+
const items = segments.map((seg, i) => {
|
|
2835
|
+
const escaped = escapeHtml(seg.label);
|
|
2836
|
+
if (i === segments.length - 1) {
|
|
2837
|
+
return `<span aria-current="page">${escaped}</span>`;
|
|
2838
|
+
}
|
|
2839
|
+
return `<a href="${escapeHtml(seg.href)}">${escaped}</a>`;
|
|
2840
|
+
});
|
|
2841
|
+
return `<nav aria-label="Breadcrumb">${items.join(escapeHtml(sep))}</nav>`;
|
|
2842
|
+
}
|
|
2843
|
+
renderMarkdown(args) {
|
|
2844
|
+
if (!args.data || args.data.segments.length === 0)
|
|
2845
|
+
return "";
|
|
2846
|
+
const sep = args.params.separator ?? DEFAULT_MD_SEPARATOR;
|
|
2847
|
+
return args.data.segments.map((seg, i, arr) => i === arr.length - 1 ? `**${seg.label}**` : `[${seg.label}](${seg.href})`).join(sep);
|
|
2734
2848
|
}
|
|
2735
2849
|
};
|
|
2850
|
+
|
|
2851
|
+
// dist/src/renderer/spa/mod.js
|
|
2852
|
+
if (globalThis.customElements) {
|
|
2853
|
+
if (!customElements.get("router-slot"))
|
|
2854
|
+
customElements.define("router-slot", RouterSlot);
|
|
2855
|
+
if (!customElements.get("mark-down"))
|
|
2856
|
+
customElements.define("mark-down", MarkdownElement);
|
|
2857
|
+
}
|
|
2736
2858
|
export {
|
|
2737
2859
|
BreadcrumbWidget,
|
|
2738
2860
|
Component,
|
|
@@ -2747,6 +2869,7 @@ export {
|
|
|
2747
2869
|
RouterSlot,
|
|
2748
2870
|
WidgetComponent,
|
|
2749
2871
|
WidgetRegistry,
|
|
2872
|
+
bootEmrouteApp,
|
|
2750
2873
|
createEmrouteApp,
|
|
2751
2874
|
createEmrouteServer,
|
|
2752
2875
|
createOverlayService,
|