@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.
@@ -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-OTaWQ_TC.js";
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 { n as matchRouteManifest, t as buildRouteManifestFromFileIds } from "../../route-manifest-OTaWQ_TC.js";
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 renderStatusPage(url, manifest.statusPages.error, 500, "Internal Server Error");
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 = splitLeadingModulePreloadLinks(await renderToString(pageArtifact, renderOptions));
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 = splitLeadingModulePreloadLinks(stream.shell);
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 routedPageArtifact(renderSsr, baseArtifact, pageProps, file, navigationEntryPath) {
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 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
- }
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: links.join("")
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
- return `<script data-markless-router-link-resumer>${escapeInlineScript(`(() => {
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) {