@markless/router 0.1.0 → 0.2.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/debug-channel-C7v9u5SM.js +250 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +156 -3
- 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 +312 -29
- package/package.json +22 -7
- package/src/vite/entries/client-entry.ts +67 -0
- package/src/vite/entries/resume-entry.ts +42 -0
- package/src/vite/entries/route-discovery.ts +8 -0
- package/src/vite/entries/route-href.ts +35 -0
- package/src/vite/entries/server-entry.ts +25 -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
|
@@ -15,12 +15,13 @@ function init(modules) {
|
|
|
15
15
|
const projectRoot = getProjectRoot(typeScript, info);
|
|
16
16
|
const pagesDir = resolvePagesDir(getConfiguredProjectRoot(info) ?? projectRoot, pluginConfig);
|
|
17
17
|
proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
18
|
-
return withRouteHrefCompletions(typeScript, info, pagesDir, languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings));
|
|
18
|
+
return withRouteHrefCompletions(typeScript, info, pagesDir, fileName, position, languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings));
|
|
19
19
|
};
|
|
20
20
|
return proxy;
|
|
21
21
|
} };
|
|
22
22
|
}
|
|
23
|
-
function withRouteHrefCompletions(typeScript, info, pagesDir, completions) {
|
|
23
|
+
function withRouteHrefCompletions(typeScript, info, pagesDir, fileName, position, completions) {
|
|
24
|
+
if (!isRouteHrefCompletionContext(typeScript, info, fileName, position)) return completions;
|
|
24
25
|
const routeHrefs = routeHrefCompletions(typeScript, info, pagesDir);
|
|
25
26
|
if (routeHrefs.length === 0) return completions;
|
|
26
27
|
const baseCompletions = completions ?? emptyCompletionInfo();
|
|
@@ -36,6 +37,95 @@ function withRouteHrefCompletions(typeScript, info, pagesDir, completions) {
|
|
|
36
37
|
entries: [...routeEntries, ...baseCompletions.entries.filter((entry) => !routeHrefSet.has(entry.name))]
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
function isRouteHrefCompletionContext(typeScript, info, fileName, position) {
|
|
41
|
+
if (extname(fileName).toLowerCase() === ".tsrx") {
|
|
42
|
+
const snapshot = info.project.getScriptInfo(fileName)?.getSnapshot();
|
|
43
|
+
if (!snapshot || position < 0 || position > snapshot.getLength()) return false;
|
|
44
|
+
const source = snapshot.getText(0, snapshot.getLength());
|
|
45
|
+
return isInsideStringLikeToken(typeScript, source, position) || isInsideHrefAttributeInitializer(source, position);
|
|
46
|
+
}
|
|
47
|
+
const sourceFile = info.languageService.getProgram()?.getSourceFile(fileName);
|
|
48
|
+
if (!sourceFile || position < 0 || position > sourceFile.getFullText().length) return false;
|
|
49
|
+
const node = deepestNodeAtPosition(sourceFile, position);
|
|
50
|
+
if (node && isStringLikeNode(typeScript, node)) return true;
|
|
51
|
+
for (let current = node; current; current = current.parent) {
|
|
52
|
+
if (!typeScript.isJsxAttribute(current) || current.name.getText(sourceFile) !== "href") continue;
|
|
53
|
+
const initializer = current.initializer;
|
|
54
|
+
return Boolean(initializer && initializer.getStart(sourceFile) <= position && position <= initializer.end);
|
|
55
|
+
}
|
|
56
|
+
return isInsideHrefAttributeInitializer(sourceFile.getFullText(), position);
|
|
57
|
+
}
|
|
58
|
+
function isInsideStringLikeToken(typeScript, source, position) {
|
|
59
|
+
const scanner = typeScript.createScanner(typeScript.ScriptTarget.Latest, false, typeScript.LanguageVariant.JSX, source);
|
|
60
|
+
const stringLikeTokens = new Set([
|
|
61
|
+
typeScript.SyntaxKind.StringLiteral,
|
|
62
|
+
typeScript.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
63
|
+
typeScript.SyntaxKind.TemplateHead,
|
|
64
|
+
typeScript.SyntaxKind.TemplateMiddle,
|
|
65
|
+
typeScript.SyntaxKind.TemplateTail
|
|
66
|
+
]);
|
|
67
|
+
for (let token = scanner.scan(); token !== typeScript.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
68
|
+
const start = scanner.getTokenPos();
|
|
69
|
+
const end = scanner.getTextPos();
|
|
70
|
+
if (position < start) return false;
|
|
71
|
+
if (stringLikeTokens.has(token) && start < position) {
|
|
72
|
+
if (position < end || position === end && scanner.isUnterminated()) return true;
|
|
73
|
+
}
|
|
74
|
+
if (position < end) return false;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function isInsideHrefAttributeInitializer(source, position) {
|
|
79
|
+
const tagStart = openJsxTagStartAtPosition(source, position);
|
|
80
|
+
if (tagStart === void 0) return false;
|
|
81
|
+
const prefix = source.slice(tagStart + 1, position);
|
|
82
|
+
return /(?:^|\s)href\s*=\s*(?:\{(?:[^{}]|\{[^{}]*\})*|[^\s>]*)$/s.test(prefix);
|
|
83
|
+
}
|
|
84
|
+
function openJsxTagStartAtPosition(source, position) {
|
|
85
|
+
let tagStart;
|
|
86
|
+
let braceDepth = 0;
|
|
87
|
+
let quote;
|
|
88
|
+
let escaped = false;
|
|
89
|
+
for (let index = 0; index < position; index += 1) {
|
|
90
|
+
const character = source[index];
|
|
91
|
+
if (quote) {
|
|
92
|
+
if (escaped) escaped = false;
|
|
93
|
+
else if (character === "\\") escaped = true;
|
|
94
|
+
else if (character === quote) quote = void 0;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (character === "'" || character === "\"" || character === "`") {
|
|
98
|
+
quote = character;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (tagStart === void 0 && character === "<") {
|
|
102
|
+
const next = source[index + 1];
|
|
103
|
+
if (next && /[A-Za-z_$/{>]/.test(next)) {
|
|
104
|
+
tagStart = index;
|
|
105
|
+
braceDepth = 0;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (tagStart === void 0) continue;
|
|
110
|
+
if (character === "{") braceDepth += 1;
|
|
111
|
+
else if (character === "}" && braceDepth > 0) braceDepth -= 1;
|
|
112
|
+
else if (character === ">" && braceDepth === 0) tagStart = void 0;
|
|
113
|
+
}
|
|
114
|
+
return tagStart;
|
|
115
|
+
}
|
|
116
|
+
function deepestNodeAtPosition(sourceFile, position) {
|
|
117
|
+
let deepest;
|
|
118
|
+
const visit = (node) => {
|
|
119
|
+
if (position < node.getFullStart() || position > node.end) return;
|
|
120
|
+
deepest = node;
|
|
121
|
+
node.forEachChild(visit);
|
|
122
|
+
};
|
|
123
|
+
visit(sourceFile);
|
|
124
|
+
return deepest;
|
|
125
|
+
}
|
|
126
|
+
function isStringLikeNode(typeScript, node) {
|
|
127
|
+
return typeScript.isStringLiteral(node) || typeScript.isNoSubstitutionTemplateLiteral(node) || node.kind === typeScript.SyntaxKind.TemplateHead || node.kind === typeScript.SyntaxKind.TemplateMiddle || node.kind === typeScript.SyntaxKind.TemplateTail;
|
|
128
|
+
}
|
|
39
129
|
function emptyCompletionInfo() {
|
|
40
130
|
return {
|
|
41
131
|
isGlobalCompletion: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as normalizeRouteFileId } from "../../route-manifest-
|
|
1
|
+
import { i as normalizeRouteFileId } from "../../route-manifest-DPgGIyrS.js";
|
|
2
2
|
//#region src/vite/runtime/create-route-discovery.ts
|
|
3
3
|
function createRouteDiscovery(discoveredPageModuleLoaders) {
|
|
4
4
|
const pageModuleLoaders = Object.fromEntries(Object.entries(discoveredPageModuleLoaders).map(([file, loader]) => [normalizeRouteFileId(file), loader]));
|
|
@@ -2,10 +2,12 @@ import { ModulePreloadInput } from "@markless/web/render-to-string";
|
|
|
2
2
|
|
|
3
3
|
//#region src/vite/runtime/create-server-entry.d.ts
|
|
4
4
|
interface ServerEntryOptions {
|
|
5
|
+
readonly dev?: boolean;
|
|
5
6
|
readonly navigationEntryPath?: string;
|
|
6
7
|
readonly resumeEntryPath?: string;
|
|
7
8
|
readonly routeModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
|
|
8
9
|
readonly routeSsrModulePreloads?: Record<string, readonly ModulePreloadInput[]>;
|
|
10
|
+
readonly routeStylesheets?: Record<string, readonly string[]>;
|
|
9
11
|
readonly documentModuleLoader: (() => Promise<unknown>) | undefined;
|
|
10
12
|
readonly pageModuleLoaders: Record<string, () => Promise<unknown>>;
|
|
11
13
|
readonly routeFileIds: readonly string[];
|
|
@@ -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) {
|