@markless/router 0.4.0 → 0.6.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/create-server-entry-DaYssO9x.js +1331 -0
- package/dist/debug-channel-CewnC9fH.js +2 -0
- package/dist/{debug-channel-CSzo-Zj8.js → debug-channel-Di6SppZv.js} +36 -17
- package/dist/index-BjxpxRL7.d.ts +248 -0
- package/dist/index.d.ts +2 -212
- package/dist/index.js +108 -40
- package/dist/protocol-constants-B4WxrZAS.js +26 -0
- package/dist/request-locals-z3rJTizs.js +90 -0
- package/dist/route-href-CGooO_g6.js +18 -0
- package/dist/route-manifest-DMizh42B.js +456 -0
- package/dist/typescript-plugin.cjs +9 -6
- package/dist/typescript-plugin.d.ts +3 -11194
- package/dist/typescript-plugin.js +9 -6
- package/dist/vite/route-href.d.ts +2 -3
- package/dist/vite/route-href.js +1 -17
- package/dist/vite/runtime/create-route-discovery.d.ts +3 -4
- package/dist/vite/runtime/create-route-discovery.js +1 -1
- package/dist/vite/runtime/create-server-entry.d.ts +27 -5
- package/dist/vite/runtime/create-server-entry.js +2 -400
- package/dist/vite/runtime/mdx-route.d.ts +1116 -17
- package/dist/vite/runtime/mdx-route.js +1097 -31
- package/dist/vite.d.ts +10 -6
- package/dist/vite.js +965 -203
- package/package.json +7 -6
- package/src/vite/entries/client-entry.ts +10 -1
- package/src/vite/entries/fragment-entry.ts +797 -0
- package/src/vite/entries/resume-entry.ts +41 -7
- package/dist/request-files-C-0LLSsx.js +0 -232
- package/dist/route-manifest-DPgGIyrS.js +0 -186
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import { basename, extname, join, normalize, relative } from "pathe";
|
|
2
|
+
import { joinURL, withLeadingSlash, withoutLeadingSlash, withoutTrailingSlash } from "ufo";
|
|
3
|
+
const MIDDLEWARE_DIR = "middleware";
|
|
4
|
+
const HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
5
|
+
"connect",
|
|
6
|
+
"delete",
|
|
7
|
+
"get",
|
|
8
|
+
"head",
|
|
9
|
+
"options",
|
|
10
|
+
"patch",
|
|
11
|
+
"post",
|
|
12
|
+
"put",
|
|
13
|
+
"trace"
|
|
14
|
+
]);
|
|
15
|
+
function parseRequestFile(fileId, sourceText) {
|
|
16
|
+
const file = normalizeRequestFileId(fileId);
|
|
17
|
+
const apiFile = apiRelativeFile(file);
|
|
18
|
+
if (apiFile) return parseApiRequestFile(file, apiFile, sourceText);
|
|
19
|
+
if (middlewareRelativeFile(file)) return parseMiddlewareRequestFile(file, sourceText);
|
|
20
|
+
return {
|
|
21
|
+
diagnostics: [],
|
|
22
|
+
file,
|
|
23
|
+
kind: "none"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function normalizeRequestFileId(fileId) {
|
|
27
|
+
return withoutLeadingSlash(normalize(fileId));
|
|
28
|
+
}
|
|
29
|
+
function transformRequestFileSource(fileId, sourceText) {
|
|
30
|
+
const requestFile = parseRequestFile(fileId, sourceText);
|
|
31
|
+
if (requestFile.kind === "none" || requestFile.diagnostics.length > 0 || !requestFile.defaultExport) return;
|
|
32
|
+
const handler = localizeDefaultExport(sourceText);
|
|
33
|
+
if (!handler) return;
|
|
34
|
+
return {
|
|
35
|
+
code: requestFileWrapperSource(requestFile, handler.code, handler.name),
|
|
36
|
+
file: requestFile.file,
|
|
37
|
+
kind: requestFile.kind
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function parseApiRequestFile(file, apiFile, sourceText) {
|
|
41
|
+
const defaultExport = findDefaultFunctionExport(sourceText);
|
|
42
|
+
const cache = parseCacheMetadata(sourceText);
|
|
43
|
+
const diagnostics = [
|
|
44
|
+
...missingDefaultFunctionDiagnostics("api", defaultExport),
|
|
45
|
+
...httpMethodExportDiagnostics(sourceText),
|
|
46
|
+
...cacheMetadataDiagnostics(sourceText, cache)
|
|
47
|
+
];
|
|
48
|
+
return {
|
|
49
|
+
...cache ? { cache } : {},
|
|
50
|
+
...defaultExport ? { defaultExport } : {},
|
|
51
|
+
diagnostics,
|
|
52
|
+
file,
|
|
53
|
+
kind: "api",
|
|
54
|
+
...apiRouteFromFile(apiFile)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function parseMiddlewareRequestFile(file, sourceText) {
|
|
58
|
+
const defaultExport = findDefaultFunctionExport(sourceText);
|
|
59
|
+
const diagnostics = missingDefaultFunctionDiagnostics("middleware", defaultExport);
|
|
60
|
+
return {
|
|
61
|
+
...defaultExport ? { defaultExport } : {},
|
|
62
|
+
diagnostics,
|
|
63
|
+
file,
|
|
64
|
+
kind: "middleware"
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function apiRouteFromFile(apiFile) {
|
|
68
|
+
const withoutExtension = apiFile.slice(0, -3);
|
|
69
|
+
const rawSegments = normalize(join(withoutExtension)).split("/");
|
|
70
|
+
const finalSegment = rawSegments.at(-1);
|
|
71
|
+
const method = methodSuffix(finalSegment);
|
|
72
|
+
if (method !== "all") rawSegments[rawSegments.length - 1] = finalSegment.slice(0, -(method.length + 1));
|
|
73
|
+
if (rawSegments.at(-1) === "index") rawSegments.pop();
|
|
74
|
+
const normalizedSegments = rawSegments.filter(Boolean).map(normalizeRouteSegment);
|
|
75
|
+
const pathnameSegments = normalizedSegments.map((segment) => segment.pathname);
|
|
76
|
+
const patternSegments = normalizedSegments.map((segment) => segment.pattern);
|
|
77
|
+
return {
|
|
78
|
+
method,
|
|
79
|
+
route: {
|
|
80
|
+
params: normalizedSegments.flatMap((segment) => segment.params),
|
|
81
|
+
pathname: requestPathname(["api", ...pathnameSegments]),
|
|
82
|
+
pattern: requestPathname(["api", ...patternSegments])
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function normalizeRouteSegment(segment) {
|
|
87
|
+
const catchAll = segment.match(/^\[\.\.\.([A-Za-z_$][\w$]*)\]$/);
|
|
88
|
+
if (catchAll) return {
|
|
89
|
+
params: [{
|
|
90
|
+
kind: "catch-all",
|
|
91
|
+
name: catchAll[1]
|
|
92
|
+
}],
|
|
93
|
+
pathname: "**",
|
|
94
|
+
pattern: `[...${catchAll[1]}]`
|
|
95
|
+
};
|
|
96
|
+
const dynamic = segment.match(/^\[([A-Za-z_$][\w$]*)\]$/);
|
|
97
|
+
if (dynamic) return {
|
|
98
|
+
params: [{
|
|
99
|
+
kind: "dynamic",
|
|
100
|
+
name: dynamic[1]
|
|
101
|
+
}],
|
|
102
|
+
pathname: `:${dynamic[1]}`,
|
|
103
|
+
pattern: `[${dynamic[1]}]`
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
params: [],
|
|
107
|
+
pathname: segment,
|
|
108
|
+
pattern: segment
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function requestPathname(segments) {
|
|
112
|
+
if (segments.length === 0) return "/";
|
|
113
|
+
const [base, ...rest] = segments;
|
|
114
|
+
return withLeadingSlash(joinURL(base, ...rest));
|
|
115
|
+
}
|
|
116
|
+
function methodSuffix(finalSegment) {
|
|
117
|
+
const suffix = finalSegment.split(".").at(-1);
|
|
118
|
+
return suffix && HTTP_METHODS.has(suffix) ? suffix : "all";
|
|
119
|
+
}
|
|
120
|
+
function findDefaultFunctionExport(sourceText) {
|
|
121
|
+
const directFunction = sourceText.match(/\bexport\s+default\s+(?:async\s+)?function(?:\s+[A-Za-z_$][\w$]*)?\s*\(\s*([A-Za-z_$][\w$]*)?/);
|
|
122
|
+
if (directFunction) return {
|
|
123
|
+
kind: "function",
|
|
124
|
+
parameterName: directFunction[1]
|
|
125
|
+
};
|
|
126
|
+
const directArrow = sourceText.match(/\bexport\s+default\s+(?:async\s+)?(?:\(\s*([A-Za-z_$][\w$]*)?\s*\)|([A-Za-z_$][\w$]*))\s*=>/);
|
|
127
|
+
if (directArrow) return {
|
|
128
|
+
kind: "function",
|
|
129
|
+
parameterName: directArrow[1] ?? directArrow[2]
|
|
130
|
+
};
|
|
131
|
+
const defaultIdentifier = sourceText.match(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;/)?.[1];
|
|
132
|
+
if (!defaultIdentifier) return;
|
|
133
|
+
const escapedIdentifier = escapeRegExp(defaultIdentifier);
|
|
134
|
+
const constArrow = sourceText.match(new RegExp(`\\bconst\\s+${escapedIdentifier}\\s*=\\s*(?:async\\s+)?(?:\\(\\s*([A-Za-z_$][\\w$]*)?\\s*\\)|([A-Za-z_$][\\w$]*))\\s*=>`));
|
|
135
|
+
if (constArrow) return {
|
|
136
|
+
kind: "function",
|
|
137
|
+
parameterName: constArrow[1] ?? constArrow[2]
|
|
138
|
+
};
|
|
139
|
+
const functionDeclaration = sourceText.match(new RegExp(`\\b(?:async\\s+)?function\\s+${escapedIdentifier}\\s*\\(\\s*([A-Za-z_$][\\w$]*)?`));
|
|
140
|
+
if (functionDeclaration) return {
|
|
141
|
+
kind: "function",
|
|
142
|
+
parameterName: functionDeclaration[1]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseCacheMetadata(sourceText) {
|
|
146
|
+
const objectLiteral = sourceText.match(/\bexport\s+const\s+cache\s*=\s*(\{[\s\S]*?\})\s*;?/)?.[1];
|
|
147
|
+
if (!objectLiteral) return;
|
|
148
|
+
const maxAge = objectLiteral.match(/^\{\s*maxAge\s*:\s*(\d+(?:\.\d+)?)\s*,?\s*\}$/)?.[1];
|
|
149
|
+
return maxAge ? { maxAge: Number(maxAge) } : void 0;
|
|
150
|
+
}
|
|
151
|
+
function missingDefaultFunctionDiagnostics(kind, defaultExport) {
|
|
152
|
+
if (defaultExport) return [];
|
|
153
|
+
return [{
|
|
154
|
+
code: "missing-default-function",
|
|
155
|
+
message: kind === "api" ? "API files must default export a function." : "Middleware files must default export a function."
|
|
156
|
+
}];
|
|
157
|
+
}
|
|
158
|
+
function httpMethodExportDiagnostics(sourceText) {
|
|
159
|
+
const exportedMethods = /* @__PURE__ */ new Set();
|
|
160
|
+
for (const pattern of [
|
|
161
|
+
/\bexport\s+(?:async\s+)?function\s+([A-Z]+)\b/g,
|
|
162
|
+
/\bexport\s+const\s+([A-Z]+)\b/g,
|
|
163
|
+
/\bexport\s*\{([^}]+)\}/g
|
|
164
|
+
]) for (const match of sourceText.matchAll(pattern)) {
|
|
165
|
+
const exportClause = match[1];
|
|
166
|
+
if (!exportClause) continue;
|
|
167
|
+
if (exportClause.includes(",")) {
|
|
168
|
+
for (const exportName of exportClause.split(",")) addHttpMethodExport(exportedMethods, exportName);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
addHttpMethodExport(exportedMethods, exportClause);
|
|
172
|
+
}
|
|
173
|
+
return Array.from(exportedMethods).toSorted().map((method) => ({
|
|
174
|
+
code: "http-method-export",
|
|
175
|
+
message: `Do not export ${method}; the HTTP method comes from the filename.`
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function addHttpMethodExport(methods, exportName) {
|
|
179
|
+
const method = exportName.trim().split(/\s+as\s+/i).at(-1)?.trim();
|
|
180
|
+
if (method && HTTP_METHODS.has(method.toLowerCase())) methods.add(method.toUpperCase());
|
|
181
|
+
}
|
|
182
|
+
function cacheMetadataDiagnostics(sourceText, cache) {
|
|
183
|
+
if (!/\bexport\s+const\s+cache\b/.test(sourceText) || cache) return [];
|
|
184
|
+
return [{
|
|
185
|
+
code: "invalid-cache-metadata",
|
|
186
|
+
message: "Use export const cache for endpoint cache metadata."
|
|
187
|
+
}];
|
|
188
|
+
}
|
|
189
|
+
function apiRelativeFile(file) {
|
|
190
|
+
if (!isRequestModuleFile(file) || file === "api" || !file.startsWith(`api/`)) return;
|
|
191
|
+
return relative("api", file);
|
|
192
|
+
}
|
|
193
|
+
function middlewareRelativeFile(file) {
|
|
194
|
+
if (!isRequestModuleFile(file) || file === MIDDLEWARE_DIR || !file.startsWith(`${MIDDLEWARE_DIR}/`)) return;
|
|
195
|
+
return relative(MIDDLEWARE_DIR, file);
|
|
196
|
+
}
|
|
197
|
+
function localizeDefaultExport(sourceText) {
|
|
198
|
+
const defaultIdentifierMatch = sourceText.match(/\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;/);
|
|
199
|
+
if (defaultIdentifierMatch) return {
|
|
200
|
+
code: sourceText.replace(defaultIdentifierMatch[0], ""),
|
|
201
|
+
name: defaultIdentifierMatch[1]
|
|
202
|
+
};
|
|
203
|
+
const directDefaultMatch = sourceText.match(/\bexport\s+default\b/);
|
|
204
|
+
if (!directDefaultMatch || directDefaultMatch.index === void 0) return;
|
|
205
|
+
return {
|
|
206
|
+
code: `${sourceText.slice(0, directDefaultMatch.index)}const __markless_request_handler__ =${sourceText.slice(directDefaultMatch.index + directDefaultMatch[0].length)}`,
|
|
207
|
+
name: "__markless_request_handler__"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function requestFileWrapperSource(requestFile, sourceText, handlerName) {
|
|
211
|
+
const defineImport = requestFile.kind === "api" && requestFile.cache ? "import { defineCachedHandler as __markless_define_handler__ } from \"nitro/cache\";" : "import { defineHandler as __markless_define_handler__ } from \"nitro\";";
|
|
212
|
+
const wrappedHandler = `(event) =>
|
|
213
|
+
${handlerName}(__markless_create_http_context__(event))`;
|
|
214
|
+
const wrappedDefault = requestFile.kind === "api" && requestFile.cache ? `__markless_define_handler__(${wrappedHandler}, cache)` : `__markless_define_handler__(${wrappedHandler})`;
|
|
215
|
+
return `${defineImport}
|
|
216
|
+
import { __marklessCreateHttpContext as __markless_create_http_context__ } from "@markless/router";
|
|
217
|
+
|
|
218
|
+
${sourceText.trimEnd()}
|
|
219
|
+
|
|
220
|
+
export default ${wrappedDefault};
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function isRequestModuleFile(file) {
|
|
224
|
+
return extname(file) === ".ts" && basename(file) !== ".ts";
|
|
225
|
+
}
|
|
226
|
+
function escapeRegExp(value) {
|
|
227
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/route-manifest.ts
|
|
231
|
+
const PAGES_DIR = "pages";
|
|
232
|
+
const PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".tsrx", ".mdx"]);
|
|
233
|
+
function buildRouteManifestFromFileIds(fileIds) {
|
|
234
|
+
const pages = unique(fileIds.map(normalizeRouteFileId)).filter(isPageModuleFile).toSorted(compareCodeUnits).map(normalizePage);
|
|
235
|
+
const routes = pages.flatMap((page) => page.kind === "route" ? [page.route] : []);
|
|
236
|
+
assertNoRouteConflicts(routes);
|
|
237
|
+
const statusPages = {};
|
|
238
|
+
for (const page of pages) if (page.kind === "status") statusPages[page.status] = page.file;
|
|
239
|
+
return {
|
|
240
|
+
routes: routes.map(toPublicRoute).toSorted(compareRoutes),
|
|
241
|
+
statusPages
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function normalizeRequestPathname(pathname) {
|
|
245
|
+
const normalizedPathname = withoutTrailingSlash(withLeadingSlash(pathname));
|
|
246
|
+
return normalizedPathname === "" ? "/" : normalizedPathname;
|
|
247
|
+
}
|
|
248
|
+
function normalizeRouteFileId(fileId) {
|
|
249
|
+
return withoutLeadingSlash(normalize(fileId));
|
|
250
|
+
}
|
|
251
|
+
function matchRouteManifest(pathname, manifest) {
|
|
252
|
+
const requestPathname = normalizeRequestPathname(pathname);
|
|
253
|
+
for (const route of manifest.routes) {
|
|
254
|
+
const params = matchRoutePathname(route, requestPathname);
|
|
255
|
+
if (params) return {
|
|
256
|
+
route,
|
|
257
|
+
params
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function isPageModuleFile(file) {
|
|
262
|
+
const pageFile = pageRelativeFile(file);
|
|
263
|
+
return pageFile !== void 0 && PAGE_EXTENSIONS.has(extname(pageFile));
|
|
264
|
+
}
|
|
265
|
+
function normalizePage(file) {
|
|
266
|
+
const relativeFile = file;
|
|
267
|
+
const pageFile = pageRelativeFile(file);
|
|
268
|
+
const extension = extname(pageFile);
|
|
269
|
+
const withoutExtension = pageFile.slice(0, -extension.length);
|
|
270
|
+
const routeFile = normalize(join(withoutExtension));
|
|
271
|
+
const rawSegments = routeFile === "." ? [] : routeFile.split("/");
|
|
272
|
+
if (rawSegments.length === 1 && rawSegments[0] === "404") return {
|
|
273
|
+
kind: "status",
|
|
274
|
+
status: "notFound",
|
|
275
|
+
file: relativeFile
|
|
276
|
+
};
|
|
277
|
+
if (rawSegments.length === 1 && rawSegments[0] === "500") return {
|
|
278
|
+
kind: "status",
|
|
279
|
+
status: "error",
|
|
280
|
+
file: relativeFile
|
|
281
|
+
};
|
|
282
|
+
if (rawSegments.length > 1 && ["404", "500"].includes(rawSegments.at(-1))) throw new Error(`Nested status pages are not supported in v0: ${relativeFile}`);
|
|
283
|
+
if (rawSegments.at(-1) === "index") rawSegments.pop();
|
|
284
|
+
const pattern = routePathname(rawSegments);
|
|
285
|
+
const segments = rawSegments.map((segment, index) => normalizeSegment(segment, index, rawSegments, relativeFile));
|
|
286
|
+
const pathnameSegments = segments.map((segment) => segment.pathname);
|
|
287
|
+
const identitySegments = segments.map((segment) => segment.identity);
|
|
288
|
+
const params = segments.flatMap((segment) => segment.params);
|
|
289
|
+
return {
|
|
290
|
+
kind: "route",
|
|
291
|
+
route: {
|
|
292
|
+
pathname: routePathname(pathnameSegments),
|
|
293
|
+
pattern,
|
|
294
|
+
identity: routePathname(identitySegments),
|
|
295
|
+
params,
|
|
296
|
+
file: relativeFile,
|
|
297
|
+
relativeFile
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function pageRelativeFile(file) {
|
|
302
|
+
if (file === PAGES_DIR || !file.startsWith(`${PAGES_DIR}/`)) return;
|
|
303
|
+
return relative(PAGES_DIR, file);
|
|
304
|
+
}
|
|
305
|
+
function normalizeSegment(segment, index, segments, relativeFile) {
|
|
306
|
+
const catchAll = segment.match(/^\[\.\.\.([A-Za-z_$][\w$]*)\]$/);
|
|
307
|
+
if (catchAll) {
|
|
308
|
+
if (index !== segments.length - 1) throw new Error(`Catch-all route segments must be final: ${relativeFile}`);
|
|
309
|
+
return {
|
|
310
|
+
pathname: "**",
|
|
311
|
+
identity: "**",
|
|
312
|
+
params: [{
|
|
313
|
+
name: catchAll[1],
|
|
314
|
+
kind: "catch-all"
|
|
315
|
+
}]
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const dynamic = segment.match(/^\[([A-Za-z_$][\w$]*)\]$/);
|
|
319
|
+
if (dynamic) return {
|
|
320
|
+
pathname: `:${dynamic[1]}`,
|
|
321
|
+
identity: ":param",
|
|
322
|
+
params: [{
|
|
323
|
+
name: dynamic[1],
|
|
324
|
+
kind: "dynamic"
|
|
325
|
+
}]
|
|
326
|
+
};
|
|
327
|
+
if (segment.includes("[") || segment.includes("]")) throw new Error(`Unsupported route segment pattern: ${relativeFile}`);
|
|
328
|
+
return {
|
|
329
|
+
pathname: segment,
|
|
330
|
+
identity: segment,
|
|
331
|
+
params: []
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function assertNoRouteConflicts(routes) {
|
|
335
|
+
const routesByIdentity = /* @__PURE__ */ new Map();
|
|
336
|
+
for (const route of routes) {
|
|
337
|
+
const existingRoutes = routesByIdentity.get(route.identity) ?? [];
|
|
338
|
+
existingRoutes.push(route);
|
|
339
|
+
routesByIdentity.set(route.identity, existingRoutes);
|
|
340
|
+
}
|
|
341
|
+
for (const [identity, conflictingRoutes] of routesByIdentity) {
|
|
342
|
+
if (conflictingRoutes.length < 2) continue;
|
|
343
|
+
const files = conflictingRoutes.map((route) => route.relativeFile).toSorted(compareCodeUnits);
|
|
344
|
+
throw new Error([`Route conflict: ${identity} is defined by both:`, ...files.map((file) => `- ${file}`)].join("\n"));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const PAGE_METHODS = /* @__PURE__ */ new Set([
|
|
348
|
+
"all",
|
|
349
|
+
"get",
|
|
350
|
+
"head"
|
|
351
|
+
]);
|
|
352
|
+
/**
|
|
353
|
+
* Page files may live under pages/api/ beside the endpoints in api/; the build fails only when one
|
|
354
|
+
* address is answered by both, since the endpoint would always win and the page could never be served.
|
|
355
|
+
*/
|
|
356
|
+
function assertNoEndpointConflicts(manifest, endpointFileIds) {
|
|
357
|
+
for (const fileId of endpointFileIds) {
|
|
358
|
+
const endpoint = parseRequestFile(fileId, "");
|
|
359
|
+
if (endpoint.kind !== "api" || !PAGE_METHODS.has(endpoint.method)) continue;
|
|
360
|
+
const segments = pathSegments(endpoint.route.pathname);
|
|
361
|
+
for (const route of manifest.routes) {
|
|
362
|
+
const address = sharedAddress(pathSegments(route.pathname), segments);
|
|
363
|
+
if (address === void 0) continue;
|
|
364
|
+
throw new Error([
|
|
365
|
+
`Route conflict: ${address} is answered by both a page and an endpoint:`,
|
|
366
|
+
`- ${route.file}`,
|
|
367
|
+
`- ${endpoint.file}`
|
|
368
|
+
].join("\n"));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const pathSegments = (pathname) => pathname.split("/").filter(Boolean);
|
|
373
|
+
function sharedAddress(left, right) {
|
|
374
|
+
const address = [];
|
|
375
|
+
for (let index = 0;; index++) {
|
|
376
|
+
const a = left[index];
|
|
377
|
+
const b = right[index];
|
|
378
|
+
if (a === "**" || b === "**") {
|
|
379
|
+
const other = a === "**" ? right : left;
|
|
380
|
+
return other.length > index ? `/${[...address, ...other.slice(index)].join("/")}` : void 0;
|
|
381
|
+
}
|
|
382
|
+
if (a === void 0 || b === void 0) return a === b ? `/${address.join("/")}` : void 0;
|
|
383
|
+
if (a.startsWith(":")) address.push(b);
|
|
384
|
+
else if (b.startsWith(":") || a === b) address.push(a);
|
|
385
|
+
else return void 0;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function toPublicRoute(route) {
|
|
389
|
+
return {
|
|
390
|
+
pathname: route.pathname,
|
|
391
|
+
pattern: route.pattern,
|
|
392
|
+
params: route.params,
|
|
393
|
+
file: route.file
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function compareRoutes(left, right) {
|
|
397
|
+
const leftSegments = splitRoutePathname(left.pathname);
|
|
398
|
+
const rightSegments = splitRoutePathname(right.pathname);
|
|
399
|
+
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
400
|
+
for (let index = 0; index < length; index += 1) {
|
|
401
|
+
const leftSegment = leftSegments[index];
|
|
402
|
+
const rightSegment = rightSegments[index];
|
|
403
|
+
if (leftSegment === void 0) return -1;
|
|
404
|
+
if (rightSegment === void 0) return 1;
|
|
405
|
+
const rankDifference = segmentRank(leftSegment) - segmentRank(rightSegment);
|
|
406
|
+
if (rankDifference !== 0) return rankDifference;
|
|
407
|
+
if (segmentRank(leftSegment) === 0 && leftSegment !== rightSegment) return compareCodeUnits(leftSegment, rightSegment);
|
|
408
|
+
}
|
|
409
|
+
return compareCodeUnits(left.pathname, right.pathname);
|
|
410
|
+
}
|
|
411
|
+
function splitRoutePathname(pathname) {
|
|
412
|
+
const route = withoutLeadingSlash(pathname);
|
|
413
|
+
return route === "" ? [] : route.split("/");
|
|
414
|
+
}
|
|
415
|
+
function matchRoutePathname(route, requestPathname) {
|
|
416
|
+
const routeSegments = splitRoutePathname(route.pathname);
|
|
417
|
+
const requestSegments = splitRoutePathname(requestPathname);
|
|
418
|
+
const catchAllIndex = routeSegments.indexOf("**");
|
|
419
|
+
if (catchAllIndex === -1 && routeSegments.length !== requestSegments.length) return;
|
|
420
|
+
if (catchAllIndex !== -1 && requestSegments.length <= catchAllIndex) return;
|
|
421
|
+
const params = {};
|
|
422
|
+
for (let index = 0; index < routeSegments.length; index += 1) {
|
|
423
|
+
const routeSegment = routeSegments[index];
|
|
424
|
+
const requestSegment = requestSegments[index];
|
|
425
|
+
if (routeSegment === "**") {
|
|
426
|
+
const catchAllParam = route.params.find((param) => param.kind === "catch-all");
|
|
427
|
+
if (!catchAllParam) return void 0;
|
|
428
|
+
params[catchAllParam.name] = joinURL(requestSegment, ...requestSegments.slice(index + 1));
|
|
429
|
+
return params;
|
|
430
|
+
}
|
|
431
|
+
if (routeSegment.startsWith(":")) {
|
|
432
|
+
params[routeSegment.slice(1)] = requestSegment;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (routeSegment !== requestSegment) return;
|
|
436
|
+
}
|
|
437
|
+
return params;
|
|
438
|
+
}
|
|
439
|
+
function segmentRank(segment) {
|
|
440
|
+
if (segment === "**") return 2;
|
|
441
|
+
if (segment.startsWith(":")) return 1;
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
function compareCodeUnits(left, right) {
|
|
445
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
446
|
+
}
|
|
447
|
+
function unique(values) {
|
|
448
|
+
return [...new Set(values)];
|
|
449
|
+
}
|
|
450
|
+
function routePathname(segments) {
|
|
451
|
+
if (segments.length === 0) return "/";
|
|
452
|
+
const [base, ...rest] = segments;
|
|
453
|
+
return withLeadingSlash(joinURL(base, ...rest));
|
|
454
|
+
}
|
|
455
|
+
//#endregion
|
|
456
|
+
export { normalizeRequestPathname as a, parseRequestFile as c, matchRouteManifest as i, transformRequestFileSource as l, buildRouteManifestFromFileIds as n, normalizeRouteFileId as o, isPageModuleFile as r, normalizeRequestFileId as s, assertNoEndpointConflicts as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
let pathe = require("pathe");
|
|
2
2
|
//#region src/typescript-plugin/index.ts
|
|
3
|
-
const PAGE_EXTENSIONS = new Set([".tsrx", ".mdx"]);
|
|
3
|
+
const PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".tsrx", ".mdx"]);
|
|
4
4
|
function init(modules) {
|
|
5
5
|
const typeScript = modules.typescript;
|
|
6
6
|
return { create(info) {
|
|
@@ -15,7 +15,8 @@ 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
|
-
|
|
18
|
+
const completions = languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings);
|
|
19
|
+
return withRouteHrefCompletions(typeScript, info, pagesDir, fileName, position, completions);
|
|
19
20
|
};
|
|
20
21
|
return proxy;
|
|
21
22
|
} };
|
|
@@ -57,7 +58,7 @@ function isRouteHrefCompletionContext(typeScript, info, fileName, position) {
|
|
|
57
58
|
}
|
|
58
59
|
function isInsideStringLikeToken(typeScript, source, position) {
|
|
59
60
|
const scanner = typeScript.createScanner(typeScript.ScriptTarget.Latest, false, typeScript.LanguageVariant.JSX, source);
|
|
60
|
-
const stringLikeTokens = new Set([
|
|
61
|
+
const stringLikeTokens = /* @__PURE__ */ new Set([
|
|
61
62
|
typeScript.SyntaxKind.StringLiteral,
|
|
62
63
|
typeScript.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
63
64
|
typeScript.SyntaxKind.TemplateHead,
|
|
@@ -164,13 +165,15 @@ function routeHrefCompletion(fileId) {
|
|
|
164
165
|
}
|
|
165
166
|
function isReservedOrUnsupportedPage(fileId) {
|
|
166
167
|
const extension = (0, pathe.extname)(fileId);
|
|
167
|
-
const
|
|
168
|
+
const withoutExtension = fileId.slice(0, -extension.length);
|
|
169
|
+
const routeFile = (0, pathe.normalize)(withoutExtension);
|
|
168
170
|
const segments = routeFile === "." ? [] : routeFile.split("/").filter(Boolean);
|
|
169
|
-
return segments
|
|
171
|
+
return segments.length === 1 && (segments[0] === "404" || segments[0] === "500");
|
|
170
172
|
}
|
|
171
173
|
function routeFromFileId(fileId) {
|
|
172
174
|
const extension = (0, pathe.extname)(fileId);
|
|
173
|
-
const
|
|
175
|
+
const withoutExtension = fileId.slice(0, -extension.length);
|
|
176
|
+
const routeFile = (0, pathe.normalize)(withoutExtension);
|
|
174
177
|
const segments = routeFile === "." ? [] : routeFile.split("/").filter(Boolean);
|
|
175
178
|
if (segments.at(-1) === "index") segments.pop();
|
|
176
179
|
const params = [];
|