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 +1 @@
1
- {"version":3,"file":"page-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/page-resolution/page-resolver.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAgCvD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAiB;gBAEpB,OAAO,EAAE,mBAAmB;IAOxC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiDxC,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAoCxB,sBAAsB;IAoD9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY1C,aAAa,IAAI,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC;CAShD"}
1
+ {"version":3,"file":"page-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/rendering/page-resolution/page-resolver.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAoCvD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,cAAc,CAAC;CACzB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAiB;gBAEpB,OAAO,EAAE,mBAAmB;IAOxC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAsDxC,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAoCxB,sBAAsB;IAoD9B,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY1C,aAAa,IAAI,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC;CAShD"}
@@ -4,7 +4,7 @@ import { VeryfrontError } from "../../errors/index.js";
4
4
  import { FILE_NOT_FOUND } from "../../errors/error-registry.js";
5
5
  import { withSpan } from "../../observability/tracing/otlp-setup.js";
6
6
  import { getEntityBySlug } from "../../types/entities/getEntityInfo.js";
7
- import { detectAppRouter, getAppRouteEntity } from "../router-detection.js";
7
+ import { detectAppRouter, getAppRouteEntity, primeRouterDetectionCache, } from "../router-detection.js";
8
8
  const PAGE_EXTENSIONS = /\.(mdx|md|tsx|jsx|ts|js)$/;
9
9
  const APP_ROUTER_PAGE_PATTERN = /^page\.(mdx|md|tsx|jsx|ts|js)$/;
