@markless/router 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/debug-channel-C7v9u5SM.js +250 -0
- package/dist/index.js +4 -2
- package/dist/typescript-plugin.cjs +227 -0
- package/dist/typescript-plugin.js +92 -2
- package/dist/vite/runtime/create-route-discovery.js +1 -1
- package/dist/vite/runtime/create-server-entry.d.ts +2 -0
- package/dist/vite/runtime/create-server-entry.js +91 -21
- package/dist/vite.js +311 -28
- package/package.json +20 -6
- package/src/vite/entries/server-entry.ts +3 -0
- /package/dist/{request-files-DTkeaKxU.js → request-files-C-0LLSsx.js} +0 -0
- /package/dist/{route-manifest-OTaWQ_TC.js → route-manifest-DPgGIyrS.js} +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as __marklessDebugBootstrapSource } from "../../debug-channel-C7v9u5SM.js";
|
|
2
|
+
import { i as normalizeRouteFileId, n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-DPgGIyrS.js";
|
|
2
3
|
import { renderToString } from "@markless/web/render-to-string";
|
|
3
4
|
import { renderToStream } from "@markless/web/render-to-stream";
|
|
5
|
+
import { normalizeMarklessDevError, renderMarklessDevErrorDocument } from "@markless/bundler/dev-error";
|
|
4
6
|
//#region src/vite/runtime/create-server-entry.ts
|
|
5
7
|
const DOCUMENT_CHILDREN_PLACEHOLDER = "__markless_router_document_children__";
|
|
6
8
|
function createServerEntry(options) {
|
|
7
9
|
const manifest = buildRouteManifestFromFileIds(options.routeFileIds);
|
|
10
|
+
assertCurrentRouteAssets(options);
|
|
8
11
|
async function fetch(request) {
|
|
9
12
|
const url = new URL(request.url);
|
|
10
13
|
if (isNitroApiPathname(url.pathname)) return new Response("Not found", { status: 404 });
|
|
@@ -14,7 +17,37 @@ function createServerEntry(options) {
|
|
|
14
17
|
return await renderPage(url, match.route.file, match.params, 200);
|
|
15
18
|
} catch (error) {
|
|
16
19
|
console.error("[markless-router] page render failed:", error);
|
|
17
|
-
return
|
|
20
|
+
if (options.dev) return renderDevelopmentError(url, error);
|
|
21
|
+
try {
|
|
22
|
+
return await renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
|
|
23
|
+
} catch (errorPageError) {
|
|
24
|
+
console.error("[markless-router] error page render failed:", errorPageError);
|
|
25
|
+
return new Response("Internal Server Error", {
|
|
26
|
+
status: 500,
|
|
27
|
+
headers: { "content-type": "text/plain;charset=utf-8" }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function renderDevelopmentError(url, error) {
|
|
33
|
+
const headers = {
|
|
34
|
+
"cache-control": "no-store",
|
|
35
|
+
"content-type": "text/html;charset=utf-8"
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const payload = normalizeMarklessDevError(error, { id: `navigation:${url.pathname}` });
|
|
39
|
+
return new Response(renderMarklessDevErrorDocument(payload), {
|
|
40
|
+
status: 500,
|
|
41
|
+
headers
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return new Response(escapeHtml(flattenError(error)), {
|
|
45
|
+
status: 500,
|
|
46
|
+
headers: {
|
|
47
|
+
...headers,
|
|
48
|
+
"content-type": "text/plain;charset=utf-8"
|
|
49
|
+
}
|
|
50
|
+
});
|
|
18
51
|
}
|
|
19
52
|
}
|
|
20
53
|
async function renderStatusPage(url, file, status, fallbackText) {
|
|
@@ -46,14 +79,14 @@ function createServerEntry(options) {
|
|
|
46
79
|
headers
|
|
47
80
|
});
|
|
48
81
|
}
|
|
49
|
-
const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath);
|
|
82
|
+
const pageArtifact = routedPageArtifact(renderSsr, baseArtifact, pageProps, file, options.navigationEntryPath, options.routeStylesheets?.[file]);
|
|
50
83
|
const renderOptions = {
|
|
51
84
|
props: pageProps,
|
|
52
85
|
resumeModuleUrl: options.resumeEntryPath ?? baseArtifact?.resumeModuleUrl,
|
|
53
86
|
modulePreloads: (html) => modulePreloadsForPage(file, html, pageProps.url.href, manifest, options.routeModulePreloads, options.routeSsrModulePreloads)
|
|
54
87
|
};
|
|
55
88
|
if (options.render === "blocking") {
|
|
56
|
-
const pageHtml =
|
|
89
|
+
const pageHtml = splitLeadingHeadHtml(await renderToString(pageArtifact, renderOptions));
|
|
57
90
|
const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
|
|
58
91
|
return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
|
|
59
92
|
status,
|
|
@@ -61,7 +94,7 @@ function createServerEntry(options) {
|
|
|
61
94
|
});
|
|
62
95
|
}
|
|
63
96
|
const stream = await renderToStream(pageArtifact, renderOptions);
|
|
64
|
-
const pageHtml =
|
|
97
|
+
const pageHtml = splitLeadingHeadHtml(stream.shell);
|
|
65
98
|
const shell = await renderDocumentShell(documentModule, pageProps, pageHtml.headHtml);
|
|
66
99
|
if (stream.pendingArmCount === 0) return new Response(fillDocumentChildren(shell, pageHtml.bodyHtml), {
|
|
67
100
|
status,
|
|
@@ -90,9 +123,38 @@ function createServerEntry(options) {
|
|
|
90
123
|
}
|
|
91
124
|
return { fetch };
|
|
92
125
|
}
|
|
93
|
-
function
|
|
126
|
+
function flattenError(error) {
|
|
127
|
+
if (error instanceof Error) return error.message;
|
|
128
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") return error.message;
|
|
129
|
+
return String(error);
|
|
130
|
+
}
|
|
131
|
+
function assertCurrentRouteAssets(options) {
|
|
132
|
+
if (options.routeStylesheets === void 0) return;
|
|
133
|
+
const routeMaps = [
|
|
134
|
+
["navigation", options.routeModulePreloads ?? {}],
|
|
135
|
+
["SSR", options.routeSsrModulePreloads ?? {}],
|
|
136
|
+
["style", options.routeStylesheets]
|
|
137
|
+
];
|
|
138
|
+
const expected = [...new Set(options.routeFileIds.map(normalizeRouteFileId))].filter((file) => /^pages\/.+\.(?:tsrx|mdx)$/.test(file)).toSorted();
|
|
139
|
+
for (const [label, routes] of routeMaps) {
|
|
140
|
+
const actual = Object.keys(routes).toSorted();
|
|
141
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`Markless Router ${label} client-asset routes are stale; expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}. Rebuild the client environment before the server environment.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath, stylesheetHrefs) {
|
|
145
|
+
const headInjections = [...baseArtifact?.headInjections ?? [], ...(stylesheetHrefs ?? []).map((href) => ({
|
|
146
|
+
tag: "link",
|
|
147
|
+
location: "head",
|
|
148
|
+
attributes: {
|
|
149
|
+
rel: "stylesheet",
|
|
150
|
+
href
|
|
151
|
+
}
|
|
152
|
+
}))];
|
|
94
153
|
return {
|
|
95
154
|
resumeModuleUrl: baseArtifact?.resumeModuleUrl,
|
|
155
|
+
inlineResumerSources: baseArtifact?.inlineResumerSources,
|
|
156
|
+
storageSeeds: baseArtifact?.storageSeeds,
|
|
157
|
+
...headInjections.length > 0 ? { headInjections } : {},
|
|
96
158
|
async renderSsr(renderProps, renderContext) {
|
|
97
159
|
const output = await renderSsr(renderProps ?? pageProps, renderContext);
|
|
98
160
|
if (!output) return { html: "" };
|
|
@@ -156,20 +218,15 @@ function addModulePreloads(preloads, seen, items) {
|
|
|
156
218
|
preloads.push(preload);
|
|
157
219
|
}
|
|
158
220
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
}
|
|
221
|
+
function splitLeadingHeadHtml(html) {
|
|
222
|
+
const containerStart = html.indexOf("<div data-async-container");
|
|
223
|
+
if (containerStart <= 0) return {
|
|
224
|
+
bodyHtml: html,
|
|
225
|
+
headHtml: ""
|
|
226
|
+
};
|
|
170
227
|
return {
|
|
171
|
-
bodyHtml,
|
|
172
|
-
headHtml:
|
|
228
|
+
bodyHtml: html.slice(containerStart),
|
|
229
|
+
headHtml: html.slice(0, containerStart)
|
|
173
230
|
};
|
|
174
231
|
}
|
|
175
232
|
async function renderDocumentShell(documentModule, pageProps, headHtml) {
|
|
@@ -216,7 +273,20 @@ function renderRouteScript(file) {
|
|
|
216
273
|
return `<script type="@markless/core/route">${escapeScriptJson({ file })}<\/script>`;
|
|
217
274
|
}
|
|
218
275
|
function renderLinkBridgeScript(resumeEntryPath) {
|
|
219
|
-
|
|
276
|
+
const debugBootstrap = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `<script data-markless-router-debug-bootstrap>${escapeInlineScript(`(() => {
|
|
277
|
+
const d = document;
|
|
278
|
+
const s = d.currentScript;
|
|
279
|
+
const r = s && s.closest('[data-async-container]');
|
|
280
|
+
if (!r) return;
|
|
281
|
+
try { globalThis[Symbol.for('markless.debug.channel.v1.bootstrap')] = ${__marklessDebugBootstrapSource()}(r, 'ssr-inline', false); } catch {}
|
|
282
|
+
})();`)}<\/script>` : "";
|
|
283
|
+
const debugRegistration = typeof __MARKLESS_DEBUG_ENABLED__ !== "undefined" && __MARKLESS_DEBUG_ENABLED__ ? `
|
|
284
|
+
try {
|
|
285
|
+
const key = Symbol.for('markless.debug.channel.v1.bootstrap');
|
|
286
|
+
const md = globalThis[key];
|
|
287
|
+
if (md) { md.router('ssr-link-bridge'); md.activate(); }
|
|
288
|
+
} catch {}` : "";
|
|
289
|
+
return `${debugBootstrap}<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
|
|
220
290
|
const d = document;
|
|
221
291
|
const s = d.currentScript;
|
|
222
292
|
const r = s && s.closest('[data-async-container]');
|
|
@@ -266,7 +336,7 @@ function renderLinkBridgeScript(resumeEntryPath) {
|
|
|
266
336
|
setTimeout(() => { throw error; });
|
|
267
337
|
location.assign(url.href);
|
|
268
338
|
}
|
|
269
|
-
}, true)
|
|
339
|
+
}, true);${debugRegistration}
|
|
270
340
|
})();`)}<\/script>`;
|
|
271
341
|
}
|
|
272
342
|
function htmlAttributes(documentModule, pageProps) {
|
package/dist/vite.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { t as buildRouteManifestFromFileIds } from "./route-manifest-
|
|
2
|
-
import { r as transformRequestFileSource } from "./request-files-
|
|
3
|
-
import { dirname, extname, isAbsolute, join, normalize, relative } from "pathe";
|
|
1
|
+
import { t as buildRouteManifestFromFileIds } from "./route-manifest-DPgGIyrS.js";
|
|
2
|
+
import { r as transformRequestFileSource } from "./request-files-C-0LLSsx.js";
|
|
3
|
+
import { dirname, extname, isAbsolute, join, normalize, relative, resolve } from "pathe";
|
|
4
4
|
import { decodePath, joinURL, parsePath, parseQuery, parseURL, stringifyQuery, withLeadingSlash, withoutLeadingSlash } from "ufo";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { nitro } from "nitro/vite";
|
|
7
7
|
import { sortUserPlugins } from "vite";
|
|
8
8
|
import { symbolVirtualModuleSourceFile } from "@markless/bundler/preload";
|
|
9
9
|
import { parseModule } from "@tsrx/core";
|
|
10
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
10
11
|
import { toHtml } from "hast-util-to-html";
|
|
11
12
|
import { HastReader, createMdxHastHandle, defineHastPlugin, dropHandle, materializeHastTree, resolveHastSubscriptions, serializeHandle, visitHastHandle } from "satteri";
|
|
12
13
|
//#region src/route-types.ts
|
|
@@ -371,6 +372,169 @@ function fail$1(message) {
|
|
|
371
372
|
throw new Error(message);
|
|
372
373
|
}
|
|
373
374
|
//#endregion
|
|
375
|
+
//#region src/vite/client-assets-manifest.ts
|
|
376
|
+
const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST = ".vite/markless-router-client-manifest.json";
|
|
377
|
+
const MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION = 1;
|
|
378
|
+
const VALID_ROUTE_FILE = /^pages\/.+\.(?:tsrx|mdx)$/;
|
|
379
|
+
const ABSOLUTE_OR_PROTOCOL_RELATIVE_URL = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
|
|
380
|
+
function createClientAssetsManifest(input) {
|
|
381
|
+
return {
|
|
382
|
+
version: MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION,
|
|
383
|
+
base: input.base,
|
|
384
|
+
entries: {
|
|
385
|
+
resume: input.resumeEntry,
|
|
386
|
+
navigation: input.navigationEntry
|
|
387
|
+
},
|
|
388
|
+
routes: {
|
|
389
|
+
navigation: sortRouteMap(input.routes.navigation),
|
|
390
|
+
ssr: sortRouteMap(input.routes.ssr),
|
|
391
|
+
styles: sortRouteMap(input.routes.styles)
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async function removeClientAssetsManifest(clientOutDir) {
|
|
396
|
+
const path = clientAssetsManifestPath(clientOutDir);
|
|
397
|
+
await Promise.all([rm(path, { force: true }), rm(clientAssetsManifestTemporaryPath(clientOutDir), { force: true })]);
|
|
398
|
+
}
|
|
399
|
+
async function writeClientAssetsManifest(clientOutDir, manifest) {
|
|
400
|
+
const validated = await validateClientAssetsManifest(manifest, manifest.base, clientOutDir);
|
|
401
|
+
const path = clientAssetsManifestPath(clientOutDir);
|
|
402
|
+
const temporaryPath = clientAssetsManifestTemporaryPath(clientOutDir);
|
|
403
|
+
await mkdir(dirname(path), { recursive: true });
|
|
404
|
+
await writeFile(temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, "utf8");
|
|
405
|
+
await rename(temporaryPath, path);
|
|
406
|
+
}
|
|
407
|
+
async function readClientAssetsManifest(clientOutDir, base) {
|
|
408
|
+
const path = clientAssetsManifestPath(clientOutDir);
|
|
409
|
+
let source;
|
|
410
|
+
try {
|
|
411
|
+
source = await readFile(path, "utf8");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
throw clientAssetsManifestError(`is missing at ${path}. Build the client environment before the server environment`, error);
|
|
414
|
+
}
|
|
415
|
+
let parsed;
|
|
416
|
+
try {
|
|
417
|
+
parsed = JSON.parse(source);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
throw clientAssetsManifestError(`is malformed at ${path}`, error);
|
|
420
|
+
}
|
|
421
|
+
return validateClientAssetsManifest(parsed, base, clientOutDir);
|
|
422
|
+
}
|
|
423
|
+
function clientAssetFileName(href, base) {
|
|
424
|
+
const dummyOrigin = "https://markless-router.invalid/";
|
|
425
|
+
if (hasControlCharacter(href) || !ABSOLUTE_OR_PROTOCOL_RELATIVE_URL.test(base) && ABSOLUTE_OR_PROTOCOL_RELATIVE_URL.test(href)) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
|
|
426
|
+
let baseUrl;
|
|
427
|
+
let assetUrl;
|
|
428
|
+
try {
|
|
429
|
+
baseUrl = new URL(base || "/", dummyOrigin);
|
|
430
|
+
assetUrl = new URL(href, baseUrl);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw clientAssetsManifestError(`contains an invalid asset URL ${JSON.stringify(href)}`, error);
|
|
433
|
+
}
|
|
434
|
+
if (assetUrl.origin !== baseUrl.origin || assetUrl.search || assetUrl.hash) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
|
|
435
|
+
const basePath = baseUrl.pathname.endsWith("/") ? baseUrl.pathname : `${baseUrl.pathname}/`;
|
|
436
|
+
if (!assetUrl.pathname.startsWith(basePath)) throw clientAssetsManifestError(`contains an asset URL outside base ${JSON.stringify(base)}: ${JSON.stringify(href)}`);
|
|
437
|
+
let fileName;
|
|
438
|
+
try {
|
|
439
|
+
fileName = decodeURIComponent(assetUrl.pathname.slice(basePath.length));
|
|
440
|
+
} catch (error) {
|
|
441
|
+
throw clientAssetsManifestError(`contains an invalid encoded asset URL ${JSON.stringify(href)}`, error);
|
|
442
|
+
}
|
|
443
|
+
const normalized = normalize(fileName);
|
|
444
|
+
if (fileName === "" || hasControlCharacter(fileName) || fileName.includes("\\") || isAbsolute(fileName) || normalized !== fileName || normalized === ".." || normalized.startsWith("../")) throw clientAssetsManifestError(`contains an unsafe asset path ${JSON.stringify(href)}`);
|
|
445
|
+
if (href !== joinURL(base, fileName)) throw clientAssetsManifestError(`contains a non-canonical asset URL ${JSON.stringify(href)} for base ${JSON.stringify(base)}`);
|
|
446
|
+
return fileName;
|
|
447
|
+
}
|
|
448
|
+
function clientAssetsManifestPath(clientOutDir) {
|
|
449
|
+
return join(clientOutDir, MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST);
|
|
450
|
+
}
|
|
451
|
+
function clientAssetsManifestTemporaryPath(clientOutDir) {
|
|
452
|
+
return `${clientAssetsManifestPath(clientOutDir)}.tmp`;
|
|
453
|
+
}
|
|
454
|
+
async function validateClientAssetsManifest(input, base, clientOutDir) {
|
|
455
|
+
if (!isRecord$1(input)) throw clientAssetsManifestError("must be a JSON object");
|
|
456
|
+
if (input.version !== MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION) throw clientAssetsManifestError(`has unsupported version ${JSON.stringify(input.version)}; expected ${MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION}`);
|
|
457
|
+
if (input.base !== base) throw clientAssetsManifestError(`was built for base ${JSON.stringify(input.base)}; expected ${JSON.stringify(base)}`);
|
|
458
|
+
if (!isRecord$1(input.entries)) throw clientAssetsManifestError("has invalid entries");
|
|
459
|
+
if (!isRecord$1(input.routes)) throw clientAssetsManifestError("has invalid route maps");
|
|
460
|
+
const entries = {
|
|
461
|
+
resume: requiredString(input.entries.resume, "entries.resume"),
|
|
462
|
+
navigation: requiredString(input.entries.navigation, "entries.navigation")
|
|
463
|
+
};
|
|
464
|
+
const routes = {
|
|
465
|
+
navigation: routeMap(input.routes.navigation, "routes.navigation"),
|
|
466
|
+
ssr: routeMap(input.routes.ssr, "routes.ssr"),
|
|
467
|
+
styles: routeMap(input.routes.styles, "routes.styles")
|
|
468
|
+
};
|
|
469
|
+
assertMatchingRouteKeys(routes);
|
|
470
|
+
const hrefs = new Set([
|
|
471
|
+
entries.resume,
|
|
472
|
+
entries.navigation,
|
|
473
|
+
...Object.values(routes.navigation).flat(),
|
|
474
|
+
...Object.values(routes.ssr).flat(),
|
|
475
|
+
...Object.values(routes.styles).flat()
|
|
476
|
+
]);
|
|
477
|
+
for (const href of hrefs) {
|
|
478
|
+
const assetPath = join(clientOutDir, clientAssetFileName(href, base));
|
|
479
|
+
const relativeAssetPath = relative(clientOutDir, assetPath);
|
|
480
|
+
if (relativeAssetPath === ".." || relativeAssetPath.startsWith("../") || isAbsolute(relativeAssetPath)) throw clientAssetsManifestError(`contains an unsafe asset path ${JSON.stringify(href)}`);
|
|
481
|
+
try {
|
|
482
|
+
if (!(await stat(assetPath)).isFile()) throw new Error("not a file");
|
|
483
|
+
} catch (error) {
|
|
484
|
+
throw clientAssetsManifestError(`references missing client asset ${JSON.stringify(href)}`, error);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
version: MARKLESS_ROUTER_CLIENT_ASSETS_MANIFEST_VERSION,
|
|
489
|
+
base,
|
|
490
|
+
entries,
|
|
491
|
+
routes
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function routeMap(input, label) {
|
|
495
|
+
if (!isRecord$1(input)) throw clientAssetsManifestError(`has invalid ${label}`);
|
|
496
|
+
const routes = {};
|
|
497
|
+
for (const [routeFile, hrefs] of Object.entries(input)) {
|
|
498
|
+
let decodedRouteFile;
|
|
499
|
+
try {
|
|
500
|
+
decodedRouteFile = decodeURIComponent(routeFile);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw clientAssetsManifestError(`contains invalid route key ${JSON.stringify(routeFile)}`, error);
|
|
503
|
+
}
|
|
504
|
+
if (!VALID_ROUTE_FILE.test(routeFile) || hasControlCharacter(routeFile) || decodedRouteFile !== routeFile || routeFile.includes("\\") || normalize(routeFile) !== routeFile) throw clientAssetsManifestError(`contains invalid route key ${JSON.stringify(routeFile)}`);
|
|
505
|
+
if (!Array.isArray(hrefs) || !hrefs.every((href) => typeof href === "string")) throw clientAssetsManifestError(`has invalid assets for route ${JSON.stringify(routeFile)}`);
|
|
506
|
+
routes[routeFile] = hrefs;
|
|
507
|
+
}
|
|
508
|
+
return routes;
|
|
509
|
+
}
|
|
510
|
+
function assertMatchingRouteKeys(routes) {
|
|
511
|
+
const navigationKeys = Object.keys(routes.navigation).toSorted();
|
|
512
|
+
for (const [label, routeMap] of [["SSR", routes.ssr], ["style", routes.styles]]) {
|
|
513
|
+
const keys = Object.keys(routeMap).toSorted();
|
|
514
|
+
if (JSON.stringify(keys) !== JSON.stringify(navigationKeys)) throw clientAssetsManifestError(`${label} route keys do not match navigation route keys`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function sortRouteMap(routes) {
|
|
518
|
+
return Object.fromEntries(Object.entries(routes).toSorted(([left], [right]) => left.localeCompare(right)).map(([routeFile, hrefs]) => [routeFile, [...hrefs]]));
|
|
519
|
+
}
|
|
520
|
+
function requiredString(input, label) {
|
|
521
|
+
if (typeof input !== "string" || input === "") throw clientAssetsManifestError(`has invalid ${label}`);
|
|
522
|
+
return input;
|
|
523
|
+
}
|
|
524
|
+
function hasControlCharacter(input) {
|
|
525
|
+
for (let index = 0; index < input.length; index++) {
|
|
526
|
+
const code = input.charCodeAt(index);
|
|
527
|
+
if (code <= 31 || code === 127) return true;
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
function isRecord$1(input) {
|
|
532
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
533
|
+
}
|
|
534
|
+
function clientAssetsManifestError(message, cause) {
|
|
535
|
+
return new Error(`Markless Router client-assets manifest ${message}.`, { cause });
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
374
538
|
//#region src/vite/html-transform.ts
|
|
375
539
|
const HELPER = "__marklessRouterHtmlAttributes";
|
|
376
540
|
const SAFE_GLOBALS = new Set([
|
|
@@ -912,9 +1076,16 @@ function devSourceModuleRequestPlugin() {
|
|
|
912
1076
|
};
|
|
913
1077
|
}
|
|
914
1078
|
function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry, routePreloads) {
|
|
1079
|
+
let clientOutDir = "";
|
|
1080
|
+
let clientEnvironmentName = "";
|
|
1081
|
+
let clientEnvironmentConfig;
|
|
1082
|
+
let serverEnvironmentConfig;
|
|
1083
|
+
let productionBuild = false;
|
|
1084
|
+
let pendingClientAssets;
|
|
915
1085
|
return {
|
|
916
1086
|
name: "markless-router:vite",
|
|
917
1087
|
enforce: "pre",
|
|
1088
|
+
sharedDuringBuild: true,
|
|
918
1089
|
config(config) {
|
|
919
1090
|
throwIfUserAddedNitro(config.plugins, nitroPluginsFromRouter);
|
|
920
1091
|
config.environments ??= {};
|
|
@@ -932,6 +1103,16 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
|
|
|
932
1103
|
};
|
|
933
1104
|
},
|
|
934
1105
|
configResolved(config) {
|
|
1106
|
+
productionBuild = config.command === "build";
|
|
1107
|
+
if (productionBuild) {
|
|
1108
|
+
const clientEnvironments = Object.entries(config.environments).filter(([, environment]) => environment.consumer === "client");
|
|
1109
|
+
if (clientEnvironments.length !== 1) throw new Error(`Markless Router requires exactly one client build environment; found ${clientEnvironments.length}.`);
|
|
1110
|
+
const [name, environment] = clientEnvironments[0];
|
|
1111
|
+
clientEnvironmentName = name;
|
|
1112
|
+
clientEnvironmentConfig = environment;
|
|
1113
|
+
serverEnvironmentConfig = config.environments.ssr;
|
|
1114
|
+
clientOutDir = resolve(config.root, environment.build.outDir);
|
|
1115
|
+
}
|
|
935
1116
|
for (const entry of [
|
|
936
1117
|
resumeEntry,
|
|
937
1118
|
navigationEntry,
|
|
@@ -942,20 +1123,65 @@ function routerConfigPlugin(nitroPluginsFromRouter, resumeEntry, navigationEntry
|
|
|
942
1123
|
configEnvironment(_name, config) {
|
|
943
1124
|
configureRouteInputs(config);
|
|
944
1125
|
},
|
|
945
|
-
|
|
946
|
-
if (
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1126
|
+
async buildStart() {
|
|
1127
|
+
if (!productionBuild) return;
|
|
1128
|
+
if (!clientOutDir) throw new Error("Markless Router could not resolve the client output directory for its client-assets manifest.");
|
|
1129
|
+
const environment = this.environment;
|
|
1130
|
+
const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
|
|
1131
|
+
const isRouterServerEnvironment = environment?.name ? environment.name === "ssr" : environment?.config === serverEnvironmentConfig;
|
|
1132
|
+
if (isClientEnvironment) {
|
|
1133
|
+
pendingClientAssets = void 0;
|
|
1134
|
+
resumeEntry.fileName = void 0;
|
|
1135
|
+
navigationEntry.fileName = void 0;
|
|
1136
|
+
routePreloads.routes = {
|
|
1137
|
+
navigation: {},
|
|
1138
|
+
ssr: {},
|
|
1139
|
+
styles: {}
|
|
1140
|
+
};
|
|
1141
|
+
routePreloads.persisted = false;
|
|
1142
|
+
await removeClientAssetsManifest(clientOutDir);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (!isRouterServerEnvironment) return;
|
|
1146
|
+
const manifest = await readClientAssetsManifest(clientOutDir, routePreloads.base);
|
|
1147
|
+
resumeEntry.fileName = clientAssetFileName(manifest.entries.resume, manifest.base);
|
|
1148
|
+
navigationEntry.fileName = clientAssetFileName(manifest.entries.navigation, manifest.base);
|
|
1149
|
+
routePreloads.routes = manifest.routes;
|
|
1150
|
+
routePreloads.persisted = true;
|
|
1151
|
+
},
|
|
1152
|
+
generateBundle: {
|
|
1153
|
+
order: "post",
|
|
1154
|
+
handler(_options, bundle) {
|
|
1155
|
+
if (this.environment?.config.consumer !== "client") return;
|
|
1156
|
+
const resumeChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, RESUME_ENTRY_ORIGIN));
|
|
1157
|
+
if (resumeChunk) resumeEntry.fileName = resumeChunk.fileName;
|
|
1158
|
+
const navigationChunk = Object.values(bundle).find((item) => item.type === "chunk" && isVirtualEntryChunk(item, NAVIGATION_ENTRY_ORIGIN));
|
|
1159
|
+
if (navigationChunk) navigationEntry.fileName = navigationChunk.fileName;
|
|
1160
|
+
routePreloads.routes = routeModulePreloadsFromBundle({
|
|
1161
|
+
base: routePreloads.base,
|
|
1162
|
+
bundle,
|
|
1163
|
+
navigationChunk,
|
|
1164
|
+
resumeChunk,
|
|
1165
|
+
root: routePreloads.root
|
|
1166
|
+
});
|
|
1167
|
+
patchRoutePreloadsInBundle(bundle, routePreloads.routes);
|
|
1168
|
+
if (productionBuild) {
|
|
1169
|
+
if (!resumeEntry.fileName || !navigationEntry.fileName) throw new Error("Markless Router client build did not emit its resume and navigation entries.");
|
|
1170
|
+
pendingClientAssets = createClientAssetsManifest({
|
|
1171
|
+
base: routePreloads.base,
|
|
1172
|
+
resumeEntry: joinURL(routePreloads.base, resumeEntry.fileName),
|
|
1173
|
+
navigationEntry: joinURL(routePreloads.base, navigationEntry.fileName),
|
|
1174
|
+
routes: routePreloads.routes
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
async writeBundle() {
|
|
1180
|
+
const environment = this.environment;
|
|
1181
|
+
const isClientEnvironment = environment?.name ? environment.name === clientEnvironmentName : environment?.config === clientEnvironmentConfig;
|
|
1182
|
+
if (!productionBuild || !isClientEnvironment) return;
|
|
1183
|
+
if (!pendingClientAssets) throw new Error("Markless Router client build finished without a client-assets manifest.");
|
|
1184
|
+
await writeClientAssetsManifest(clientOutDir, pendingClientAssets);
|
|
959
1185
|
}
|
|
960
1186
|
};
|
|
961
1187
|
}
|
|
@@ -1083,10 +1309,12 @@ function resumeEntryState() {
|
|
|
1083
1309
|
function routePreloadState() {
|
|
1084
1310
|
return {
|
|
1085
1311
|
base: "/",
|
|
1312
|
+
persisted: false,
|
|
1086
1313
|
root: ".",
|
|
1087
1314
|
routes: {
|
|
1088
1315
|
navigation: {},
|
|
1089
|
-
ssr: {}
|
|
1316
|
+
ssr: {},
|
|
1317
|
+
styles: {}
|
|
1090
1318
|
}
|
|
1091
1319
|
};
|
|
1092
1320
|
}
|
|
@@ -1113,6 +1341,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
|
|
|
1113
1341
|
let root = "";
|
|
1114
1342
|
return {
|
|
1115
1343
|
name: "markless-router:routes",
|
|
1344
|
+
sharedDuringBuild: true,
|
|
1116
1345
|
configResolved(config) {
|
|
1117
1346
|
root = config.root;
|
|
1118
1347
|
},
|
|
@@ -1130,7 +1359,7 @@ function virtualModulesPlugin(resumeEntry = resumeEntryState(), navigationEntry
|
|
|
1130
1359
|
if (id.startsWith(`\0${SERVER_ENTRY_ID}`)) return serverEntrySource(root);
|
|
1131
1360
|
if (id.startsWith(`\0${RESUME_ENTRY_PATH_ID}`)) return entryPathSource("resumeEntryPath", resumeEntry, RESUME_ENTRY_ID, root);
|
|
1132
1361
|
if (id.startsWith(`\0${NAVIGATION_ENTRY_PATH_ID}`)) return entryPathSource("navigationEntryPath", navigationEntry, NAVIGATION_ENTRY_ID, root);
|
|
1133
|
-
if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads);
|
|
1362
|
+
if (id.startsWith(`\0${ROUTE_PRELOADS_ID}`)) return routePreloadsSource(routePreloads, this?.environment?.config.consumer === "client");
|
|
1134
1363
|
if (id.startsWith(`\0${ROUTER_OPTIONS_ID}`)) return "export const routerMode = \"path\"; // retained for compat; hash paths are always first-class";
|
|
1135
1364
|
}
|
|
1136
1365
|
};
|
|
@@ -1145,14 +1374,16 @@ function serverEntrySource(root) {
|
|
|
1145
1374
|
`import { createServerEntry } from '@markless/router/vite/runtime/create-server-entry';`,
|
|
1146
1375
|
`import { resumeEntryPath } from '${RESUME_ENTRY_PATH_ID}${query}';`,
|
|
1147
1376
|
`import { navigationEntryPath } from '${NAVIGATION_ENTRY_PATH_ID}${query}';`,
|
|
1148
|
-
`import { routeModulePreloads, routeSsrModulePreloads } from '${ROUTE_PRELOADS_ID}${query}';`,
|
|
1377
|
+
`import { routeModulePreloads, routeSsrModulePreloads, routeStylesheets } from '${ROUTE_PRELOADS_ID}${query}';`,
|
|
1149
1378
|
`import { pageModuleLoaders, routeFileIds } from '${ROUTE_DISCOVERY_ID}${query}';`,
|
|
1150
1379
|
`const documentModuleLoaders = import.meta.glob(['/document.tsrx']);`,
|
|
1151
1380
|
`const entry = createServerEntry({`,
|
|
1381
|
+
` dev: import.meta.env.DEV,`,
|
|
1152
1382
|
` resumeEntryPath,`,
|
|
1153
1383
|
` navigationEntryPath,`,
|
|
1154
1384
|
` routeModulePreloads,`,
|
|
1155
1385
|
` routeSsrModulePreloads,`,
|
|
1386
|
+
` routeStylesheets,`,
|
|
1156
1387
|
` documentModuleLoader: documentModuleLoaders['/document.tsrx'],`,
|
|
1157
1388
|
` pageModuleLoaders,`,
|
|
1158
1389
|
` routeFileIds,`,
|
|
@@ -1166,14 +1397,20 @@ function rootScopeQuery(root, id = "") {
|
|
|
1166
1397
|
...parseQuery(parsePath(id).search),
|
|
1167
1398
|
"markless-router-root": root || "."
|
|
1168
1399
|
});
|
|
1169
|
-
return query ? `?${query}` : "";
|
|
1170
|
-
}
|
|
1171
|
-
function routePreloadsSource(state) {
|
|
1400
|
+
return query ? `?${query}&lang.ts` : "";
|
|
1401
|
+
}
|
|
1402
|
+
function routePreloadsSource(state, client) {
|
|
1403
|
+
const routes = client || !state.persisted ? {
|
|
1404
|
+
navigation: state.routes.navigation,
|
|
1405
|
+
ssr: state.routes.ssr
|
|
1406
|
+
} : state.routes;
|
|
1407
|
+
const routeStylesheetExport = client ? [] : [`export const routeStylesheets = ${state.persisted ? "routePreloadData.styles ?? {}" : "undefined"};`];
|
|
1172
1408
|
return [
|
|
1173
1409
|
`const routePreloadsJson = globalThis.__marklessRouterRoutePreloadsJson ?? ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)};`,
|
|
1174
|
-
`const routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(
|
|
1410
|
+
`const routePreloadData = routePreloadsJson === ${JSON.stringify(ROUTE_PRELOADS_PLACEHOLDER)} ? ${JSON.stringify(routes)} : JSON.parse(routePreloadsJson);`,
|
|
1175
1411
|
`export const routeModulePreloads = routePreloadData.navigation ?? {};`,
|
|
1176
1412
|
`export const routeSsrModulePreloads = routePreloadData.ssr ?? {};`,
|
|
1413
|
+
...routeStylesheetExport,
|
|
1177
1414
|
`export function preloadRouteModule(file, document = globalThis.document) {`,
|
|
1178
1415
|
` const hrefs = routeModulePreloads[file];`,
|
|
1179
1416
|
` if (!hrefs?.length || !document?.head) return [];`,
|
|
@@ -1194,7 +1431,10 @@ function routePreloadsSource(state) {
|
|
|
1194
1431
|
].join("\n");
|
|
1195
1432
|
}
|
|
1196
1433
|
function patchRoutePreloadsInBundle(bundle, routes) {
|
|
1197
|
-
const replacement = jsStringLiteralContent(JSON.stringify(
|
|
1434
|
+
const replacement = jsStringLiteralContent(JSON.stringify({
|
|
1435
|
+
navigation: routes.navigation,
|
|
1436
|
+
ssr: routes.ssr
|
|
1437
|
+
}));
|
|
1198
1438
|
for (const chunk of outputChunks(bundle)) {
|
|
1199
1439
|
if (!chunk.code?.includes(ROUTE_PRELOADS_PLACEHOLDER)) continue;
|
|
1200
1440
|
chunk.code = chunk.code.replace(ROUTE_PRELOADS_PLACEHOLDER, replacement);
|
|
@@ -1211,13 +1451,17 @@ function routeModulePreloadsFromBundle(input) {
|
|
|
1211
1451
|
for (const chunk of chunks) {
|
|
1212
1452
|
const routeFile = routeFileForChunk(input.root, chunk);
|
|
1213
1453
|
if (!routeFile) continue;
|
|
1214
|
-
routeChunks.
|
|
1454
|
+
const routeFileChunks = routeChunks.get(routeFile) ?? [];
|
|
1455
|
+
routeFileChunks.push(chunk);
|
|
1456
|
+
routeChunks.set(routeFile, routeFileChunks);
|
|
1215
1457
|
routeChunkFileNames.add(chunk.fileName);
|
|
1216
1458
|
}
|
|
1217
1459
|
const symbolChunks = symbolChunksBySourceFile(chunks);
|
|
1218
1460
|
const navigation = {};
|
|
1219
1461
|
const ssr = {};
|
|
1220
|
-
|
|
1462
|
+
const styles = {};
|
|
1463
|
+
for (const [routeFile, routeFileChunks] of routeChunks) {
|
|
1464
|
+
const routeChunk = primaryRouteChunk(input.root, routeFile, routeFileChunks);
|
|
1221
1465
|
const navigationFileNames = /* @__PURE__ */ new Set();
|
|
1222
1466
|
if (input.navigationChunk) {
|
|
1223
1467
|
includeChunk(navigationFileNames, chunksByFileName, input.navigationChunk.fileName);
|
|
@@ -1242,11 +1486,28 @@ function routeModulePreloadsFromBundle(input) {
|
|
|
1242
1486
|
}
|
|
1243
1487
|
navigation[routeFile] = [...navigationFileNames].map((fileName) => joinURL(input.base, fileName));
|
|
1244
1488
|
ssr[routeFile] = [...ssrFileNames].map((fileName) => joinURL(input.base, fileName));
|
|
1489
|
+
styles[routeFile] = routeStylesheetsForChunk(routeChunk, chunksByFileName, input.base);
|
|
1245
1490
|
}
|
|
1246
1491
|
return {
|
|
1247
1492
|
navigation,
|
|
1248
|
-
ssr
|
|
1493
|
+
ssr,
|
|
1494
|
+
styles
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function routeStylesheetsForChunk(routeChunk, chunksByFileName, base) {
|
|
1498
|
+
const styles = /* @__PURE__ */ new Set();
|
|
1499
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1500
|
+
const visit = (chunk) => {
|
|
1501
|
+
if (visited.has(chunk.fileName)) return;
|
|
1502
|
+
visited.add(chunk.fileName);
|
|
1503
|
+
for (const imported of [...chunk.imports, ...codeStaticImports(chunk)]) {
|
|
1504
|
+
const dependency = chunksByFileName.get(imported);
|
|
1505
|
+
if (dependency) visit(dependency);
|
|
1506
|
+
}
|
|
1507
|
+
for (const stylesheet of chunk.viteMetadata?.importedCss ?? []) styles.add(joinURL(base, stylesheet));
|
|
1249
1508
|
};
|
|
1509
|
+
visit(routeChunk);
|
|
1510
|
+
return [...styles];
|
|
1250
1511
|
}
|
|
1251
1512
|
function symbolChunksBySourceFile(chunks) {
|
|
1252
1513
|
const bySourceFile = /* @__PURE__ */ new Map();
|
|
@@ -1334,6 +1595,28 @@ function routeFileForChunk(root, chunk) {
|
|
|
1334
1595
|
if (routeFile) return routeFile;
|
|
1335
1596
|
}
|
|
1336
1597
|
}
|
|
1598
|
+
function primaryRouteChunk(root, routeFile, chunks) {
|
|
1599
|
+
let selected = chunks[0];
|
|
1600
|
+
let selectedRank = routeChunkRank(root, routeFile, selected);
|
|
1601
|
+
for (const chunk of chunks.slice(1)) {
|
|
1602
|
+
const rank = routeChunkRank(root, routeFile, chunk);
|
|
1603
|
+
if (rank <= selectedRank) continue;
|
|
1604
|
+
selected = chunk;
|
|
1605
|
+
selectedRank = rank;
|
|
1606
|
+
}
|
|
1607
|
+
return selected;
|
|
1608
|
+
}
|
|
1609
|
+
function routeChunkRank(root, routeFile, chunk) {
|
|
1610
|
+
let rank = 0;
|
|
1611
|
+
for (const moduleId of [chunk.facadeModuleId, ...chunk.moduleIds ?? []]) {
|
|
1612
|
+
if (routeFileForModuleId(root, moduleId) !== routeFile) continue;
|
|
1613
|
+
const search = parsePath(moduleId).search;
|
|
1614
|
+
if (/(?:^|[?&])markless-symbols(?:[=&]|$)/.test(search)) rank = Math.max(rank, 1);
|
|
1615
|
+
else if (/(?:^|[?&])markless-resume(?:[=&]|$)/.test(search)) rank = Math.max(rank, 2);
|
|
1616
|
+
else return 3;
|
|
1617
|
+
}
|
|
1618
|
+
return rank;
|
|
1619
|
+
}
|
|
1337
1620
|
function routeFileForModuleId(root, moduleId) {
|
|
1338
1621
|
if (!moduleId || moduleId.startsWith("\0")) return void 0;
|
|
1339
1622
|
const pathname = decodePath(parseURL(moduleId).pathname);
|