@markless/router 0.1.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +123 -0
- package/dist/request-files-DTkeaKxU.js +232 -0
- package/dist/route-manifest-OTaWQ_TC.js +186 -0
- package/dist/typescript-plugin.d.ts +11199 -0
- package/dist/typescript-plugin.js +137 -0
- package/dist/vite/route-href.d.ts +4 -0
- package/dist/vite/route-href.js +18 -0
- package/dist/vite/runtime/create-route-discovery.d.ts +8 -0
- package/dist/vite/runtime/create-route-discovery.js +11 -0
- package/dist/vite/runtime/create-server-entry.d.ts +18 -0
- package/dist/vite/runtime/create-server-entry.js +297 -0
- package/dist/vite/runtime/mdx-route.d.ts +68 -0
- package/dist/vite/runtime/mdx-route.js +113 -0
- package/dist/vite.d.ts +10 -0
- package/dist/vite.js +1355 -0
- package/package.json +71 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { extname, isAbsolute, normalize, relative, resolve } from "pathe";
|
|
2
|
+
//#region src/typescript-plugin/index.ts
|
|
3
|
+
const PAGE_EXTENSIONS = new Set([".tsrx", ".mdx"]);
|
|
4
|
+
function init(modules) {
|
|
5
|
+
const typeScript = modules.typescript;
|
|
6
|
+
return { create(info) {
|
|
7
|
+
(info.project.projectService?.logger)?.info("[markless-router] TypeScript plugin loaded");
|
|
8
|
+
const languageService = info.languageService;
|
|
9
|
+
const proxy = Object.create(null);
|
|
10
|
+
for (const key of Object.keys(languageService)) {
|
|
11
|
+
const value = languageService[key];
|
|
12
|
+
proxy[key] = typeof value === "function" ? value.bind(languageService) : value;
|
|
13
|
+
}
|
|
14
|
+
const pluginConfig = info.config ?? {};
|
|
15
|
+
const projectRoot = getProjectRoot(typeScript, info);
|
|
16
|
+
const pagesDir = resolvePagesDir(getConfiguredProjectRoot(info) ?? projectRoot, pluginConfig);
|
|
17
|
+
proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
18
|
+
return withRouteHrefCompletions(typeScript, info, pagesDir, languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings));
|
|
19
|
+
};
|
|
20
|
+
return proxy;
|
|
21
|
+
} };
|
|
22
|
+
}
|
|
23
|
+
function withRouteHrefCompletions(typeScript, info, pagesDir, completions) {
|
|
24
|
+
const routeHrefs = routeHrefCompletions(typeScript, info, pagesDir);
|
|
25
|
+
if (routeHrefs.length === 0) return completions;
|
|
26
|
+
const baseCompletions = completions ?? emptyCompletionInfo();
|
|
27
|
+
const routeHrefSet = new Set(routeHrefs);
|
|
28
|
+
const routeEntries = routeHrefs.map((href) => ({
|
|
29
|
+
name: href,
|
|
30
|
+
kind: typeScript.ScriptElementKind.string,
|
|
31
|
+
kindModifiers: "",
|
|
32
|
+
sortText: `0 ${href}`
|
|
33
|
+
}));
|
|
34
|
+
return {
|
|
35
|
+
...baseCompletions,
|
|
36
|
+
entries: [...routeEntries, ...baseCompletions.entries.filter((entry) => !routeHrefSet.has(entry.name))]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function emptyCompletionInfo() {
|
|
40
|
+
return {
|
|
41
|
+
isGlobalCompletion: false,
|
|
42
|
+
isMemberCompletion: false,
|
|
43
|
+
isNewIdentifierLocation: false,
|
|
44
|
+
entries: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function routeHrefCompletions(typeScript, info, pagesDir) {
|
|
48
|
+
const hrefs = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const pageFileName of routeCompletionPageFileNames(typeScript, info, pagesDir)) {
|
|
50
|
+
const relativeFileId = relative(pagesDir, pageFileName);
|
|
51
|
+
if (!isRelativeInsidePath(relativeFileId)) continue;
|
|
52
|
+
if (!PAGE_EXTENSIONS.has(extname(relativeFileId))) continue;
|
|
53
|
+
const href = routeHrefCompletion(relativeFileId);
|
|
54
|
+
if (href) hrefs.add(href);
|
|
55
|
+
}
|
|
56
|
+
return Array.from(hrefs).toSorted();
|
|
57
|
+
}
|
|
58
|
+
function routeCompletionPageFileNames(typeScript, info, pagesDir) {
|
|
59
|
+
const fileNames = new Set(info.languageServiceHost.getScriptFileNames?.() ?? []);
|
|
60
|
+
for (const fileName of readPageFileNames(typeScript, pagesDir)) fileNames.add(fileName);
|
|
61
|
+
return [...fileNames];
|
|
62
|
+
}
|
|
63
|
+
function readPageFileNames(typeScript, pagesDir) {
|
|
64
|
+
if (typeScript.sys.directoryExists && !typeScript.sys.directoryExists(pagesDir)) return [];
|
|
65
|
+
try {
|
|
66
|
+
return typeScript.sys.readDirectory(pagesDir, Array.from(PAGE_EXTENSIONS), void 0, void 0);
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function routeHrefCompletion(fileId) {
|
|
72
|
+
if (isReservedOrUnsupportedPage(fileId)) return;
|
|
73
|
+
return routeFromFileId(fileId).pattern;
|
|
74
|
+
}
|
|
75
|
+
function isReservedOrUnsupportedPage(fileId) {
|
|
76
|
+
const extension = extname(fileId);
|
|
77
|
+
const routeFile = normalize(fileId.slice(0, -extension.length));
|
|
78
|
+
const segments = routeFile === "." ? [] : routeFile.split("/").filter(Boolean);
|
|
79
|
+
return segments[0] === "api" || segments.length === 1 && (segments[0] === "404" || segments[0] === "500");
|
|
80
|
+
}
|
|
81
|
+
function routeFromFileId(fileId) {
|
|
82
|
+
const extension = extname(fileId);
|
|
83
|
+
const routeFile = normalize(fileId.slice(0, -extension.length));
|
|
84
|
+
const segments = routeFile === "." ? [] : routeFile.split("/").filter(Boolean);
|
|
85
|
+
if (segments.at(-1) === "index") segments.pop();
|
|
86
|
+
const params = [];
|
|
87
|
+
const routeSegments = [];
|
|
88
|
+
for (const segment of segments) {
|
|
89
|
+
const catchAllName = bracketParamName(segment, "[...");
|
|
90
|
+
if (catchAllName) {
|
|
91
|
+
routeSegments.push(`[...${catchAllName}]`);
|
|
92
|
+
params.push(catchAllName);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const dynamicName = bracketParamName(segment, "[");
|
|
96
|
+
if (dynamicName) {
|
|
97
|
+
routeSegments.push(`[${dynamicName}]`);
|
|
98
|
+
params.push(dynamicName);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
routeSegments.push(segment);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
pattern: routePathname(routeSegments),
|
|
105
|
+
params
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function bracketParamName(segment, opening) {
|
|
109
|
+
if (!segment.startsWith(opening) || !segment.endsWith("]")) return;
|
|
110
|
+
const name = segment.slice(opening.length, -1);
|
|
111
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : void 0;
|
|
112
|
+
}
|
|
113
|
+
function routePathname(segments) {
|
|
114
|
+
if (segments.length === 0) return "/";
|
|
115
|
+
return `/${segments.join("/")}`;
|
|
116
|
+
}
|
|
117
|
+
function getProjectRoot(typeScript, info) {
|
|
118
|
+
return info.project.getCurrentDirectory() ?? info.languageServiceHost.getCurrentDirectory?.() ?? typeScript.sys.getCurrentDirectory();
|
|
119
|
+
}
|
|
120
|
+
function getConfiguredProjectRoot(info) {
|
|
121
|
+
const project = info.project;
|
|
122
|
+
try {
|
|
123
|
+
const configFilePath = project.getConfigFilePath?.();
|
|
124
|
+
return configFilePath ? configFilePath.slice(0, configFilePath.lastIndexOf("/")) : void 0;
|
|
125
|
+
} catch {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function resolvePagesDir(projectRoot, config) {
|
|
130
|
+
const configured = config.pagesDir ?? "pages";
|
|
131
|
+
return isAbsolute(configured) ? configured : resolve(projectRoot, configured);
|
|
132
|
+
}
|
|
133
|
+
function isRelativeInsidePath(path) {
|
|
134
|
+
return path !== "" && path !== ".." && !path.startsWith("../") && !isAbsolute(path);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { init as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/vite/entries/route-href.ts
|
|
2
|
+
const routeParamPattern = /\[\.\.\.([A-Za-z_$][\w$]*)\]|\[([A-Za-z_$][\w$]*)\]/g;
|
|
3
|
+
function __marklessRouteHref(pattern, params) {
|
|
4
|
+
return pattern.replace(routeParamPattern, (_match, catchAll, dynamic) => {
|
|
5
|
+
const name = catchAll || dynamic;
|
|
6
|
+
const value = params?.[name];
|
|
7
|
+
if (catchAll) return encodeCatchAllParam(pattern, value);
|
|
8
|
+
if (value === null || value === void 0) throw new Error(`Typed route error: ${pattern} requires params:\n- ${name}`);
|
|
9
|
+
return encodeURIComponent(String(value));
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function encodeCatchAllParam(pattern, value) {
|
|
13
|
+
const segments = Array.isArray(value) ? value : value === null || value === void 0 ? [] : String(value).split("/");
|
|
14
|
+
if (segments.length === 0 || segments.some((segment) => String(segment) === "")) throw new Error(`Typed route error: ${pattern} requires a non-empty catch-all param.`);
|
|
15
|
+
return segments.map((segment) => encodeURIComponent(String(segment))).join("/");
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { __marklessRouteHref };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/vite/runtime/create-route-discovery.d.ts
|
|
2
|
+
interface RouteDiscovery {
|
|
3
|
+
readonly pageModuleLoaders: Record<string, () => Promise<unknown>>;
|
|
4
|
+
readonly routeFileIds: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
declare function createRouteDiscovery(discoveredPageModuleLoaders: Record<string, () => Promise<unknown>>): RouteDiscovery;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { RouteDiscovery, createRouteDiscovery };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { i as normalizeRouteFileId } from "../../route-manifest-OTaWQ_TC.js";
|
|
2
|
+
//#region src/vite/runtime/create-route-discovery.ts
|
|
3
|
+
function createRouteDiscovery(discoveredPageModuleLoaders) {
|
|
4
|
+
const pageModuleLoaders = Object.fromEntries(Object.entries(discoveredPageModuleLoaders).map(([file, loader]) => [normalizeRouteFileId(file), loader]));
|
|
5
|
+
return {
|
|
6
|
+
pageModuleLoaders,
|
|
7
|
+
routeFileIds: Object.keys(pageModuleLoaders)
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { createRouteDiscovery };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ModulePreloadInput } from "@markless/web/render-to-string";
|
|
2
|
+
|
|
3
|
+
//#region src/vite/runtime/create-server-entry.d.ts
|
|
4
|
+
interface ServerEntryOptions {
|
|
5
|
+
readonly navigationEntryPath?: string;
|
|
6
|
+
readonly resumeEntryPath?: string;
|
|
7
|
+
readonly routeModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
|
|
8
|
+
readonly routeSsrModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
|
|
9
|
+
readonly documentModuleLoader: (() => Promise<unknown>) | undefined;
|
|
10
|
+
readonly pageModuleLoaders: Record<string, () => Promise<unknown>>;
|
|
11
|
+
readonly routeFileIds: readonly string[];
|
|
12
|
+
readonly render?: 'streaming' | 'blocking';
|
|
13
|
+
}
|
|
14
|
+
declare function createServerEntry(options: ServerEntryOptions): {
|
|
15
|
+
fetch: (request: Request) => Promise<Response>;
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { ServerEntryOptions, createServerEntry };
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-OTaWQ_TC.js";
|
|
2
|
+
import { renderToString } from "@markless/web/render-to-string";
|
|
3
|
+
import { renderToStream } from "@markless/web/render-to-stream";
|
|
4
|
+
//#region src/vite/runtime/create-server-entry.ts
|
|
5
|
+
const DOCUMENT_CHILDREN_PLACEHOLDER = "__markless_router_document_children__";
|
|
6
|
+
function createServerEntry(options) {
|
|
7
|
+
const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
|
|
8
|
+
async function fetch(request) {
|
|
9
|
+
const url = new URL(request.url);
|
|
10
|
+
if (isNitroApiPathname(url.pathname)) return new Response("Not found", { status: 404 });
|
|
11
|
+
const match = matchRouteManifest(url.pathname, manifest);
|
|
12
|
+
if (!match) return renderStatusPage(url, manifest.statusPages.notFound, 404, "Not found");
|
|
13
|
+
try {
|
|
14
|
+
return await renderPage(url, match.route.file, match.params, 200);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error("[markless-router] page render failed:", error);
|
|
17
|
+
return renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function renderStatusPage(url, file, status, fallbackText) {
|
|
21
|
+
if (!file) return new Response(fallbackText, { status });
|
|
22
|
+
return renderPage(url, file, {}, status);
|
|
23
|
+
}
|
|
24
|
+
async function renderPage(url, file, params, status) {
|
|
25
|
+
const loadPageModule = options.pageModuleLoaders[file];
|
|
26
|
+
if (!loadPageModule) return new Response(`Page module not found: ${file}`, { status: 500 });
|
|
27
|
+
const pageModule = await loadPageModule();
|
|
28
|
+
const pageProps = {
|
|
29
|
+
params,
|
|
30
|
+
url: {
|
|
31
|
+
href: url.href,
|
|
32
|
+
pathname: url.pathname,
|
|
33
|
+
search: url.search
|
|
34
|
+
},
|
|
35
|
+
status
|
|
36
|
+
};
|
|
37
|
+
const documentModule = options.documentModuleLoader ? await options.documentModuleLoader() : void 0;
|
|
38
|
+
const headers = { "content-type": "text/html;charset=utf-8" };
|
|
39
|
+
const baseArtifact = pageModule.default;
|
|
40
|
+
const renderSsr = baseArtifact?.renderSsr ?? pageModule.marklessRenderSsr;
|
|
41
|
+
if (!renderSsr) {
|
|
42
|
+
const shell = await renderDocumentShell(documentModule, pageProps, "");
|
|
43
|
+
const message = `Page module must export an Markless compiled artifact: ${escapeHtml(file)}`;
|
|
44
|
+
return new Response(fillDocumentChildren(shell, message), {
|
|
45
|
+
status,
|
|
46
|
+
headers
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath);
|
|
50
|
+
const renderOptions = {
|
|
51
|
+
props: pageProps,
|
|
52
|
+
resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
|
|
53
|
+
modulePreloads: (html) => modulePreloadsForPage(file, html, pageProps.url.href, manifest, options.routeModulePreloads, options.routeSsrModulePreloads)
|
|
54
|
+
};
|
|
55
|
+
if (options.render === "blocking") {
|
|
56
|
+
const pageHtml = splitLeadingModulePreloadLinks(await renderToString(pageArtifact, renderOptions));
|
|
57
|
+
const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
|
|
58
|
+
return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
|
|
59
|
+
status,
|
|
60
|
+
headers
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const stream = await renderToStream(pageArtifact, renderOptions);
|
|
64
|
+
const pageHtml = splitLeadingModulePreloadLinks(stream.shell);
|
|
65
|
+
const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
|
|
66
|
+
if (stream.pendingArmCount === 0) return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
|
|
67
|
+
status,
|
|
68
|
+
headers
|
|
69
|
+
});
|
|
70
|
+
const placeholderAt = shell.indexOf(DOCUMENT_CHILDREN_PLACEHOLDER);
|
|
71
|
+
const prefix = placeholderAt === -1 ? shell : shell.slice(0, placeholderAt) + pageHtml.bodyHtml;
|
|
72
|
+
const suffix = placeholderAt === -1 ? "" : shell.slice(placeholderAt + 37);
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
const body = new ReadableStream({ async start(controller) {
|
|
75
|
+
controller.enqueue(encoder.encode(prefix));
|
|
76
|
+
try {
|
|
77
|
+
for await (const chunk of stream.appends()) controller.enqueue(encoder.encode(chunk));
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("[markless-router] streaming settle failed:", error);
|
|
80
|
+
controller.error(error);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
controller.enqueue(encoder.encode(suffix));
|
|
84
|
+
controller.close();
|
|
85
|
+
} });
|
|
86
|
+
return new Response(body, {
|
|
87
|
+
status,
|
|
88
|
+
headers
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { fetch };
|
|
92
|
+
}
|
|
93
|
+
function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath) {
|
|
94
|
+
return {
|
|
95
|
+
resumeModuleUrl: baseArtifact?.resumeModuleUrl,
|
|
96
|
+
async renderSsr(renderProps, renderContext) {
|
|
97
|
+
const output = await renderSsr(renderProps ?? pageProps, renderContext);
|
|
98
|
+
if (!output) return { html: "" };
|
|
99
|
+
const routeScript = output.state || output.view ? renderRouteScript(file) : "";
|
|
100
|
+
const linkBridge = navigationEntryPath ? renderLinkBridgeScript(navigationEntryPath) : "";
|
|
101
|
+
const stateWithProps = withPagePropsCell(output.state, pageProps);
|
|
102
|
+
return routeScript || linkBridge || stateWithProps !== output.state ? {
|
|
103
|
+
...output,
|
|
104
|
+
state: stateWithProps,
|
|
105
|
+
html: `${output.html}${routeScript}${linkBridge}`
|
|
106
|
+
} : output;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function withPagePropsCell(state, props) {
|
|
111
|
+
if (!state || typeof state !== "object") return state;
|
|
112
|
+
const cells = state.cells;
|
|
113
|
+
if (!Array.isArray(cells) || cells.some((cell) => cell?.graphNodeId === "prop:props")) return state;
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
cells: [...cells, {
|
|
117
|
+
graphNodeId: "prop:props",
|
|
118
|
+
name: "props",
|
|
119
|
+
valueKind: "object",
|
|
120
|
+
directValue: props
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function modulePreloadsForPage(file, html, baseHref, manifest, routeModulePreloads, routeSsrModulePreloads) {
|
|
125
|
+
const preloads = [];
|
|
126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
127
|
+
addModulePreloads(preloads, seen, routeSsrModulePreloads?.[file]);
|
|
128
|
+
if (!routeModulePreloads || !html.includes("data-markless-router-link")) return preloads.length > 0 ? preloads : void 0;
|
|
129
|
+
for (const href of routerLinkHrefs(html)) {
|
|
130
|
+
const url = parseSameOriginUrl(href, baseHref);
|
|
131
|
+
const match = url && matchRouteManifest(url.pathname, manifest);
|
|
132
|
+
addModulePreloads(preloads, seen, match && routeModulePreloads[match.route.file]);
|
|
133
|
+
}
|
|
134
|
+
return preloads.length > 0 ? preloads : void 0;
|
|
135
|
+
}
|
|
136
|
+
function routerLinkHrefs(html) {
|
|
137
|
+
return [...html.matchAll(/<a\b(?=[^>]*\bdata-markless-router-link(?:[\s=>]|$))(?=[^>]*\bhref="([^"]*)")[^>]*>/g)].map((match) => unescapeHtmlAttribute(match[1] ?? ""));
|
|
138
|
+
}
|
|
139
|
+
function parseSameOriginUrl(href, base) {
|
|
140
|
+
try {
|
|
141
|
+
const current = new URL(base);
|
|
142
|
+
const url = new URL(href, current);
|
|
143
|
+
return url.origin === current.origin ? url : void 0;
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function unescapeHtmlAttribute(value) {
|
|
149
|
+
return value.replaceAll(""", "\"").replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
150
|
+
}
|
|
151
|
+
function addModulePreloads(preloads, seen, items) {
|
|
152
|
+
for (const preload of items ?? []) {
|
|
153
|
+
const preloadHref = typeof preload === "string" ? preload : preload.href;
|
|
154
|
+
if (!preloadHref || seen.has(preloadHref)) continue;
|
|
155
|
+
seen.add(preloadHref);
|
|
156
|
+
preloads.push(preload);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function splitLeadingModulePreloadLinks(html) {
|
|
160
|
+
let bodyHtml = html;
|
|
161
|
+
const links = [];
|
|
162
|
+
while (bodyHtml.startsWith("<link ")) {
|
|
163
|
+
const end = bodyHtml.indexOf(">");
|
|
164
|
+
if (end === -1) break;
|
|
165
|
+
const link = bodyHtml.slice(0, end + 1);
|
|
166
|
+
if (!link.includes("rel=\"modulepreload\"")) break;
|
|
167
|
+
links.push(link);
|
|
168
|
+
bodyHtml = bodyHtml.slice(end + 1);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
bodyHtml,
|
|
172
|
+
headHtml: links.join("")
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function renderDocumentShell(documentModule, pageProps, headHtml) {
|
|
176
|
+
const attributes = htmlAttributes(documentModule, pageProps);
|
|
177
|
+
const documentHtml = await renderDocumentModule(documentModule, {
|
|
178
|
+
...pageProps,
|
|
179
|
+
children: DOCUMENT_CHILDREN_PLACEHOLDER
|
|
180
|
+
});
|
|
181
|
+
if (documentHtml !== void 0) return [
|
|
182
|
+
"<!doctype html>",
|
|
183
|
+
`<html${renderAttributes(attributes)}>`,
|
|
184
|
+
insertHeadHtml(documentHtml, headHtml),
|
|
185
|
+
"</html>"
|
|
186
|
+
].join("");
|
|
187
|
+
return [
|
|
188
|
+
"<!doctype html>",
|
|
189
|
+
`<html${renderAttributes(attributes)}>`,
|
|
190
|
+
"<head>",
|
|
191
|
+
"<meta charset=\"utf-8\">",
|
|
192
|
+
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">",
|
|
193
|
+
headHtml,
|
|
194
|
+
"</head>",
|
|
195
|
+
"<body>",
|
|
196
|
+
DOCUMENT_CHILDREN_PLACEHOLDER,
|
|
197
|
+
"</body>",
|
|
198
|
+
"</html>"
|
|
199
|
+
].join("");
|
|
200
|
+
}
|
|
201
|
+
function fillDocumentChildren(documentShell, children) {
|
|
202
|
+
const at = documentShell.indexOf(DOCUMENT_CHILDREN_PLACEHOLDER);
|
|
203
|
+
if (at === -1) return documentShell;
|
|
204
|
+
return documentShell.slice(0, at) + children + documentShell.slice(at + 37);
|
|
205
|
+
}
|
|
206
|
+
async function renderDocumentModule(documentModule, props) {
|
|
207
|
+
return (await documentModule?.default?.renderSsr?.(props))?.html;
|
|
208
|
+
}
|
|
209
|
+
function insertHeadHtml(documentHtml, headHtml) {
|
|
210
|
+
if (!headHtml) return documentHtml;
|
|
211
|
+
const headEnd = documentHtml.indexOf("</head>");
|
|
212
|
+
if (headEnd === -1) return `${headHtml}${documentHtml}`;
|
|
213
|
+
return `${documentHtml.slice(0, headEnd)}${headHtml}${documentHtml.slice(headEnd)}`;
|
|
214
|
+
}
|
|
215
|
+
function renderRouteScript(file) {
|
|
216
|
+
return `<script type="@markless/core/route">${escapeScriptJson({ file })}<\/script>`;
|
|
217
|
+
}
|
|
218
|
+
function renderLinkBridgeScript(resumeEntryPath) {
|
|
219
|
+
return `<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
|
|
220
|
+
const d = document;
|
|
221
|
+
const s = d.currentScript;
|
|
222
|
+
const r = s && s.closest('[data-async-container]');
|
|
223
|
+
if (!r || r.__marklessRouterLinkResumerStarted) return;
|
|
224
|
+
r.__marklessRouterLinkResumerStarted = true;
|
|
225
|
+
const linkAttr = 'data-markless-router-link';
|
|
226
|
+
const replaceAttr = 'data-markless-router-replace';
|
|
227
|
+
const scrollAttr = 'data-markless-router-scroll';
|
|
228
|
+
const anchorFrom = (event) => {
|
|
229
|
+
const target = event.composedPath && event.composedPath()[0] || event.target;
|
|
230
|
+
return target && target.closest ? target.closest('a[href]') : target && target.parentElement && target.parentElement.closest ? target.parentElement.closest('a[href]') : null;
|
|
231
|
+
};
|
|
232
|
+
const sameOrigin = (href) => {
|
|
233
|
+
try {
|
|
234
|
+
const url = new URL(href, location.href);
|
|
235
|
+
return url.origin === location.origin ? url : null;
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
if (location.hash && location.hash.startsWith('#/')) {
|
|
241
|
+
import(${JSON.stringify(resumeEntryPath)}).catch(() => {});
|
|
242
|
+
}
|
|
243
|
+
r.addEventListener('click', async (event) => {
|
|
244
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
|
|
245
|
+
const anchor = anchorFrom(event);
|
|
246
|
+
if (!anchor || !anchor.hasAttribute(linkAttr) || anchor.hasAttribute('download')) return;
|
|
247
|
+
const target = anchor.getAttribute('target');
|
|
248
|
+
if (target && target !== '_self') return;
|
|
249
|
+
if (anchor.relList && anchor.relList.contains('external')) return;
|
|
250
|
+
const url = sameOrigin(anchor.href);
|
|
251
|
+
if (!url) return;
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
try {
|
|
254
|
+
const mod = await import(${JSON.stringify(resumeEntryPath)});
|
|
255
|
+
const navigate = mod.navigateMarklessRouterLink;
|
|
256
|
+
if (typeof navigate === 'function') {
|
|
257
|
+
await navigate({
|
|
258
|
+
href: url.href,
|
|
259
|
+
replace: anchor.hasAttribute(replaceAttr),
|
|
260
|
+
scroll: anchor.getAttribute(scrollAttr) === 'manual' ? 'manual' : undefined,
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
location.assign(url.href);
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
setTimeout(() => { throw error; });
|
|
267
|
+
location.assign(url.href);
|
|
268
|
+
}
|
|
269
|
+
}, true);
|
|
270
|
+
})();`)}<\/script>`;
|
|
271
|
+
}
|
|
272
|
+
function htmlAttributes(documentModule, pageProps) {
|
|
273
|
+
const attributes = documentModule?.__marklessRouterHtmlAttributes?.(pageProps) ?? { lang: "en" };
|
|
274
|
+
const normalized = {};
|
|
275
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
276
|
+
if (value === false || value === null || value === void 0) continue;
|
|
277
|
+
normalized[name] = String(value);
|
|
278
|
+
}
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
function renderAttributes(attributes) {
|
|
282
|
+
return Object.entries(attributes).map(([name, value]) => ` ${name}="${escapeHtml(value)}"`).join("");
|
|
283
|
+
}
|
|
284
|
+
function escapeHtml(value) {
|
|
285
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
286
|
+
}
|
|
287
|
+
function escapeScriptJson(value) {
|
|
288
|
+
return JSON.stringify(value).replaceAll("<", "\\u003c");
|
|
289
|
+
}
|
|
290
|
+
function escapeInlineScript(value) {
|
|
291
|
+
return value.replace(/<\/script/gi, "<\\/script");
|
|
292
|
+
}
|
|
293
|
+
function isNitroApiPathname(pathname) {
|
|
294
|
+
return pathname === "/api" || pathname.startsWith("/api/");
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
export { createServerEntry };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region src/vite/runtime/mdx-route.d.ts
|
|
2
|
+
type MdxRoutePart = {
|
|
3
|
+
readonly kind: 'html';
|
|
4
|
+
readonly elementCount: number;
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: 'component';
|
|
7
|
+
readonly componentIndex: number;
|
|
8
|
+
};
|
|
9
|
+
type MdxRenderOutput = {
|
|
10
|
+
readonly html?: string;
|
|
11
|
+
readonly root?: ChildNode;
|
|
12
|
+
readonly state?: MdxStatePayload;
|
|
13
|
+
readonly view?: MdxViewPayload;
|
|
14
|
+
readonly loadSymbol?: (symbolId: string) => unknown;
|
|
15
|
+
readonly connectRuntime?: (context: unknown) => unknown;
|
|
16
|
+
};
|
|
17
|
+
type MdxChild = {
|
|
18
|
+
readonly componentIndex: number;
|
|
19
|
+
readonly hostPrefix: string;
|
|
20
|
+
readonly symbolPrefix: string;
|
|
21
|
+
readonly output?: MdxRenderOutput;
|
|
22
|
+
};
|
|
23
|
+
type MdxComponentArtifact = {
|
|
24
|
+
readonly renderSsr?: (props?: unknown) => MdxRenderOutput | Promise<MdxRenderOutput>;
|
|
25
|
+
readonly renderCsr?: (props?: unknown) => MdxRenderOutput | Promise<MdxRenderOutput>;
|
|
26
|
+
};
|
|
27
|
+
type MdxSymbolLoader = {
|
|
28
|
+
readonly prefix: string;
|
|
29
|
+
readonly loadSymbol: (symbolId: string) => unknown;
|
|
30
|
+
};
|
|
31
|
+
type MdxStatePayload = {
|
|
32
|
+
readonly version: unknown;
|
|
33
|
+
readonly cells?: readonly unknown[];
|
|
34
|
+
readonly computed?: readonly unknown[];
|
|
35
|
+
readonly sharedDefinitions?: readonly unknown[];
|
|
36
|
+
};
|
|
37
|
+
type MdxLocator = {
|
|
38
|
+
readonly hostNodeId: string;
|
|
39
|
+
readonly index: number;
|
|
40
|
+
readonly [key: string]: unknown;
|
|
41
|
+
};
|
|
42
|
+
type MdxEventRecord = {
|
|
43
|
+
readonly hostNodeId: string;
|
|
44
|
+
readonly symbolIds: readonly string[];
|
|
45
|
+
readonly [key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
type MdxSymbolRecord = {
|
|
48
|
+
readonly hostNodeId: string;
|
|
49
|
+
readonly symbolId?: string;
|
|
50
|
+
readonly [key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
type MdxViewPayload = {
|
|
53
|
+
readonly version: unknown;
|
|
54
|
+
readonly locators?: readonly MdxLocator[];
|
|
55
|
+
readonly events?: readonly MdxEventRecord[];
|
|
56
|
+
readonly domUpdates?: readonly MdxSymbolRecord[];
|
|
57
|
+
readonly behaviors?: readonly MdxSymbolRecord[];
|
|
58
|
+
readonly elementHandles?: readonly MdxSymbolRecord[];
|
|
59
|
+
readonly asyncBoundaries?: readonly unknown[];
|
|
60
|
+
};
|
|
61
|
+
declare function renderMdxChild(children: MdxChild[], component: MdxComponentArtifact, props: unknown, child: Omit<MdxChild, 'output'>): Promise<string>;
|
|
62
|
+
declare function rootFromMdxHtml(html: string): Element;
|
|
63
|
+
declare function replaceMdxChild(root: ParentNode, index: number, child: ChildNode | undefined): void;
|
|
64
|
+
declare function composeMdxState(children: readonly MdxChild[]): MdxStatePayload | undefined;
|
|
65
|
+
declare function composeMdxView(parts: readonly MdxRoutePart[], children: readonly MdxChild[], initialElementOffset: number): MdxViewPayload | undefined;
|
|
66
|
+
declare function loadMdxSymbol(symbolId: string, children: readonly MdxChild[], loaders: readonly MdxSymbolLoader[]): unknown;
|
|
67
|
+
//#endregion
|
|
68
|
+
export { MdxChild, MdxComponentArtifact, MdxRenderOutput, MdxRoutePart, MdxSymbolLoader, composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//#region src/vite/runtime/mdx-route.ts
|
|
2
|
+
async function renderMdxChild(children, component, props, child) {
|
|
3
|
+
const output = await component.renderSsr?.(props);
|
|
4
|
+
if (output) children.push({
|
|
5
|
+
...child,
|
|
6
|
+
output
|
|
7
|
+
});
|
|
8
|
+
return output?.html ?? "";
|
|
9
|
+
}
|
|
10
|
+
function rootFromMdxHtml(html) {
|
|
11
|
+
const template = document.createElement("template");
|
|
12
|
+
template.innerHTML = html;
|
|
13
|
+
const root = template.content.firstElementChild;
|
|
14
|
+
if (!root) throw new Error("Markless Router MDX render did not produce a root element.");
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
function replaceMdxChild(root, index, child) {
|
|
18
|
+
const placeholder = root.querySelector?.(`[data-markless-mdx-child="${index}"]`);
|
|
19
|
+
if (placeholder && child) {
|
|
20
|
+
placeholder.replaceWith(child);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
placeholder?.remove();
|
|
24
|
+
}
|
|
25
|
+
function composeMdxState(children) {
|
|
26
|
+
const childStates = children.map((child) => child.output?.state).filter(isDefined);
|
|
27
|
+
if (childStates.length === 0) return;
|
|
28
|
+
return {
|
|
29
|
+
version: childStates[0].version,
|
|
30
|
+
cells: childStates.flatMap((state) => state.cells ?? []),
|
|
31
|
+
computed: childStates.flatMap((state) => state.computed ?? []),
|
|
32
|
+
...childStates.some((state) => state.sharedDefinitions?.length) ? { sharedDefinitions: childStates.flatMap((state) => state.sharedDefinitions ?? []) } : {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function composeMdxView(parts, children, initialElementOffset) {
|
|
36
|
+
const childViews = children.map((child) => ({
|
|
37
|
+
...child,
|
|
38
|
+
view: child.output?.view,
|
|
39
|
+
hostCount: child.output?.view?.locators?.length ?? 0
|
|
40
|
+
})).filter((child) => Boolean(child.view));
|
|
41
|
+
if (childViews.length === 0) return;
|
|
42
|
+
const childByIndex = new Map(childViews.map((child) => [child.componentIndex, child]));
|
|
43
|
+
const locators = [];
|
|
44
|
+
const events = [];
|
|
45
|
+
const domUpdates = [];
|
|
46
|
+
const behaviors = [];
|
|
47
|
+
const elementHandles = [];
|
|
48
|
+
let elementOffset = initialElementOffset;
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
if (part.kind === "html") {
|
|
51
|
+
elementOffset += part.elementCount;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const child = childByIndex.get(part.componentIndex);
|
|
55
|
+
if (!child) continue;
|
|
56
|
+
appendMdxChildView({
|
|
57
|
+
child,
|
|
58
|
+
elementOffset,
|
|
59
|
+
locators,
|
|
60
|
+
events,
|
|
61
|
+
domUpdates,
|
|
62
|
+
behaviors,
|
|
63
|
+
elementHandles
|
|
64
|
+
});
|
|
65
|
+
elementOffset += child.hostCount;
|
|
66
|
+
}
|
|
67
|
+
locators.sort((a, b) => a.index - b.index);
|
|
68
|
+
return {
|
|
69
|
+
version: childViews[0].view.version,
|
|
70
|
+
locators,
|
|
71
|
+
events,
|
|
72
|
+
domUpdates,
|
|
73
|
+
behaviors,
|
|
74
|
+
elementHandles,
|
|
75
|
+
asyncBoundaries: []
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function loadMdxSymbol(symbolId, children, loaders) {
|
|
79
|
+
for (const child of children) if (symbolId.startsWith(child.symbolPrefix) && child.output?.loadSymbol) return child.output.loadSymbol(symbolId.slice(child.symbolPrefix.length));
|
|
80
|
+
for (const loader of loaders) if (symbolId.startsWith(loader.prefix)) return loader.loadSymbol(symbolId);
|
|
81
|
+
return Promise.reject(/* @__PURE__ */ new Error(`Unknown Markless MDX symbol ${symbolId}`));
|
|
82
|
+
}
|
|
83
|
+
function appendMdxChildView(context) {
|
|
84
|
+
const childView = context.child.view;
|
|
85
|
+
for (const locator of childView.locators ?? []) context.locators.push({
|
|
86
|
+
...locator,
|
|
87
|
+
hostNodeId: context.child.hostPrefix + locator.hostNodeId,
|
|
88
|
+
index: context.elementOffset + locator.index
|
|
89
|
+
});
|
|
90
|
+
for (const event of childView.events ?? []) context.events.push({
|
|
91
|
+
...event,
|
|
92
|
+
hostNodeId: context.child.hostPrefix + event.hostNodeId,
|
|
93
|
+
symbolIds: event.symbolIds.map((symbolId) => context.child.symbolPrefix + symbolId)
|
|
94
|
+
});
|
|
95
|
+
for (const update of childView.domUpdates ?? []) context.domUpdates.push(prefixMdxSymbolRecord(update, context.child));
|
|
96
|
+
for (const behavior of childView.behaviors ?? []) context.behaviors.push(prefixMdxSymbolRecord(behavior, context.child));
|
|
97
|
+
for (const handle of childView.elementHandles ?? []) context.elementHandles.push({
|
|
98
|
+
...handle,
|
|
99
|
+
hostNodeId: context.child.hostPrefix + handle.hostNodeId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function prefixMdxSymbolRecord(record, child) {
|
|
103
|
+
return {
|
|
104
|
+
...record,
|
|
105
|
+
hostNodeId: child.hostPrefix + record.hostNodeId,
|
|
106
|
+
...record.symbolId ? { symbolId: child.symbolPrefix + record.symbolId } : {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function isDefined(value) {
|
|
110
|
+
return value !== void 0;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { composeMdxState, composeMdxView, loadMdxSymbol, renderMdxChild, replaceMdxChild, rootFromMdxHtml };
|