10
10
  function isPageFile(name) {
@@ -40,20 +40,33 @@ export class PageResolver {
40
40
  }
41
41
  resolvePage(slug) {
42
42
  return withSpan("routing.resolve_page", async () => {
43
- const useAppRouter = await detectAppRouter(this.projectDir, this.config, this.adapter, { projectId: this.projectId });
44
43
  const appDirName = this.config.directories?.app ?? "app";
44
+ const cacheKey = this.projectId ?? this.projectDir;
45
45
  let pageInfo;
46
- if (useAppRouter) {
46
+ if (this.config.router === "app") {
47
+ pageInfo = await getAppRouteEntity(this.projectDir, slug, this.adapter, appDirName);
48
+ if (pageInfo) {
49
+ primeRouterDetectionCache(cacheKey, "app");
50
+ }
51
+ }
52
+ else if (this.config.router === "pages") {
53
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
54
+ if (pageInfo) {
55
+ primeRouterDetectionCache(cacheKey, "pages");
56
+ }
57
+ }
58
+ else {
59
+ // Auto mode stays structural: a single resolved route must not pin router mode
60
+ // for projects that are still transitioning between app/ and pages/.
47
61
  pageInfo = await getAppRouteEntity(this.projectDir, slug, this.adapter, appDirName);
48
62
  if (!pageInfo) {
49
- logger.debug("App Router resolution failed, falling back to Pages Router", { slug });
63
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
50
64
  }
51
65
  }
52
- pageInfo ??= await getEntityBySlug(this.projectDir, slug, this.adapter);
53
66
  if (!pageInfo) {
54
67
  throw FILE_NOT_FOUND.create({
55
68
  detail: `Page not found: ${slug}`,
56
- context: { slug, useAppRouter },
69
+ context: { slug, router: this.config.router ?? "auto" },
57
70
  });
58
71
  }
59
72
  return pageInfo;
@@ -21,6 +21,7 @@ export declare function clearRouterDetectionCache(): void;
21
21
  * @param projectId - The project ID used as cache key. Falls back to projectDir for local dev.
22
22
  */
23
23
  export declare function clearRouterDetectionCacheForProject(projectId: string): void;
24
+ export declare function primeRouterDetectionCache(projectKey: string, mode: "app" | "pages"): void;
24
25
  export interface DetectAppRouterOptions {
25
26
  /** Project ID for cache isolation in multi-tenant deployments */
26
27
  projectId?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"router-detection.d.ts","sourceRoot":"","sources":["../../../src/src/rendering/router-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;4BAO4B;AAK5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAQ1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAY5D;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,OAAO,CAAC,CA6BlB"}
1
+ {"version":3,"file":"router-detection.d.ts","sourceRoot":"","sources":["../../../src/src/rendering/router-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;4BAO4B;AAK5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAQ1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAY5D;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE3E;AAED,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,KAAK,GAAG,OAAO,GACpB,IAAI,CAEN;AAED,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,OAAO,CAAC,CA6BlB"}
@@ -38,6 +38,9 @@ export function clearRouterDetectionCache() {
38
38
  export function clearRouterDetectionCacheForProject(projectId) {
39
39
  routerDetectionCache.delete(projectId);
40
40
  }
41
+ export function primeRouterDetectionCache(projectKey, mode) {
42
+ routerDetectionCache.set(projectKey, mode === "app");
43
+ }
41
44
  /**
42
45
  * Detect if app router should be used based on config and directory structure.
43
46
  *
@@ -1 +1 @@
1
- {"version":3,"file":"getEntityInfo.d.ts","sourceRoot":"","sources":["../../../../src/src/types/entities/getEntityInfo.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAU,UAAU,EAAe,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAWtE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA6I5B;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA+L5B;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAyE5B"}
1
+ {"version":3,"file":"getEntityInfo.d.ts","sourceRoot":"","sources":["../../../../src/src/types/entities/getEntityInfo.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAU,UAAU,EAAe,MAAM,gBAAgB,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAWtE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAmJ5B;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAwM5B;AAED,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAyE5B"}
@@ -29,37 +29,42 @@ export async function getEntityInfo(filePath, adapter) {
29
29
  }
30
30
  }
31
31
  try {
32
+ const shouldReadDirectly = adapter
33
+ ? isExtendedFSAdapter(adapter.fs) && adapter.fs.isVeryfrontAdapter()
34
+ : false;
35
+ let content;
32
36
  if (adapter) {
33
- try {
34
- const stat = await withFallback(() => adapter.fs.stat(normalizedPath), async () => {
35
- const exists = await fs.exists(filePath);
36
- if (!exists) {
37
- throw toError(createError({
38
- type: "file",
39
- message: "File not found",
40
- context: { path: filePath, operation: "read" },
41
- }));
42
- }
43
- return await fs.stat(filePath);
44
- }, { operationName: "stat:getEntityInfo", logError: false });
45
- if (!stat.isFile)
37
+ if (!shouldReadDirectly) {
38
+ try {
39
+ const stat = await withFallback(() => adapter.fs.stat(normalizedPath), async () => {
40
+ const exists = await fs.exists(filePath);
41
+ if (!exists) {
42
+ throw toError(createError({
43
+ type: "file",
44
+ message: "File not found",
45
+ context: { path: filePath, operation: "read" },
46
+ }));
47
+ }
48
+ return await fs.stat(filePath);
49
+ }, { operationName: "stat:getEntityInfo", logError: false });
50
+ if (!stat.isFile)
51
+ return null;
52
+ }
53
+ catch (error) {
54
+ entityInfoScope.runSync(() => {
55
+ throw error;
56
+ }, { path: filePath, details: { reason: "stat-failed" } }, undefined);
46
57
  return null;
58
+ }
47
59
  }
48
- catch (error) {
49
- entityInfoScope.runSync(() => {
50
- throw error;
51
- }, { path: filePath, details: { reason: "stat-failed" } }, undefined);
52
- return null;
53
- }
60
+ content = await withFallback(() => adapter.fs.readFile(normalizedPath), () => fs.readTextFile(filePath), { operationName: "readFile:getEntityInfo", logError: false });
54
61
  }
55
62
  else {
56
63
  const exists = await fs.exists(filePath);
57
64
  if (!exists)
58
65
  return null;
66
+ content = await fs.readTextFile(filePath);
59
67
  }
60
- const content = adapter
61
- ? await withFallback(() => adapter.fs.readFile(normalizedPath), () => fs.readTextFile(filePath), { operationName: "readFile:getEntityInfo", logError: false })
62
- : await fs.readTextFile(filePath);
63
68
  const ext = pathHelper.extname(filePath).toLowerCase();
64
69
  let frontmatter = {};
65
70
  let body = content;
@@ -120,23 +125,27 @@ export async function getEntityInfo(filePath, adapter) {
120
125
  }
121
126
  export async function getEntityBySlug(projectDir, slug, adapter) {
122
127
  return await withSpan("types.getEntityBySlug", async () => {
123
- const isVeryfrontRoute = slug.startsWith(".veryfront/") || slug === ".veryfront";
128
+ const normalizedSlug = normalizeSlug(slug);
129
+ const isVeryfrontRoute = normalizedSlug.startsWith(".veryfront/") ||
130
+ normalizedSlug === ".veryfront";
124
131
  const resolveFile = adapter?.fs.resolveFile;
125
132
  logger.debug("START", {
126
133
  slug,
134
+ normalizedSlug,
127
135
  projectDir,
128
136
  isVeryfrontRoute,
129
137
  hasResolveFile: !!resolveFile,
130
138
  });
131
139
  if (resolveFile) {
132
- const basePaths = [pathHelper.join(projectDir, "pages", slug)];
140
+ const basePaths = [pathHelper.join(projectDir, "pages", normalizedSlug)];
133
141
  if (isVeryfrontRoute)
134
- basePaths.unshift(pathHelper.join(projectDir, slug));
135
- if (slug === "index" || slug === "") {
142
+ basePaths.unshift(pathHelper.join(projectDir, normalizedSlug));
143
+ if (normalizedSlug === "index" || normalizedSlug === "") {
136
144
  basePaths.unshift(pathHelper.join(projectDir, "pages", "index"));
137
145
  }
138
146
  logger.debug("Checking paths (resolveFile branch)", {
139
147
  slug,
148
+ normalizedSlug,
140
149
  basePaths,
141
150
  });
142
151
  const pathResults = await parallelMap(basePaths, async (basePath) => {
@@ -153,12 +162,13 @@ export async function getEntityBySlug(projectDir, slug, adapter) {
153
162
  if (info?.entity.isPage) {
154
163
  logger.debug("Found page via resolveFile", {
155
164
  slug,
165
+ normalizedSlug,
156
166
  path: info.entity.path,
157
167
  });
158
168
  return info;
159
169
  }
160
170
  }
161
- const slugParts = slug.split("/");
171
+ const slugParts = normalizedSlug === "" ? [] : normalizedSlug.split("/");
162
172
  for (let depth = slugParts.length - 1; depth >= 0; depth--) {
163
173
  const parentPath = slugParts.slice(0, depth).join("/");
164
174
  const pagesDir = parentPath
@@ -194,25 +204,25 @@ export async function getEntityBySlug(projectDir, slug, adapter) {
194
204
  /* expected: directory may not exist or readDir may fail */
195
205
  }
196
206
  }
197
- logger.debug("No page found via resolveFile branch", { slug });
207
+ logger.debug("No page found via resolveFile branch", { slug, normalizedSlug });
198
208
  return null;
199
209
  }
200
210
  const possiblePaths = [
201
- pathHelper.join(projectDir, "pages", `${slug}.mdx`),
202
- pathHelper.join(projectDir, "pages", `${slug}.md`),
203
- pathHelper.join(projectDir, "pages", `${slug}.tsx`),
204
- pathHelper.join(projectDir, "pages", `${slug}.jsx`),
205
- pathHelper.join(projectDir, "pages", `${slug}.ts`),
206
- pathHelper.join(projectDir, "pages", `${slug}/index.mdx`),
207
- pathHelper.join(projectDir, "pages", `${slug}/index.md`),
208
- pathHelper.join(projectDir, "pages", `${slug}/index.tsx`),
209
- pathHelper.join(projectDir, "pages", `${slug}/index.jsx`),
210
- pathHelper.join(projectDir, "pages", `${slug}/index.ts`),
211
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.mdx`),
212
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.md`),
213
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.tsx`),
214
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.jsx`),
215
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}.ts`),
216
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.mdx`),
217
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.md`),
218
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.tsx`),
219
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.jsx`),
220
+ pathHelper.join(projectDir, "pages", `${normalizedSlug}/index.ts`),
211
221
  ];
212
222
  if (isVeryfrontRoute) {
213
- possiblePaths.unshift(pathHelper.join(projectDir, `${slug}.mdx`), pathHelper.join(projectDir, `${slug}.md`), pathHelper.join(projectDir, `${slug}.tsx`), pathHelper.join(projectDir, `${slug}.ts`));
223
+ possiblePaths.unshift(pathHelper.join(projectDir, `${normalizedSlug}.mdx`), pathHelper.join(projectDir, `${normalizedSlug}.md`), pathHelper.join(projectDir, `${normalizedSlug}.tsx`), pathHelper.join(projectDir, `${normalizedSlug}.ts`));
214
224
  }
215
- if (slug === "index" || slug === "") {
225
+ if (normalizedSlug === "index" || normalizedSlug === "") {
216
226
  possiblePaths.unshift(pathHelper.join(projectDir, "pages", "index.mdx"), pathHelper.join(projectDir, "pages", "index.md"), pathHelper.join(projectDir, "pages", "index.tsx"), pathHelper.join(projectDir, "pages", "index.ts"));
217
227
  }
218
228
  const pathResults = await parallelMap(possiblePaths, async (p) => {
@@ -222,7 +232,7 @@ export async function getEntityBySlug(projectDir, slug, adapter) {
222
232
  if (info?.entity.isPage)
223
233
  return info;
224
234
  }
225
- const slugParts = slug.split("/");
235
+ const slugParts = normalizedSlug === "" ? [] : normalizedSlug.split("/");
226
236
  for (let depth = slugParts.length - 1; depth >= 0; depth--) {
227
237
  const parentPath = slugParts.slice(0, depth).join("/");
228
238
  const pagesDir = parentPath
@@ -267,7 +277,11 @@ export async function getEntityBySlug(projectDir, slug, adapter) {
267
277
  }
268
278
  }
269
279
  return null;
270
- }, { "entity.slug": slug, "entity.projectDir": projectDir });
280
+ }, {
281
+ "entity.slug": slug,
282
+ "entity.normalized_slug": normalizeSlug(slug),
283
+ "entity.projectDir": projectDir,
284
+ });
271
285
  }
272
286
  export async function getLayoutEntity(projectDir, layoutName, adapter) {
273
287
  return await withSpan("types.getLayoutEntity", async () => {
@@ -345,3 +359,6 @@ function getSlugFromPath(filePath) {
345
359
  const parentDir = parts[parts.length - 2];
346
360
  return parentDir === "pages" ? "" : parentDir ?? "";
347
361
  }
362
+ function normalizeSlug(slug) {
363
+ return slug === "/" ? "" : slug.replace(/^\/+/, "").replace(/\/+$/, "");
364
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.82",
3
+ "version": "0.1.83",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -117,7 +117,11 @@ export interface FileSystemAdapter {
117
117
  makeTempDir(prefix: string): Promise<string>;
118
118
  watch(paths: string | string[], options?: WatchOptions): FileWatcher;
119
119
  /** Resolve a file path with extension fallback (e.g., pages/test → pages/test.mdx) */
120
- resolveFile?(basePath: string): Promise<string | null>;
120
+ resolveFile?(basePath: string, options?: ResolveFileOptions): Promise<string | null>;
121
+ }
122
+
123
+ export interface ResolveFileOptions {
124
+ allowPagesPrefix?: boolean;
121
125
  }
122
126
 
123
127
  export interface DirEntry {
@@ -1,6 +1,7 @@
1
1
  import { logger } from "../../../../utils/index.js";
2
2
  import { CONFIG_INVALID } from "../../../../errors/index.js";
3
3
  import { getEnv } from "../../../compat/process.js";
4
+ import type { ResolveFileOptions } from "../../base.js";
4
5
  import { FileCache } from "../cache/file-cache.js";
5
6
  import type { FSAdapter, FSAdapterConfig } from "../veryfront/types.js";
6
7
  import { GitHubApiClient } from "./github-api-client.js";
@@ -115,9 +116,9 @@ export class GitHubFSAdapter implements FSAdapter {
115
116
  return this.dirOps.readdir(path);
116
117
  }
117
118
 
118
- async resolveFile(basePath: string): Promise<string | null> {
119
+ async resolveFile(basePath: string, options?: ResolveFileOptions): Promise<string | null> {
119
120
  await this.ensureInitialized();
120
- return this.statOps.resolveFile(basePath);
121
+ return this.statOps.resolveFile(basePath, options);
121
122
  }
122
123
 
123
124
  getCacheStats(): {
@@ -5,6 +5,7 @@ import {
5
5
  buildGitHubStatCacheKey,
6
6
  buildGitHubTreeCacheKey,
7
7
  } from "../../../../cache/index.js";
8
+ import type { ResolveFileOptions } from "../../base.js";
8
9
  import type { FileCache } from "../cache/file-cache.js";
9
10
  import type { GitHubApiClient } from "./github-api-client.js";
10
11
  import type { FileIndexEntry, FileInfo, GitHubTreeEntry, ResolvedGitHubConfig } from "./types.js";
@@ -176,7 +177,7 @@ export class GitHubStatOperations {
176
177
  }
177
178
  }
178
179
 
179
- async resolveFile(basePath: string): Promise<string | null> {
180
+ async resolveFile(basePath: string, options?: ResolveFileOptions): Promise<string | null> {
180
181
  await this.ensureIndex();
181
182
 
182
183
  const normalizedPath = normalizeGitHubPath(basePath, this.projectDir);
@@ -185,7 +186,7 @@ export class GitHubStatOperations {
185
186
  if (cached !== undefined) return cached;
186
187
 
187
188
  const resolved = this.tryResolve(normalizedPath) ??
188
- this.tryResolveWithPagesPrefix(normalizedPath);
189
+ (options?.allowPagesPrefix === false ? null : this.tryResolveWithPagesPrefix(normalizedPath));
189
190
 
190
191
  this.cache.set(cacheKey, resolved);
191
192
  return resolved;
@@ -9,7 +9,7 @@ import type {
9
9
  InvalidationCallbacks,
10
10
  ResolvedContentContext,
11
11
  } from "./types.js";
12
- import type { FileInfo } from "../../base.js";
12
+ import type { FileInfo, ResolveFileOptions } from "../../base.js";
13
13
  import { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
14
14
  import type { Project } from "../../veryfront-api-client/index.js";
15
15
  import { FileCache } from "../cache/file-cache.js";
@@ -142,6 +142,24 @@ export class VeryfrontFSAdapter implements FSAdapter {
142
142
 
143
143
  return result;
144
144
  },
145
+ hasCachedFileList: async () => {
146
+ if (!this.contentContext) {
147
+ logger.debug("hasCachedFileList: no contentContext");
148
+ return false;
149
+ }
150
+
151
+ const cacheKey = buildFileListCacheKey(this.contentContext);
152
+ const result = await this.cache.getAsync<Array<{ path: string }>>(cacheKey);
153
+ const hasResult = Array.isArray(result) && result.length > 0;
154
+
155
+ logger.debug("hasCachedFileList lookup", {
156
+ cacheKey,
157
+ hasResult,
158
+ resultSize: result?.length ?? 0,
159
+ });
160
+
161
+ return hasResult;
162
+ },
145
163
  isPersistentCacheInvalidated: (prefix: string) => this.isPersistentCacheInvalidated(prefix),
146
164
  isReleaseBeingInvalidated: (releaseId: string) =>
147
165
  this.isPersistentCacheInvalidated(
@@ -410,9 +428,12 @@ export class VeryfrontFSAdapter implements FSAdapter {
410
428
  return this.statOps.exists(path);
411
429
  }
412
430
 
413
- async resolveFile(basePath: string): Promise<string | null> {
431
+ async resolveFile(
432
+ basePath: string,
433
+ options?: ResolveFileOptions,
434
+ ): Promise<string | null> {
414
435
  await this.ensureInitialized();
415
- return this.statOps.resolveFile(basePath);
436
+ return this.statOps.resolveFile(basePath, options);
416
437
  }
417
438
 
418
439
  dispose(): void {
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { logger as baseLogger } from "../../../../utils/index.js";
3
3
  import { INITIALIZATION_ERROR } from "../../../../errors/index.js";
4
4
  import type { DirectoryEntry, FSAdapter, FSAdapterConfig } from "./types.js";
5
- import type { FileInfo } from "../../base.js";
5
+ import type { FileInfo, ResolveFileOptions } from "../../base.js";
6
6
  import { ProxyFSAdapterManager } from "./proxy-manager.js";
7
7
  import type { VeryfrontFSAdapter } from "./index.js";
8
8
  import { runWithCacheBatching } from "../../../../cache/request-cache-batcher.js";
@@ -213,9 +213,12 @@ export class MultiProjectFSAdapter implements FSAdapter {
213
213
  }
214
214
  }
215
215
 
216
- async resolveFile(basePath: string): Promise<string | null> {
216
+ async resolveFile(
217
+ basePath: string,
218
+ options?: ResolveFileOptions,
219
+ ): Promise<string | null> {
217
220
  const adapter = await this.getAdapter();
218
- return adapter.resolveFile(basePath);
221
+ return adapter.resolveFile(basePath, options);
219
222
  }
220
223
 
221
224
  dispose(): void {
@@ -41,6 +41,7 @@ export interface ContentContextProvider {
41
41
  updated_at?: string;
42
42
  }> | undefined
43
43
  >;
44
+ hasCachedFileList?: () => Promise<boolean>;
44
45
  /** True if cache prefix is being deleted - skip persistent cache reads */
45
46
  isPersistentCacheInvalidated?: (prefix: string) => boolean;
46
47
  /** Back-compat: release-scoped invalidation */