veryfront 0.1.82 → 0.1.83

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.
Files changed (51) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/platform/adapters/base.d.ts +4 -1
  3. package/esm/src/platform/adapters/base.d.ts.map +1 -1
  4. package/esm/src/platform/adapters/fs/github/adapter.d.ts +2 -1
  5. package/esm/src/platform/adapters/fs/github/adapter.d.ts.map +1 -1
  6. package/esm/src/platform/adapters/fs/github/adapter.js +2 -2
  7. package/esm/src/platform/adapters/fs/github/stat-operations.d.ts +2 -1
  8. package/esm/src/platform/adapters/fs/github/stat-operations.d.ts.map +1 -1
  9. package/esm/src/platform/adapters/fs/github/stat-operations.js +2 -2
  10. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts +2 -2
  11. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  12. package/esm/src/platform/adapters/fs/veryfront/adapter.js +17 -2
  13. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.d.ts +2 -2
  14. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.d.ts.map +1 -1
  15. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.js +2 -2
  16. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts +1 -0
  17. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
  18. package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts +6 -2
  19. package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
  20. package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +131 -21
  21. package/esm/src/platform/adapters/fs/veryfront/types.d.ts +2 -1
  22. package/esm/src/platform/adapters/fs/veryfront/types.d.ts.map +1 -1
  23. package/esm/src/platform/adapters/fs/wrapper.d.ts +2 -2
  24. package/esm/src/platform/adapters/fs/wrapper.d.ts.map +1 -1
  25. package/esm/src/platform/adapters/fs/wrapper.js +2 -2
  26. package/esm/src/rendering/app-route-resolver.js +0 -9
  27. package/esm/src/rendering/orchestrator/file-resolver/index.d.ts.map +1 -1
  28. package/esm/src/rendering/orchestrator/file-resolver/index.js +17 -0
  29. package/esm/src/rendering/page-resolution/page-resolver.d.ts.map +1 -1
  30. package/esm/src/rendering/page-resolution/page-resolver.js +19 -6
  31. package/esm/src/rendering/router-detection.d.ts +1 -0
  32. package/esm/src/rendering/router-detection.d.ts.map +1 -1
  33. package/esm/src/rendering/router-detection.js +3 -0
  34. package/esm/src/types/entities/getEntityInfo.d.ts.map +1 -1
  35. package/esm/src/types/entities/getEntityInfo.js +59 -42
  36. package/package.json +1 -1
  37. package/src/deno.js +1 -1
  38. package/src/src/platform/adapters/base.ts +5 -1
  39. package/src/src/platform/adapters/fs/github/adapter.ts +3 -2
  40. package/src/src/platform/adapters/fs/github/stat-operations.ts +3 -2
  41. package/src/src/platform/adapters/fs/veryfront/adapter.ts +24 -3
  42. package/src/src/platform/adapters/fs/veryfront/multi-project-adapter.ts +6 -3
  43. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +1 -0
  44. package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +161 -25
  45. package/src/src/platform/adapters/fs/veryfront/types.ts +2 -1
  46. package/src/src/platform/adapters/fs/wrapper.ts +10 -3
  47. package/src/src/rendering/app-route-resolver.ts +0 -8
  48. package/src/src/rendering/orchestrator/file-resolver/index.ts +19 -0
  49. package/src/src/rendering/page-resolution/page-resolver.ts +26 -17
  50. package/src/src/rendering/router-detection.ts +7 -0
  51. package/src/src/types/entities/getEntityInfo.ts +78 -59
@@ -1,6 +1,6 @@
1
1
  import { logger as baseLogger } from "../../../../utils/index.js";
2
2
  import { isFrameworkSourcePath } from "../../../../utils/path-utils.js";
3
- import type { FileInfo } from "../../base.js";
3
+ import type { FileInfo, ResolveFileOptions } from "../../base.js";
4
4
  import type { ProjectFile } from "../../veryfront-api-client/index.js";
5
5
  import { VeryfrontOperationsBase } from "./base-operations.js";
6
6
  import { createError, toError } from "../../../../errors/index.js";
@@ -326,6 +326,125 @@ export class StatOperations extends VeryfrontOperationsBase {
326
326
  return files;
327
327
  }
328
328
 
