@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.
@@ -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
- return withRouteHrefCompletions(typeScript, info, pagesDir, fileName, position, languageService.getCompletionsAtPosition(fileName, position, options, formattingSettings));
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 routeFile = (0, pathe.normalize)(fileId.slice(0, -extension.length));
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[0] === "api" || segments.length === 1 && (segments[0] === "404" || segments[0] === "500");
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 routeFile = (0, pathe.normalize)(fileId.slice(0, -extension.length));
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 = [];