329
+ private buildResolveSearchPatterns(
330
+ normalizedPath: string,
331
+ options?: ResolveFileOptions,
332
+ ): string[] {
333
+ const patterns = new Set<string>();
334
+ const pathWithoutExt = stripKnownExtension(normalizedPath, EXTENSION_PRIORITY);
335
+ const allowPagesPrefix = options?.allowPagesPrefix !== false;
336
+ const addPattern = (pattern: string): void => {
337
+ if (pattern.length > 0) patterns.add(pattern);
338
+ };
339
+
340
+ if (EXTENSION_PRIORITY.some((ext) => normalizedPath.endsWith(ext))) {
341
+ addPattern(normalizedPath);
342
+ return [...patterns];
343
+ }
344
+
345
+ addPattern(`${pathWithoutExt}.*`);
346
+ if (allowPagesPrefix && !pathWithoutExt.startsWith("pages/")) {
347
+ addPattern(`pages/${pathWithoutExt}.*`);
348
+ }
349
+
350
+ addPattern(`${pathWithoutExt}/index.*`);
351
+ if (allowPagesPrefix && !pathWithoutExt.startsWith("pages/")) {
352
+ addPattern(`pages/${pathWithoutExt}/index.*`);
353
+ }
354
+
355
+ return [...patterns];
356
+ }
357
+
358
+ private normalizeMatchedPaths(
359
+ matches: Array<{ path: string }>,
360
+ ): Array<{ path: string }> {
361
+ return matches.map((match) => ({
362
+ path: normalizeIndexedFilePath(match as ProjectFile).normalizedPath,
363
+ }));
364
+ }
365
+
366
+ private async tryResolveViaApiSearch(
367
+ normalizedPath: string,
368
+ options?: ResolveFileOptions,
369
+ ): Promise<string | null | undefined> {
370
+ if (isFrameworkSourcePath(normalizedPath)) {
371
+ logger.debug("Skipping API search for framework path", { normalizedPath });
372
+ return null;
373
+ }
374
+
375
+ if (!this.apiSearchCircuitBreaker.canSearch()) {
376
+ logger.warn("API search circuit breaker open, skipping", { normalizedPath });
377
+ return undefined;
378
+ }
379
+
380
+ const patterns = this.buildResolveSearchPatterns(normalizedPath, options);
381
+ let sawSuccessfulSearch = false;
382
+
383
+ for (const pattern of patterns) {
384
+ try {
385
+ const matches = await this.client.searchFiles(pattern);
386
+ sawSuccessfulSearch = true;
387
+ this.apiSearchCircuitBreaker.recordSuccess();
388
+
389
+ const normalizedMatches = this.normalizeMatchedPaths(matches);
390
+ if (pattern === normalizedPath) {
391
+ const exactMatch = normalizedMatches.find((match) => match.path === normalizedPath);
392
+ if (exactMatch) {
393
+ logger.debug("resolveFile found exact file via API search", {
394
+ normalizedPath,
395
+ pattern,
396
+ });
397
+ return exactMatch.path;
398
+ }
399
+ continue;
400
+ }
401
+
402
+ const sortedMatches = sortPathsByExtensionPriority(normalizedMatches, EXTENSION_PRIORITY);
403
+ const first = sortedMatches[0];
404
+ if (first) {
405
+ logger.debug("resolveFile found via API search", {
406
+ normalizedPath,
407
+ pattern,
408
+ resolvedPath: first.path,
409
+ });
410
+ return first.path;
411
+ }
412
+ } catch (error) {
413
+ const result = this.apiSearchCircuitBreaker.recordFailure();
414
+ if (result.tripped) {
415
+ logger.warn("API search circuit breaker tripped", {
416
+ failures: result.failures,
417
+ });
418
+ return undefined;
419
+ }
420
+ logger.error("API pattern search failed", { pattern, error });
421
+ }
422
+
423
+ if (!this.apiSearchCircuitBreaker.canSearch()) {
424
+ logger.warn("API search circuit breaker open, aborting remaining patterns", {
425
+ normalizedPath,
426
+ });
427
+ return undefined;
428
+ }
429
+ }
430
+
431
+ if (sawSuccessfulSearch) {
432
+ logger.debug("resolveFile not found via API search", { normalizedPath, patterns });
433
+ return null;
434
+ }
435
+
436
+ return undefined;
437
+ }
438
+
439
+ private async hasCachedFileList(): Promise<boolean> {
440
+ if (this.contextProvider?.hasCachedFileList) {
441
+ return await this.contextProvider.hasCachedFileList();
442
+ }
443
+
444
+ const files = await this.contextProvider?.getFileList?.();
445
+ return Array.isArray(files) && files.length > 0;
446
+ }
447
+
329
448
  async exists(path: string): Promise<boolean> {
330
449
  const normalizedPath = this.normalizer.normalize(path);
331
450
  try {
@@ -337,7 +456,7 @@ export class StatOperations extends VeryfrontOperationsBase {
337
456
  }
338
457
  }
339
458
 
340
- async resolveFile(basePath: string): Promise<string | null> {
459
+ async resolveFile(basePath: string, options?: ResolveFileOptions): Promise<string | null> {
341
460
  const resolveStart = performance.now();
342
461
  const normalizedPath = this.normalizer.normalize(basePath);
343
462
  const ctx = this.contextProvider?.getContentContext();
@@ -349,6 +468,36 @@ export class StatOperations extends VeryfrontOperationsBase {
349
468
  cacheKey,
350
469
  });
351
470
 
471
+ const cached = await this.cache.getAsync<string>(cacheKey);
472
+ if (cached === NOT_FOUND_SENTINEL) {
473
+ logger.debug("resolveFile cached negative result", { normalizedPath });
474
+ return null;
475
+ }
476
+
477
+ if (cached !== undefined) {
478
+ logger.debug("resolveFile cache hit", {
479
+ normalizedPath,
480
+ cached,
481
+ });
482
+ return cached;
483
+ }
484
+
485
+ const hasCachedFileList = await this.hasCachedFileList();
486
+ const attemptedApiResolve = !hasCachedFileList;
487
+
488
+ if (!hasCachedFileList) {
489
+ const apiResolved = await this.tryResolveViaApiSearch(normalizedPath, options);
490
+ if (typeof apiResolved === "string") {
491
+ this.cache.set(cacheKey, apiResolved);
492
+ return apiResolved;
493
+ }
494
+
495
+ if (apiResolved === null) {
496
+ this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
497
+ return null;
498
+ }
499
+ }
500
+
352
501
  const indexStart = performance.now();
353
502
  await this.ensureIndexBuilt();
354
503
  const indexMs = Math.round(performance.now() - indexStart);
@@ -382,7 +531,7 @@ export class StatOperations extends VeryfrontOperationsBase {
382
531
  return resolvedDirect;
383
532
  }
384
533
 
385
- if (!pathWithoutExt.startsWith("pages/")) {
534
+ if (options?.allowPagesPrefix !== false && !pathWithoutExt.startsWith("pages/")) {
386
535
  const resolvedPages = resolveByExtensionPriority(
387
536
  fileIdx,
388
537
  `pages/${pathWithoutExt}`,
@@ -410,6 +559,15 @@ export class StatOperations extends VeryfrontOperationsBase {
410
559
  return indexPath;
411
560
  }
412
561
 
562
+ if (attemptedApiResolve) {
563
+ logger.debug("resolveFile not found after pre-index API search", {
564
+ normalizedPath,
565
+ indexMs,
566
+ });
567
+ this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
568
+ return null;
569
+ }
570
+
413
571
  if (isFrameworkSourcePath(normalizedPath)) {
414
572
  logger.debug("Skipping API search for framework path", { normalizedPath });
415
573
  return null;
@@ -425,32 +583,10 @@ export class StatOperations extends VeryfrontOperationsBase {
425
583
  return null;
426
584
  }
427
585
 
428
- const cacheCheckStart = performance.now();
429
- const cached = await this.cache.getAsync<string>(cacheKey);
430
- const cacheCheckMs = Math.round(performance.now() - cacheCheckStart);
431
-
432
- if (cached === NOT_FOUND_SENTINEL) {
433
- logger.debug("resolveFile cached negative result", {
434
- normalizedPath,
435
- cacheCheckMs,
436
- });
437
- return null;
438
- }
439
-
440
- if (cached !== undefined) {
441
- logger.debug("resolveFile cache hit (unexpected)", {
442
- normalizedPath,
443
- cached,
444
- cacheCheckMs,
445
- });
446
- return cached;
447
- }
448
-
449
586
  const searchPattern = `${pathWithoutExt}.*`;
450
587
  logger.debug("Searching for file via API", {
451
588
  pattern: searchPattern,
452
589
  normalizedPath,
453
- cacheCheckMs,
454
590
  });
455
591
 
456
592
  try {
@@ -1,3 +1,4 @@
1
+ import type { ResolveFileOptions } from "../../base.js";
1
2
  import type { Project } from "../../veryfront-api-client/index.js";
2
3
  import type { GitHubConfig } from "../github/types.js";
3
4
  import type { DirectoryEntry } from "../shared-types.js";
@@ -26,7 +27,7 @@ export interface FSAdapter {
26
27
  initialize?(): Promise<void>;
27
28
  shutdown?(): Promise<void>;
28
29
 
29
- resolveFile?(basePath: string): Promise<string | null>;
30
+ resolveFile?(basePath: string, options?: ResolveFileOptions): Promise<string | null>;
30
31
  }
31
32
 
32
33
  export interface ContextualFSAdapter extends FSAdapter {
@@ -1,4 +1,11 @@
1
- import type { DirEntry, FileInfo, FileSystemAdapter, FileWatcher, WatchOptions } from "../base.js";
1
+ import type {
2
+ DirEntry,
3
+ FileInfo,
4
+ FileSystemAdapter,
5
+ FileWatcher,
6
+ ResolveFileOptions,
7
+ WatchOptions,
8
+ } from "../base.js";
2
9
  import type { ContextualFSAdapter, DirectoryEntry, FSAdapter } from "./veryfront/types.js";
3
10
 
4
11
  export interface ExtendedFileSystemAdapter extends FileSystemAdapter {
@@ -224,9 +231,9 @@ export class FSAdapterWrapper implements ExtendedFileSystemAdapter {
224
231
  };
225
232
  }
226
233
 
227
- resolveFile(basePath: string): Promise<string | null> {
234
+ resolveFile(basePath: string, options?: ResolveFileOptions): Promise<string | null> {
228
235
  if (!this._fsAdapter.resolveFile) throw new NotSupportedError("resolveFile", this.adapterType);
229
- return this._fsAdapter.resolveFile(basePath);
236
+ return this._fsAdapter.resolveFile(basePath, options);
230
237
  }
231
238
 
232
239
  async mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {
@@ -127,14 +127,6 @@ async function tryLoadPageFile(
127
127
  slug: string,
128
128
  adapter: RuntimeAdapter,
129
129
  ): Promise<EntityInfo | null> {
130
- try {
131
- const info = await adapter.fs.stat(file);
132
- if (!info.isFile) return null;
133
- } catch (_) {
134
- /* expected: file may not exist */
135
- return null;
136
- }
137
-
138
130
  let raw: string;
139
131
  try {
140
132
  raw = await adapter.fs.readFile(file);
@@ -6,6 +6,7 @@
6
6
  * @module rendering/orchestrator/file-resolver
7
7
  */
8
8
 
9
+ import { join } from "../../../platform/compat/path/index.js";
9
10
  import { rendererLogger as logger } from "../../../utils/index.js";
10
11
  import type { RuntimeAdapter } from "../../../platform/adapters/base.js";
11
12
  import { buildCandidatePaths, findFirstExisting } from "./candidates.js";
@@ -48,6 +49,24 @@ export async function findSourceFile(
48
49
  projectDir: string,
49
50
  adapter: RuntimeAdapter,
50
51
  ): Promise<string | null> {
52
+ if (adapter.fs.resolveFile) {
53
+ const directBases = [join(projectDir, basePath)];
54
+ const withoutComponents = basePath.replace(/^components\//, "");
55
+ if (withoutComponents !== basePath) {
56
+ directBases.push(join(projectDir, withoutComponents));
57
+ }
58
+
59
+ for (const candidateBase of directBases) {
60
+ const resolved = await adapter.fs.resolveFile(candidateBase, {
61
+ allowPagesPrefix: false,
62
+ });
63
+ if (resolved) {
64
+ logger.debug("[FileResolver] Found file via resolveFile:", resolved);
65
+ return resolved;
66
+ }
67
+ }
68
+ }
69
+
51
70
  const candidates = buildCandidatePaths(projectDir, basePath, SOURCE_EXTENSIONS);
52
71
 
53
72
  const withoutComponents = basePath.replace(/^components\//, "");
@@ -7,7 +7,11 @@ import type { RuntimeAdapter } from "../../platform/adapters/base.js";
7
7
  import type { VeryfrontConfig } from "../../config/index.js";
8
8
  import type { EntityInfo } from "../../types/index.js";
9
9
  import { getEntityBySlug } from "../../types/entities/getEntityInfo.js";
10
- import { detectAppRouter, getAppRouteEntity } from "../router-detection.js";
10
+ import {
11
+ detectAppRouter,
12
+ getAppRouteEntity,
13
+ primeRouterDetectionCache,
14
+ } from "../router-detection.js";
11
15
 
12
16
  const PAGE_EXTENSIONS = /\.(mdx|md|tsx|jsx|ts|js)$/;
13
17
  const APP_ROUTER_PAGE_PATTERN = /^page\.(mdx|md|tsx|jsx|ts|js)$/;
@@ -61,39 +65,44 @@ export class PageResolver {
61
65
  return withSpan(
62
66
  "routing.resolve_page",
63
67
  async () => {
64
- const useAppRouter = await detectAppRouter(
65
- this.projectDir,
66
- this.config,
67
- this.adapter,
68
- { projectId: this.projectId },
69
- );
70
-
71
68
  const appDirName = this.config.directories?.app ?? "app";
69
+ const cacheKey = this.projectId ?? this.projectDir;
72
70
 
73
71
  let pageInfo: EntityInfo | null | undefined;
74
72
 
75
- if (useAppRouter) {
73
+ if (this.config.router === "app") {
74
+ pageInfo = await getAppRouteEntity(
75
+ this.projectDir,
76
+ slug,
77
+ this.adapter,
78
+ appDirName,
79
+ );
80
+ if (pageInfo) {
81
+ primeRouterDetectionCache(cacheKey, "app");
82
+ }
83
+ } else if (this.config.router === "pages") {
84
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
85
+ if (pageInfo) {
86
+ primeRouterDetectionCache(cacheKey, "pages");
87
+ }
88
+ } else {
89
+ // Auto mode stays structural: a single resolved route must not pin router mode
90
+ // for projects that are still transitioning between app/ and pages/.
76
91
  pageInfo = await getAppRouteEntity(
77
92
  this.projectDir,
78
93
  slug,
79
94
  this.adapter,
80
95
  appDirName,
81
96
  );
82
-
83
97
  if (!pageInfo) {
84
- logger.debug(
85
- "App Router resolution failed, falling back to Pages Router",
86
- { slug },
87
- );
98
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
88
99
  }
89
100
  }
90
101
 
91
- pageInfo ??= await getEntityBySlug(this.projectDir, slug, this.adapter);
92
-
93
102
  if (!pageInfo) {
94
103
  throw FILE_NOT_FOUND.create({
95
104
  detail: `Page not found: ${slug}`,
96
- context: { slug, useAppRouter },
105
+ context: { slug, router: this.config.router ?? "auto" },
97
106
  });
98
107
  }
99
108
 
@@ -49,6 +49,13 @@ export function clearRouterDetectionCacheForProject(projectId: string): void {
49
49
  routerDetectionCache.delete(projectId);
50
50
  }
51
51
 
52
+ export function primeRouterDetectionCache(
53
+ projectKey: string,
54
+ mode: "app" | "pages",
55
+ ): void {
56
+ routerDetectionCache.set(projectKey, mode === "app");
57
+ }
58
+
52
59
  export interface DetectAppRouterOptions {
53
60
  /** Project ID for cache isolation in multi-tenant deployments */
54
61
  projectId?: string;
@@ -43,49 +43,55 @@ export async function getEntityInfo(
43
43
  }
44
44
 
45
45
  try {
46
+ const shouldReadDirectly = adapter
47
+ ? isExtendedFSAdapter(adapter.fs) && adapter.fs.isVeryfrontAdapter()
48
+ : false;
49
+
50
+ let content: string;
46
51
  if (adapter) {
47
- try {
48
- const stat = await withFallback(
49
- () => adapter.fs.stat(normalizedPath),
50
- async () => {
51
- const exists = await fs.exists(filePath);
52
- if (!exists) {
53
- throw toError(
54
- createError({
55
- type: "file",
56
- message: "File not found",
57
- context: { path: filePath, operation: "read" },
58
- }),
59
- );
60
- }
61
- return await fs.stat(filePath);
62
- },
63
- { operationName: "stat:getEntityInfo", logError: false },
64
- );
52
+ if (!shouldReadDirectly) {
53
+ try {
54
+ const stat = await withFallback(
55
+ () => adapter.fs.stat(normalizedPath),
56
+ async () => {
57
+ const exists = await fs.exists(filePath);
58
+ if (!exists) {
59
+ throw toError(
60
+ createError({
61
+ type: "file",
62
+ message: "File not found",
63
+ context: { path: filePath, operation: "read" },
64
+ }),
65
+ );
66
+ }
67
+ return await fs.stat(filePath);
68
+ },
69
+ { operationName: "stat:getEntityInfo", logError: false },
70
+ );
65
71
 
66
- if (!stat.isFile) return null;
67
- } catch (error) {
68
- entityInfoScope.runSync(
69
- () => {
70
- throw error;
71
- },
72
- { path: filePath, details: { reason: "stat-failed" } },
73
- undefined,
74
- );
75
- return null;
72
+ if (!stat.isFile) return null;
73
+ } catch (error) {
74
+ entityInfoScope.runSync(
75
+ () => {
76
+ throw error;
77
+ },
78
+ { path: filePath, details: { reason: "stat-failed" } },
79
+ undefined,
80
+ );
81
+ return null;
82
+ }
76
83
  }
77
- } else {
78
- const exists = await fs.exists(filePath);
79
- if (!exists) return null;
80
- }
81
84
 
82
- const content = adapter
83
- ? await withFallback(
85
+ content = await withFallback(
84
86
  () => adapter.fs.readFile(normalizedPath),
85
87
  () => fs.readTextFile(filePath),
86
88
  { operationName: "readFile:getEntityInfo", logError: false },
87
- )
88
- : await fs.readTextFile(filePath);
89
+ );
90
+ } else {
91
+ const exists = await fs.exists(filePath);
92
+ if (!exists) return null;
93
+ content = await fs.readTextFile(filePath);
94
+ }
89
95
 
90
96
  const ext = pathHelper.extname(filePath).toLowerCase();
91
97
 
@@ -171,26 +177,30 @@ export async function getEntityBySlug(
171
177
  return await withSpan(
172
178
  "types.getEntityBySlug",
173
179
  async () => {
174
- const isVeryfrontRoute = slug.startsWith(".veryfront/") || slug === ".veryfront";
180
+ const normalizedSlug = normalizeSlug(slug);
181
+ const isVeryfrontRoute = normalizedSlug.startsWith(".veryfront/") ||
182
+ normalizedSlug === ".veryfront";
175
183
  const resolveFile = adapter?.fs.resolveFile;
176
184
 
177
185
  logger.debug("START", {
178
186
  slug,
187
+ normalizedSlug,
179
188
  projectDir,
180
189
  isVeryfrontRoute,
181
190
  hasResolveFile: !!resolveFile,
182
191
  });
183
192
 
184
193
  if (resolveFile) {
185
- const basePaths = [pathHelper.join(projectDir, "pages", slug)];
194
+ const basePaths = [pathHelper.join(projectDir, "pages", normalizedSlug)];
186
195
 
187
- if (isVeryfrontRoute) basePaths.unshift(pathHelper.join(projectDir, slug));
188
- if (slug === "index" || slug === "") {
196
+ if (isVeryfrontRoute) basePaths.unshift(pathHelper.join(projectDir, normalizedSlug));
197
+ if (normalizedSlug === "index" || normalizedSlug === "") {
189
198
  basePaths.unshift(pathHelper.join(projectDir, "pages", "index"));
190
199
  }
191
200
 
192
201
  logger.debug("Checking paths (resolveFile branch)", {
193
202
  slug,
203
+ normalizedSlug,
194
204
  basePaths,
195
205
  });
196
206
 
@@ -208,13 +218,14 @@ export async function getEntityBySlug(
208
218
  if (info?.entity.isPage) {
209
219
  logger.debug("Found page via resolveFile", {
210
220
  slug,
221
+ normalizedSlug,
211
222
  path: info.entity.path,
212
223
  });
213
224
  return info;
214
225
  }
215
226
  }
216
227
 
217
- const slugParts = slug.split("/");
228
+ const slugParts = normalizedSlug === "" ? [] : normalizedSlug.split("/");
218
229
  for (let depth = slugParts.length - 1; depth >= 0; depth--) {
219
230
  const parentPath = slugParts.slice(0, depth).join("/");
220
231
  const pagesDir = parentPath
@@ -259,33 +270,33 @@ export async function getEntityBySlug(
259
270
  }
260
271
  }
261
272
 
262
- logger.debug("No page found via resolveFile branch", { slug });
273
+ logger.debug("No page found via resolveFile branch", { slug, normalizedSlug });
263
274
  return null;
264
275
  }
265
276
 
266
277
  const possiblePaths = [
267
- pathHelper.join(projectDir, "pages", `${slug}.mdx`),
268
- pathHelper.join(projectDir, "pages", `${slug}.md`),
269
- pathHelper.join(projectDir, "pages", `${slug}.tsx`),
270
- pathHelper.join(projectDir, "pages", `${slug}.jsx`),
271
- pathHelper.join(projectDir, "pages", `${slug}.ts`),
272
- pathHelper.join(projectDir, "pages", `${slug}/index.mdx`),
273
- pathHelper.join(projectDir, "pages", `${slug}/index.md`),
274
- pathHelper.join(projectDir, "pages", `${slug}/index.tsx`),
275
- pathHelper.join(projectDir, "pages", `${slug}/index.jsx`),
276
- pathHelper.join(projectDir, "pages", `${slug}/index.ts`),
278
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.mdx`),
279
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.md`),
280
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.tsx`),
281
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.jsx`),
282
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.ts`),
283
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.mdx`),
284
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.md`),
285
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.tsx`),
286
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.jsx`),
287
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.ts`),
277
288
  ];
278
289
 
279
290
  if (isVeryfrontRoute) {
280
291
  possiblePaths.unshift(
281
- pathHelper.join(projectDir, `${slug}.mdx`),
282
- pathHelper.join(projectDir, `${slug}.md`),
283
- pathHelper.join(projectDir, `${slug}.tsx`),
284
- pathHelper.join(projectDir, `${slug}.ts`),
292
+ pathHelper.join(projectDir, `${normalizedSlug}.mdx`),
293
+ pathHelper.join(projectDir, `${normalizedSlug}.md`),
294
+ pathHelper.join(projectDir, `${normalizedSlug}.tsx`),
295
+ pathHelper.join(projectDir, `${normalizedSlug}.ts`),
285
296
  );
286
297
  }
287
298
 
288
- if (slug === "index" || slug === "") {
299
+ if (normalizedSlug === "index" || normalizedSlug === "") {
289
300
  possiblePaths.unshift(
290
301
  pathHelper.join(projectDir, "pages", "index.mdx"),
291
302
  pathHelper.join(projectDir, "pages", "index.md"),
@@ -302,7 +313,7 @@ export async function getEntityBySlug(
302
313
  if (info?.entity.isPage) return info;
303
314
  }
304
315
 
305
- const slugParts = slug.split("/");
316
+ const slugParts = normalizedSlug === "" ? [] : normalizedSlug.split("/");
306
317
  for (let depth = slugParts.length - 1; depth >= 0; depth--) {
307
318
  const parentPath = slugParts.slice(0, depth).join("/");
308
319
  const pagesDir = parentPath
@@ -356,7 +367,11 @@ export async function getEntityBySlug(
356
367
 
357
368
  return null;
358
369
  },
359
- { "entity.slug": slug, "entity.projectDir": projectDir },
370
+ {
371
+ "entity.slug": slug,
372
+ "entity.normalized_slug": normalizeSlug(slug),
373
+ "entity.projectDir": projectDir,
374
+ },
360
375
  );
361
376
  }
362
377
 
@@ -448,3 +463,7 @@ function getSlugFromPath(filePath: string): string {
448
463
  const parentDir = parts[parts.length - 2];
449
464
  return parentDir === "pages" ? "" : parentDir ?? "";
450
465
  }
466
+
467
+ function normalizeSlug(slug: string): string {
468
+ return slug === "/" ? "" : slug.replace(/^\/+/, "").replace(/\/+$/, "");
469
+ }