veryfront 0.1.86 → 0.1.87

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 (24) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/platform/adapters/fs/veryfront/content-metrics.d.ts +1 -1
  3. package/esm/src/platform/adapters/fs/veryfront/content-metrics.d.ts.map +1 -1
  4. package/esm/src/platform/adapters/fs/veryfront/content-metrics.js +7 -1
  5. package/esm/src/platform/adapters/fs/veryfront/file-list-index.d.ts +10 -0
  6. package/esm/src/platform/adapters/fs/veryfront/file-list-index.d.ts.map +1 -1
  7. package/esm/src/platform/adapters/fs/veryfront/file-list-index.js +83 -19
  8. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts +5 -0
  9. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts.map +1 -1
  10. package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.js +6 -0
  11. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
  12. package/esm/src/platform/adapters/fs/veryfront/read-operations.js +53 -34
  13. package/esm/src/rendering/page-resolution/page-resolver.d.ts.map +1 -1
  14. package/esm/src/rendering/page-resolution/page-resolver.js +10 -4
  15. package/esm/src/utils/version.d.ts +1 -1
  16. package/esm/src/utils/version.js +1 -1
  17. package/package.json +1 -1
  18. package/src/deno.js +1 -1
  19. package/src/src/platform/adapters/fs/veryfront/content-metrics.ts +13 -2
  20. package/src/src/platform/adapters/fs/veryfront/file-list-index.ts +101 -18
  21. package/src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts +10 -0
  22. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +87 -30
  23. package/src/src/rendering/page-resolution/page-resolver.ts +17 -6
  24. package/src/src/utils/version.ts +1 -1
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.86",
3
+ "version": "0.1.87",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -1,4 +1,4 @@
1
- export type MissReason = "cold_start" | "not_in_filelist" | "invalidation" | "no_filelist_cache";
1
+ export type MissReason = "cold_start" | "not_in_filelist" | "invalidation" | "no_filelist_cache" | "indexed_without_content";
2
2
  interface CumulativeMetrics {
3
3
  requestScopedHits: number;
4
4
  persistentCacheHits: number;
@@ -1 +1 @@
1
- {"version":3,"file":"content-metrics.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/content-metrics.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,iBAAiB,GAAG,cAAc,GAAG,mBAAmB,CAAC;AAejG,UAAU,iBAAiB;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAsCD,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,iBAAiB,CAC/B,cAAc,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,IAAI,CAuCN;AAED,KAAK,kBAAkB,GACnB,oBAAoB,GACpB,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,wBAAwB,GACxB,YAAY,CAAC;AAEjB,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GACA,IAAI,CAyCN;AAED,wBAAgB,yBAAyB,IAAI,iBAAiB,GAAG;IAC/D,sBAAsB,EAAE,MAAM,CAAC;CAChC,CAOA;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
1
+ {"version":3,"file":"content-metrics.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/content-metrics.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAClB,YAAY,GACZ,iBAAiB,GACjB,cAAc,GACd,mBAAmB,GACnB,yBAAyB,CAAC;AAe9B,UAAU,iBAAiB;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AA4CD,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,iBAAiB,CAC/B,cAAc,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,IAAI,CAuCN;AAED,KAAK,kBAAkB,GACnB,oBAAoB,GACpB,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,wBAAwB,GACxB,YAAY,CAAC;AAEjB,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE;IACP,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GACA,IAAI,CAyCN;AAED,wBAAgB,yBAAyB,IAAI,iBAAiB,GAAG;IAC/D,sBAAsB,EAAE,MAAM,CAAC;CAChC,CAOA;AAED,wBAAgB,mBAAmB,IAAI,IAAI,CAO1C"}
@@ -20,7 +20,13 @@ function createFreshRequestMetrics() {
20
20
  networkFetches: 0,
21
21
  networkMs: 0,
22
22
  fetchesByType: { page: 0, layout: 0, component: 0, api: 0, data: 0, config: 0, other: 0 },
23
- missReasons: { cold_start: 0, not_in_filelist: 0, invalidation: 0, no_filelist_cache: 0 },
23
+ missReasons: {
24
+ cold_start: 0,
25
+ not_in_filelist: 0,
26
+ invalidation: 0,
27
+ no_filelist_cache: 0,
28
+ indexed_without_content: 0,
29
+ },
24
30
  isPreviewMode: null,
25
31
  filesAccessed: new Set(),
26
32
  };
@@ -2,21 +2,31 @@ interface FileListCacheEntry {
2
2
  path: string;
3
3
  content?: string;
4
4
  }
5
+ export interface FileListMatchResult {
6
+ status: "unavailable" | "missing" | "present_without_content" | "hit";
7
+ fresh: boolean;
8
+ path?: string;
9
+ content?: string;
10
+ }
5
11
  export declare class FileListIndex {
6
12
  private readonly getFileListCache?;
7
13
  private index;
14
+ private pathSet;
8
15
  private indexKey;
9
16
  private indexBuiltAt;
17
+ private indexFresh;
10
18
  private readyPromise;
11
19
  constructor(getFileListCache?: (() => Promise<Array<FileListCacheEntry> | undefined>) | undefined);
12
20
  setReadyPromise(promise: Promise<void>): void;
13
21
  clear(): void;
14
22
  private ensureReady;
15
23
  lookup(normalizedPath: string): Promise<string | undefined>;
24
+ match(normalizedPath: string): Promise<FileListMatchResult>;
16
25
  findFirstWithContent(normalizedPaths: string[]): Promise<{
17
26
  path: string;
18
27
  content: string;
19
28
  } | undefined>;
29
+ findFirstMatch(normalizedPaths: string[]): Promise<FileListMatchResult>;
20
30
  private getOrBuild;
21
31
  }
22
32
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"file-list-index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/file-list-index.ts"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAeD,qBAAa,aAAa;IAOtB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IANpC,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,YAAY,CAA8B;gBAG/B,gBAAgB,CAAC,GAAE,MAAM,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC,aAAA;IAG1F,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI7C,KAAK,IAAI,IAAI;YAUC,WAAW;IAWnB,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IA4B3D,oBAAoB,CACxB,eAAe,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;YAc3C,UAAU;CA0EzB"}
1
+ {"version":3,"file":"file-list-index.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/file-list-index.ts"],"names":[],"mappings":"AAIA,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,yBAAyB,GAAG,KAAK,CAAC;IACtE,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAeD,qBAAa,aAAa;IAStB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IARpC,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAA8B;gBAG/B,gBAAgB,CAAC,GAAE,MAAM,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,SAAS,CAAC,aAAA;IAG1F,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAI7C,KAAK,IAAI,IAAI;YAYC,WAAW;IAWnB,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAK3D,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA8C3D,oBAAoB,CACxB,eAAe,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAMnD,cAAc,CAClB,eAAe,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,mBAAmB,CAAC;YA6BjB,UAAU;CAoGzB"}
@@ -13,8 +13,10 @@ const INDEX_STALENESS_LIMIT_MS = 5 * 60 * 1000; // 5 minutes
13
13
  export class FileListIndex {
14
14
  getFileListCache;
15
15
  index = null;
16
+ pathSet = null;
16
17
  indexKey = null;
17
18
  indexBuiltAt = 0;
19
+ indexFresh = false;
18
20
  readyPromise = null;
19
21
  constructor(getFileListCache) {
20
22
  this.getFileListCache = getFileListCache;
@@ -27,8 +29,10 @@ export class FileListIndex {
27
29
  return;
28
30
  const indexedWithContent = this.index.size;
29
31
  this.index = null;
32
+ this.pathSet = null;
30
33
  this.indexKey = null;
31
34
  this.indexBuiltAt = 0;
35
+ this.indexFresh = false;
32
36
  logger.debug("Cleared file list index", { indexedWithContent });
33
37
  }
34
38
  async ensureReady() {
@@ -43,19 +47,35 @@ export class FileListIndex {
43
47
  }
44
48
  }
45
49
  async lookup(normalizedPath) {
50
+ const match = await this.match(normalizedPath);
51
+ return match.status === "hit" ? match.content : undefined;
52
+ }
53
+ async match(normalizedPath) {
46
54
  await this.ensureReady();
47
- const index = await this.getOrBuild();
48
- if (!index) {
55
+ const snapshot = await this.getOrBuild();
56
+ if (!snapshot) {
49
57
  logger.debug("No file list cache available");
50
- return undefined;
58
+ return { status: "unavailable", fresh: false };
51
59
  }
52
- const content = index.get(normalizedPath);
53
- if (!content) {
60
+ if (!snapshot.paths.has(normalizedPath)) {
54
61
  logger.debug("Content not in file list index", {
55
62
  path: normalizedPath,
56
- indexSize: index.size,
63
+ indexSize: snapshot.content.size,
64
+ fresh: snapshot.fresh,
57
65
  });
58
- return undefined;
66
+ return { status: "missing", fresh: snapshot.fresh };
67
+ }
68
+ const content = snapshot.content.get(normalizedPath);
69
+ if (!content) {
70
+ logger.debug("File list index contains path without inline content", {
71
+ path: normalizedPath,
72
+ fresh: snapshot.fresh,
73
+ });
74
+ return {
75
+ status: "present_without_content",
76
+ fresh: snapshot.fresh,
77
+ path: normalizedPath,
78
+ };
59
79
  }
60
80
  logger.debug("FILE_LIST_CACHE_HIT - serving from file list cache", {
61
81
  path: normalizedPath,
@@ -63,19 +83,43 @@ export class FileListIndex {
63
83
  contentHash: hashPreview(content),
64
84
  contentPreview: previewText(content, 200).replace(/\n/g, "\\n"),
65
85
  });
66
- return content;
86
+ return {
87
+ status: "hit",
88
+ fresh: snapshot.fresh,
89
+ path: normalizedPath,
90
+ content,
91
+ };
67
92
  }
68
93
  async findFirstWithContent(normalizedPaths) {
69
- await this.ensureReady();
70
- const index = await this.getOrBuild();
71
- if (!index)
94
+ const match = await this.findFirstMatch(normalizedPaths);
95
+ if (match.status !== "hit" || !match.path || !match.content)
72
96
  return undefined;
97
+ return { path: match.path, content: match.content };
98
+ }
99
+ async findFirstMatch(normalizedPaths) {
100
+ await this.ensureReady();
101
+ const snapshot = await this.getOrBuild();
102
+ if (!snapshot)
103
+ return { status: "unavailable", fresh: false };
73
104
  for (const path of normalizedPaths) {
74
- const content = index.get(path);
75
- if (content)
76
- return { path, content };
105
+ if (!snapshot.paths.has(path))
106
+ continue;
107
+ const content = snapshot.content.get(path);
108
+ if (content) {
109
+ return {
110
+ status: "hit",
111
+ fresh: snapshot.fresh,
112
+ path,
113
+ content,
114
+ };
115
+ }
116
+ return {
117
+ status: "present_without_content",
118
+ fresh: snapshot.fresh,
119
+ path,
120
+ };
77
121
  }
78
- return undefined;
122
+ return { status: "missing", fresh: snapshot.fresh };
79
123
  }
80
124
  async getOrBuild() {
81
125
  if (!this.getFileListCache) {
@@ -95,7 +139,12 @@ export class FileListIndex {
95
139
  indexSize: this.index.size,
96
140
  indexAgeMs: age,
97
141
  });
98
- return this.index;
142
+ this.indexFresh = false;
143
+ return {
144
+ content: this.index,
145
+ paths: this.pathSet ?? new Set(),
146
+ fresh: false,
147
+ };
99
148
  }
100
149
  logger.debug("getOrBuildFileListIndex: in-memory index too stale, discarding", {
101
150
  indexSize: this.index.size,
@@ -103,7 +152,9 @@ export class FileListIndex {
103
152
  staleLimitMs: INDEX_STALENESS_LIMIT_MS,
104
153
  });
105
154
  this.index = null;
155
+ this.pathSet = null;
106
156
  this.indexKey = null;
157
+ this.indexFresh = false;
107
158
  }
108
159
  logger.debug("[ReadOperations] getOrBuildFileListIndex: getFileListCache returned null/undefined");
109
160
  return null;
@@ -117,18 +168,27 @@ export class FileListIndex {
117
168
  sampleContentPreview: cacheCheckSample?.content?.slice(0, 200)?.replace(/\n/g, "\\n"),
118
169
  });
119
170
  const indexKey = `${fileList.length}:${fileList[0]?.path ?? ""}:${fileList[fileList.length - 1]?.path ?? ""}`;
120
- if (this.index && this.indexKey === indexKey) {
171
+ if (this.index && this.pathSet && this.indexKey === indexKey) {
121
172
  this.indexBuiltAt = Date.now();
122
- return this.index;
173
+ this.indexFresh = true;
174
+ return {
175
+ content: this.index,
176
+ paths: this.pathSet,
177
+ fresh: true,
178
+ };
123
179
  }
124
180
  const index = new Map();
181
+ const pathSet = new Set();
125
182
  for (const file of fileList) {
183
+ pathSet.add(file.path);
126
184
  if (file.content)
127
185
  index.set(file.path, file.content);
128
186
  }
129
187
  this.index = index;
188
+ this.pathSet = pathSet;
130
189
  this.indexKey = indexKey;
131
190
  this.indexBuiltAt = Date.now();
191
+ this.indexFresh = true;
132
192
  const sampleFile = fileList.find((f) => /welcome/i.test(f.path));
133
193
  const sampleContent = sampleFile?.content;
134
194
  logger.debug("Built file list index", {
@@ -139,6 +199,10 @@ export class FileListIndex {
139
199
  sampleContentHash: sampleContent ? hashPreview(sampleContent) : undefined,
140
200
  sampleContentPreview: sampleContent?.slice(0, 200)?.replace(/\n/g, "\\n"),
141
201
  });
142
- return index;
202
+ return {
203
+ content: index,
204
+ paths: pathSet,
205
+ fresh: true,
206
+ };
143
207
  }
144
208
  }
@@ -1,6 +1,9 @@
1
1
  import { READ_OPERATION_EXTENSION_PRIORITY } from "./extension-priority.js";
2
2
  import type { ResolvedContentContext } from "./types.js";
3
3
  export { READ_OPERATION_EXTENSION_PRIORITY };
4
+ export type NotFoundLikeError = Error & {
5
+ code?: string;
6
+ };
4
7
  interface ReadContextProviderLike {
5
8
  isProductionMode: () => boolean;
6
9
  isPersistentCacheInvalidated?: (prefix: string) => boolean;
@@ -28,9 +31,11 @@ interface BuildReadFetchStateOptions {
28
31
  export declare function assertProjectSourcePath(normalizedPath: string): void;
29
32
  export declare function buildReadFetchState(options: BuildReadFetchStateOptions): ReadFetchState;
30
33
  export declare function getResolvedCacheKey(cacheKeyPrefix: string, normalizedResolvedPath: string): string;
34
+ export declare function buildExtensionCandidatePaths(basePath: string): string[];
31
35
  export declare function splitKnownFileExtension(apiPath: string): {
32
36
  originalExtension: string;
33
37
  basePath: string;
34
38
  } | null;
35
39
  export declare function isNotFoundLikeError(error: unknown): boolean;
40
+ export declare function createNotFoundLikeError(path: string): NotFoundLikeError;
36
41
  //# sourceMappingURL=read-operations-helpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"read-operations-helpers.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iCAAiC,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,iCAAiC,EAAE,CAAC;AAE7C,UAAU,uBAAuB;IAC/B,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,4BAA4B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3D,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5D;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,oBAAoB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,UAAU,0BAA0B;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAOpE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,cAAc,CA2BvF;AAED,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,EACtB,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAER;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,GACd;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASxD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG3D"}
1
+ {"version":3,"file":"read-operations-helpers.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iCAAiC,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,iCAAiC,EAAE,CAAC;AAE7C,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,UAAU,uBAAuB;IAC/B,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,4BAA4B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3D,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5D;AAED,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,oBAAoB,EAAE,OAAO,GAAG,SAAS,CAAC;IAC1C,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAED,UAAU,0BAA0B;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAOpE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,cAAc,CA2BvF;AAED,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,EACtB,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAER;AAED,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvE;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,GACd;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASxD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG3D;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAEvE"}
@@ -40,6 +40,9 @@ export function buildReadFetchState(options) {
40
40
  export function getResolvedCacheKey(cacheKeyPrefix, normalizedResolvedPath) {
41
41
  return `${cacheKeyPrefix}:${normalizedResolvedPath}`;
42
42
  }
43
+ export function buildExtensionCandidatePaths(basePath) {
44
+ return READ_OPERATION_EXTENSION_PRIORITY.map((ext) => `${basePath}${ext}`);
45
+ }
43
46
  export function splitKnownFileExtension(apiPath) {
44
47
  const extMatch = apiPath.match(/\.(tsx|ts|jsx|js|mdx|md)$/);
45
48
  if (!extMatch)
@@ -54,3 +57,6 @@ export function isNotFoundLikeError(error) {
54
57
  const errorMessage = error instanceof Error ? error.message : String(error);
55
58
  return errorMessage.includes("404") || errorMessage.includes("Not Found");
56
59
  }
60
+ export function createNotFoundLikeError(path) {
61
+ return Object.assign(new Error(`404 Not Found: ${path}`), { code: "ENOENT" });
62
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"read-operations.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/read-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAStD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,sBAAsB,GAAG,IAAI,CAAC;IACvD,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,OAAO,CACzB,KAAK,CAAC;QACJ,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,GAAG,SAAS,CACf,CAAC;IACF,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,0EAA0E;IAC1E,4BAA4B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3D,+CAA+C;IAC/C,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5D;AAUD,qBAAa,cAAc;IAWvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAfpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAI9B;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,mGAAmG;IACnG,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAA6B;gBAGnD,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,SAAS,EAChB,UAAU,EAAE,cAAc,EAC1B,eAAe,CAAC,EAAE,sBAAsB,YAAA,EACxC,kBAAkB,CAAC,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,aAAA,EAC7C,gBAAgB,CAAC,GAAE,MAAM,OAAO,CAC/C,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,SAAS,CACtD,aAAA;IAKH,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIrD,kBAAkB,IAAI,IAAI;IAK1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAY3C,OAAO,CAAC,mBAAmB;YAsBb,+BAA+B;YA0C/B,mBAAmB;IAgDjC,OAAO,CAAC,kBAAkB;YAoFZ,2BAA2B;YA8D3B,uCAAuC;IAmCrD,OAAO,CAAC,oBAAoB;YAgBd,YAAY;YAgGZ,qBAAqB;YA8DrB,qBAAqB;YAgDrB,+BAA+B;YA8D/B,iBAAiB;CAyBhC"}
1
+ {"version":3,"file":"read-operations.d.ts","sourceRoot":"","sources":["../../../../../../src/src/platform/adapters/fs/veryfront/read-operations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAWtD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAI9B,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC;IAChC,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,sBAAsB,GAAG,IAAI,CAAC;IACvD,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,OAAO,CACzB,KAAK,CAAC;QACJ,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,GAAG,SAAS,CACf,CAAC;IACF,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,0EAA0E;IAC1E,4BAA4B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3D,+CAA+C;IAC/C,yBAAyB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5D;AAUD,qBAAa,cAAc;IAWvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC;IAfpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAI9B;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,mGAAmG;IACnG,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAA6B;gBAGnD,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,SAAS,EAChB,UAAU,EAAE,cAAc,EAC1B,eAAe,CAAC,EAAE,sBAAsB,YAAA,EACxC,kBAAkB,CAAC,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,aAAA,EAC7C,gBAAgB,CAAC,GAAE,MAAM,OAAO,CAC/C,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,SAAS,CACtD,aAAA;IAKH,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;IAIrD,kBAAkB,IAAI,IAAI;IAK1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAY3C,OAAO,CAAC,mBAAmB;YAsBb,+BAA+B;YA0C/B,mBAAmB;IA0CjC,OAAO,CAAC,kBAAkB;YAoFZ,2BAA2B;YA8D3B,uCAAuC;IAmCrD,OAAO,CAAC,oBAAoB;YAgBd,YAAY;YA6JZ,qBAAqB;YA8DrB,qBAAqB;YAgDrB,+BAA+B;YA8D/B,iBAAiB;CAyBhC"}
@@ -4,7 +4,7 @@ import { logContentMetric } from "./content-metrics.js";
4
4
  import { FileListIndex } from "./file-list-index.js";
5
5
  import { InFlightRequestDeduper } from "./in-flight-dedupe.js";
6
6
  import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adapter.js";
7
- import { assertProjectSourcePath, buildReadFetchState, getResolvedCacheKey, isNotFoundLikeError, READ_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY, splitKnownFileExtension, } from "./read-operations-helpers.js";
7
+ import { assertProjectSourcePath, buildExtensionCandidatePaths, buildReadFetchState, createNotFoundLikeError, getResolvedCacheKey, isNotFoundLikeError, READ_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY, splitKnownFileExtension, } from "./read-operations-helpers.js";
8
8
  export { endRequestMetrics, getContentMetricsSnapshot, resetContentMetrics, startRequestMetrics, } from "./content-metrics.js";
9
9
  const logger = baseLogger.component("read-operations");
10
10
  const IN_FLIGHT_REQUEST_TIMEOUT_MS = 15_000;
@@ -113,10 +113,15 @@ export class ReadOperations {
113
113
  // - File list is refreshed on every WebSocket poke (websocket-manager.ts:483-500)
114
114
  // - Request-scoped cache ensures consistency within a single render
115
115
  // - Persistent cache is only written for production mode (to avoid staleness risk in preview)
116
- if (!skipPersistentCaches) {
117
- const fileListContent = await this.fileListIndex.lookup(normalizedPath);
118
- if (!fileListContent)
119
- return null;
116
+ if (skipPersistentCaches) {
117
+ logger.debug("Skipping file list cache due to invalidation", {
118
+ path: normalizedPath,
119
+ cacheKeyPrefix,
120
+ });
121
+ return { status: "unavailable", fresh: false };
122
+ }
123
+ const match = await this.fileListIndex.match(normalizedPath);
124
+ if (match.status === "hit" && match.content) {
120
125
  logContentMetric("FILE_LIST_HIT", {
121
126
  path: normalizedPath,
122
127
  mode: ctx?.sourceType ?? "unknown",
@@ -126,25 +131,13 @@ export class ReadOperations {
126
131
  // Only cache to persistent storage for production mode
127
132
  // Preview mode uses file list cache directly without persisting (fresher, WebSocket-driven)
128
133
  if (isProduction) {
129
- this.cache.set(cacheKey, fileListContent);
134
+ this.cache.set(cacheKey, match.content);
130
135
  }
131
- setRequestScopedFile(cacheKey, fileListContent);
132
- return fileListContent;
136
+ setRequestScopedFile(cacheKey, match.content);
133
137
  }
134
- // Skip only happens during cache invalidation (both preview and production)
135
- logContentMetric("CACHE_MISS", {
136
- path: normalizedPath,
137
- mode: ctx?.sourceType ?? "unknown",
138
- missReason: "invalidation",
139
- isPreviewMode,
140
- });
141
- logger.debug("Skipping file list cache due to invalidation", {
142
- path: normalizedPath,
143
- cacheKeyPrefix,
144
- });
145
- return null;
138
+ return match;
146
139
  }
147
- setupInFlightFetch(normalizedPath, apiPath, cacheKey, isPublished, isProduction, isPreviewMode, ctx) {
140
+ setupInFlightFetch(normalizedPath, apiPath, cacheKey, isPublished, isProduction, isPreviewMode, ctx, missReason) {
148
141
  const cleanupResult = this.inFlightRequests.cleanup();
149
142
  if (cleanupResult) {
150
143
  logger.warn("Cleaned up in-flight requests", cleanupResult);
@@ -159,11 +152,10 @@ export class ReadOperations {
159
152
  return existingEntry.promise;
160
153
  }
161
154
  // Track why we're making a network fetch (for optimization analysis)
162
- const hasFileListCache = !!this.getFileListCache;
163
155
  logContentMetric("CACHE_MISS", {
164
156
  path: normalizedPath,
165
157
  mode: ctx?.sourceType ?? "unknown",
166
- missReason: (hasFileListCache ? "not_in_filelist" : "no_filelist_cache"),
158
+ missReason,
167
159
  isPreviewMode,
168
160
  });
169
161
  // THIS IS A NETWORK FETCH - every call here = API round trip
@@ -248,10 +240,10 @@ export class ReadOperations {
248
240
  }
249
241
  }
250
242
  async tryResolveExtensionlessPathFromFileList(normalizedPath, cacheKeyPrefix, cacheKey, isProduction, ctx, isPreviewMode) {
251
- const candidatePaths = EXTENSION_PRIORITY.map((ext) => `${normalizedPath}${ext}`);
252
- const resolved = await this.fileListIndex.findFirstWithContent(candidatePaths);
253
- if (!resolved)
254
- return null;
243
+ const candidatePaths = buildExtensionCandidatePaths(normalizedPath);
244
+ const resolved = await this.fileListIndex.findFirstMatch(candidatePaths);
245
+ if (resolved.status !== "hit" || !resolved.path || !resolved.content)
246
+ return resolved;
255
247
  const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolved.path);
256
248
  this.extensionResolutionCache.set(normalizedPath, resolved.path);
257
249
  logContentMetric("FILE_LIST_HIT", {
@@ -268,7 +260,7 @@ export class ReadOperations {
268
260
  resolvedCacheKey: resolvedCacheKey === cacheKey ? undefined : resolvedCacheKey,
269
261
  });
270
262
  this.cacheResolvedContent(cacheKey, resolvedCacheKey, resolved.content, isProduction);
271
- return resolved.content;
263
+ return resolved;
272
264
  }
273
265
  cacheResolvedContent(cacheKey, resolvedCacheKey, content, persistToCache) {
274
266
  if (persistToCache) {
@@ -309,20 +301,47 @@ export class ReadOperations {
309
301
  const persistentCached = await this.getProductionPersistentCacheHit(normalizedPath, cacheKeyPrefix, cacheKey, isProduction, skipPersistentCaches, currentReleaseId, isPrefixInvalidated, ctx);
310
302
  if (persistentCached)
311
303
  return persistentCached;
312
- const fileListCached = await this.getFileListCacheHit(normalizedPath, cacheKeyPrefix, cacheKey, isProduction, skipPersistentCaches, isPreviewMode, ctx);
313
- if (fileListCached)
314
- return fileListCached;
304
+ const fileListMatch = await this.getFileListCacheHit(normalizedPath, cacheKeyPrefix, cacheKey, isProduction, skipPersistentCaches, isPreviewMode, ctx);
305
+ if (fileListMatch.status === "hit" && fileListMatch.content)
306
+ return fileListMatch.content;
307
+ if (fileListMatch.status === "present_without_content") {
308
+ return this.setupInFlightFetch(normalizedPath, apiPath, cacheKey, isPublished, isProduction, isPreviewMode, ctx, "indexed_without_content");
309
+ }
315
310
  if (!hasKnownExt) {
316
311
  if (!skipPersistentCaches) {
317
312
  const resolvedFromFileList = await this.tryResolveExtensionlessPathFromFileList(normalizedPath, cacheKeyPrefix, cacheKey, isProduction, ctx, isPreviewMode);
318
- if (resolvedFromFileList)
319
- return resolvedFromFileList;
313
+ if (resolvedFromFileList.status === "hit" && resolvedFromFileList.content) {
314
+ return resolvedFromFileList.content;
315
+ }
316
+ if (resolvedFromFileList.status === "present_without_content" &&
317
+ resolvedFromFileList.path) {
318
+ const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolvedFromFileList.path);
319
+ const resolvedApiPath = this.getOriginalApiPath?.(resolvedFromFileList.path) ??
320
+ resolvedFromFileList.path;
321
+ const fetchedResolved = await this.setupInFlightFetch(resolvedFromFileList.path, resolvedApiPath, resolvedCacheKey, isPublished, isProduction, isPreviewMode, ctx, "indexed_without_content");
322
+ this.extensionResolutionCache.set(normalizedPath, resolvedFromFileList.path);
323
+ this.cacheResolvedContent(cacheKey, resolvedCacheKey, fetchedResolved, isProduction && !skipPersistentCaches);
324
+ return fetchedResolved;
325
+ }
326
+ if (fileListMatch.status === "missing" &&
327
+ fileListMatch.fresh &&
328
+ resolvedFromFileList.status === "missing" &&
329
+ resolvedFromFileList.fresh) {
330
+ throw createNotFoundLikeError(normalizedPath);
331
+ }
320
332
  }
321
333
  const resolved = await this.tryResolveExtensionlessPath(apiPath, cacheKeyPrefix, cacheKey, isProduction, skipPersistentCaches);
322
334
  if (resolved)
323
335
  return resolved;
324
336
  }
325
- return this.setupInFlightFetch(normalizedPath, apiPath, cacheKey, isPublished, isProduction, isPreviewMode, ctx);
337
+ if (fileListMatch.status === "missing" && fileListMatch.fresh) {
338
+ throw createNotFoundLikeError(normalizedPath);
339
+ }
340
+ return this.setupInFlightFetch(normalizedPath, apiPath, cacheKey, isPublished, isProduction, isPreviewMode, ctx, skipPersistentCaches
341
+ ? "invalidation"
342
+ : fileListMatch.status === "missing" && fileListMatch.fresh
343
+ ? "not_in_filelist"
344
+ : "no_filelist_cache");
326
345
  }
327
346
  async fetchPublishedContent(normalizedPath, apiPath, cacheKey, releaseId, environmentName, shouldCache) {
328
347
  logger.debug("Fetching published content", {
@@ -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;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"}
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;IAiExC,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"}
@@ -56,10 +56,16 @@ export class PageResolver {
56
56
  }
57
57
  }
58
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/.
61
- pageInfo = await getAppRouteEntity(this.projectDir, slug, this.adapter, appDirName);
62
- if (!pageInfo) {
59
+ // Auto mode stays structural: detect the dominant router once, then keep
60
+ // pages fallback available for mixed or in-transition projects.
61
+ const useAppRouter = await detectAppRouter(this.projectDir, this.config, this.adapter, { projectId: this.projectId });
62
+ if (useAppRouter) {
63
+ pageInfo = await getAppRouteEntity(this.projectDir, slug, this.adapter, appDirName);
64
+ if (!pageInfo) {
65
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
66
+ }
67
+ }
68
+ else {
63
69
  pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
64
70
  }
65
71
  }
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.1.59";
1
+ export declare const VERSION = "0.1.87";
2
2
  export declare const SERVER_START_TIME: number;
3
3
  export interface BuildVersion {
4
4
  framework: string;
@@ -1,6 +1,6 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.59";
3
+ export const VERSION = "0.1.87";
4
4
  export const SERVER_START_TIME = Date.now();
5
5
  export function createBuildVersion(projectUpdatedAt) {
6
6
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.86",
3
+ "version": "0.1.87",
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.86",
3
+ "version": "0.1.87",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -8,7 +8,12 @@ import {
8
8
  const logger = baseLogger.component("content-metrics");
9
9
 
10
10
  type FileType = "page" | "layout" | "component" | "api" | "data" | "config" | "other";
11
- export type MissReason = "cold_start" | "not_in_filelist" | "invalidation" | "no_filelist_cache";
11
+ export type MissReason =
12
+ | "cold_start"
13
+ | "not_in_filelist"
14
+ | "invalidation"
15
+ | "no_filelist_cache"
16
+ | "indexed_without_content";
12
17
 
13
18
  interface PerRequestMetrics {
14
19
  startTime: number;
@@ -52,7 +57,13 @@ function createFreshRequestMetrics(): PerRequestMetrics {
52
57
  networkFetches: 0,
53
58
  networkMs: 0,
54
59
  fetchesByType: { page: 0, layout: 0, component: 0, api: 0, data: 0, config: 0, other: 0 },
55
- missReasons: { cold_start: 0, not_in_filelist: 0, invalidation: 0, no_filelist_cache: 0 },
60
+ missReasons: {
61
+ cold_start: 0,
62
+ not_in_filelist: 0,
63
+ invalidation: 0,
64
+ no_filelist_cache: 0,
65
+ indexed_without_content: 0,
66
+ },
56
67
  isPreviewMode: null,
57
68
  filesAccessed: new Set(),
58
69
  };
@@ -7,6 +7,13 @@ interface FileListCacheEntry {
7
7
  content?: string;
8
8
  }
9
9
 
10
+ export interface FileListMatchResult {
11
+ status: "unavailable" | "missing" | "present_without_content" | "hit";
12
+ fresh: boolean;
13
+ path?: string;
14
+ content?: string;
15
+ }
16
+
10
17
  function hashPreview(content: string): number {
11
18
  return content
12
19
  .slice(0, 100)
@@ -22,8 +29,10 @@ const INDEX_STALENESS_LIMIT_MS = 5 * 60 * 1000; // 5 minutes
22
29
 
23
30
  export class FileListIndex {
24
31
  private index: Map<string, string> | null = null;
32
+ private pathSet: Set<string> | null = null;
25
33
  private indexKey: string | null = null;
26
34
  private indexBuiltAt = 0;
35
+ private indexFresh = false;
27
36
  private readyPromise: Promise<void> | null = null;
28
37
 
29
38
  constructor(
@@ -39,8 +48,10 @@ export class FileListIndex {
39
48
 
40
49
  const indexedWithContent = this.index.size;
41
50
  this.index = null;
51
+ this.pathSet = null;
42
52
  this.indexKey = null;
43
53
  this.indexBuiltAt = 0;
54
+ this.indexFresh = false;
44
55
  logger.debug("Cleared file list index", { indexedWithContent });
45
56
  }
46
57
 
@@ -56,21 +67,39 @@ export class FileListIndex {
56
67
  }
57
68
 
58
69
  async lookup(normalizedPath: string): Promise<string | undefined> {
70
+ const match = await this.match(normalizedPath);
71
+ return match.status === "hit" ? match.content : undefined;
72
+ }
73
+
74
+ async match(normalizedPath: string): Promise<FileListMatchResult> {
59
75
  await this.ensureReady();
60
76
 
61
- const index = await this.getOrBuild();
62
- if (!index) {
77
+ const snapshot = await this.getOrBuild();
78
+ if (!snapshot) {
63
79
  logger.debug("No file list cache available");
64
- return undefined;
80
+ return { status: "unavailable", fresh: false };
65
81
  }
66
82
 
67
- const content = index.get(normalizedPath);
68
- if (!content) {
83
+ if (!snapshot.paths.has(normalizedPath)) {
69
84
  logger.debug("Content not in file list index", {
70
85
  path: normalizedPath,
71
- indexSize: index.size,
86
+ indexSize: snapshot.content.size,
87
+ fresh: snapshot.fresh,
72
88
  });
73
- return undefined;
89
+ return { status: "missing", fresh: snapshot.fresh };
90
+ }
91
+
92
+ const content = snapshot.content.get(normalizedPath);
93
+ if (!content) {
94
+ logger.debug("File list index contains path without inline content", {
95
+ path: normalizedPath,
96
+ fresh: snapshot.fresh,
97
+ });
98
+ return {
99
+ status: "present_without_content",
100
+ fresh: snapshot.fresh,
101
+ path: normalizedPath,
102
+ };
74
103
  }
75
104
 
76
105
  logger.debug("FILE_LIST_CACHE_HIT - serving from file list cache", {
@@ -80,26 +109,60 @@ export class FileListIndex {
80
109
  contentPreview: previewText(content, 200).replace(/\n/g, "\\n"),
81
110
  });
82
111
 
83
- return content;
112
+ return {
113
+ status: "hit",
114
+ fresh: snapshot.fresh,
115
+ path: normalizedPath,
116
+ content,
117
+ };
84
118
  }
85
119
 
86
120
  async findFirstWithContent(
87
121
  normalizedPaths: string[],
88
122
  ): Promise<{ path: string; content: string } | undefined> {
123
+ const match = await this.findFirstMatch(normalizedPaths);
124
+ if (match.status !== "hit" || !match.path || !match.content) return undefined;
125
+ return { path: match.path, content: match.content };
126
+ }
127
+
128
+ async findFirstMatch(
129
+ normalizedPaths: string[],
130
+ ): Promise<FileListMatchResult> {
89
131
  await this.ensureReady();
90
132
 
91
- const index = await this.getOrBuild();
92
- if (!index) return undefined;
133
+ const snapshot = await this.getOrBuild();
134
+ if (!snapshot) return { status: "unavailable", fresh: false };
93
135
 
94
136
  for (const path of normalizedPaths) {
95
- const content = index.get(path);
96
- if (content) return { path, content };
137
+ if (!snapshot.paths.has(path)) continue;
138
+
139
+ const content = snapshot.content.get(path);
140
+ if (content) {
141
+ return {
142
+ status: "hit",
143
+ fresh: snapshot.fresh,
144
+ path,
145
+ content,
146
+ };
147
+ }
148
+
149
+ return {
150
+ status: "present_without_content",
151
+ fresh: snapshot.fresh,
152
+ path,
153
+ };
97
154
  }
98
155
 
99
- return undefined;
156
+ return { status: "missing", fresh: snapshot.fresh };
100
157
  }
101
158
 
102
- private async getOrBuild(): Promise<Map<string, string> | null> {
159
+ private async getOrBuild(): Promise<
160
+ {
161
+ content: Map<string, string>;
162
+ paths: Set<string>;
163
+ fresh: boolean;
164
+ } | null
165
+ > {
103
166
  if (!this.getFileListCache) {
104
167
  logger.debug("getOrBuildFileListIndex: no getFileListCache function");
105
168
  return null;
@@ -118,7 +181,12 @@ export class FileListIndex {
118
181
  indexSize: this.index.size,
119
182
  indexAgeMs: age,
120
183
  });
121
- return this.index;
184
+ this.indexFresh = false;
185
+ return {
186
+ content: this.index,
187
+ paths: this.pathSet ?? new Set<string>(),
188
+ fresh: false,
189
+ };
122
190
  }
123
191
  logger.debug("getOrBuildFileListIndex: in-memory index too stale, discarding", {
124
192
  indexSize: this.index.size,
@@ -126,7 +194,9 @@ export class FileListIndex {
126
194
  staleLimitMs: INDEX_STALENESS_LIMIT_MS,
127
195
  });
128
196
  this.index = null;
197
+ this.pathSet = null;
129
198
  this.indexKey = null;
199
+ this.indexFresh = false;
130
200
  }
131
201
  logger.debug(
132
202
  "[ReadOperations] getOrBuildFileListIndex: getFileListCache returned null/undefined",
@@ -146,19 +216,28 @@ export class FileListIndex {
146
216
  const indexKey = `${fileList.length}:${fileList[0]?.path ?? ""}:${
147
217
  fileList[fileList.length - 1]?.path ?? ""
148
218
  }`;
149
- if (this.index && this.indexKey === indexKey) {
219
+ if (this.index && this.pathSet && this.indexKey === indexKey) {
150
220
  this.indexBuiltAt = Date.now();
151
- return this.index;
221
+ this.indexFresh = true;
222
+ return {
223
+ content: this.index,
224
+ paths: this.pathSet,
225
+ fresh: true,
226
+ };
152
227
  }
153
228
 
154
229
  const index = new Map<string, string>();
230
+ const pathSet = new Set<string>();
155
231
  for (const file of fileList) {
232
+ pathSet.add(file.path);
156
233
  if (file.content) index.set(file.path, file.content);
157
234
  }
158
235
 
159
236
  this.index = index;
237
+ this.pathSet = pathSet;
160
238
  this.indexKey = indexKey;
161
239
  this.indexBuiltAt = Date.now();
240
+ this.indexFresh = true;
162
241
 
163
242
  const sampleFile = fileList.find((f) => /welcome/i.test(f.path));
164
243
  const sampleContent = sampleFile?.content;
@@ -171,6 +250,10 @@ export class FileListIndex {
171
250
  sampleContentPreview: sampleContent?.slice(0, 200)?.replace(/\n/g, "\\n"),
172
251
  });
173
252
 
174
- return index;
253
+ return {
254
+ content: index,
255
+ paths: pathSet,
256
+ fresh: true,
257
+ };
175
258
  }
176
259
  }
@@ -6,6 +6,8 @@ import type { ResolvedContentContext } from "./types.js";
6
6
 
7
7
  export { READ_OPERATION_EXTENSION_PRIORITY };
8
8
 
9
+ export type NotFoundLikeError = Error & { code?: string };
10
+
9
11
  interface ReadContextProviderLike {
10
12
  isProductionMode: () => boolean;
11
13
  isPersistentCacheInvalidated?: (prefix: string) => boolean;
@@ -78,6 +80,10 @@ export function getResolvedCacheKey(
78
80
  return `${cacheKeyPrefix}:${normalizedResolvedPath}`;
79
81
  }
80
82
 
83
+ export function buildExtensionCandidatePaths(basePath: string): string[] {
84
+ return READ_OPERATION_EXTENSION_PRIORITY.map((ext) => `${basePath}${ext}`);
85
+ }
86
+
81
87
  export function splitKnownFileExtension(
82
88
  apiPath: string,
83
89
  ): { originalExtension: string; basePath: string } | null {
@@ -95,3 +101,7 @@ export function isNotFoundLikeError(error: unknown): boolean {
95
101
  const errorMessage = error instanceof Error ? error.message : String(error);
96
102
  return errorMessage.includes("404") || errorMessage.includes("Not Found");
97
103
  }
104
+
105
+ export function createNotFoundLikeError(path: string): NotFoundLikeError {
106
+ return Object.assign(new Error(`404 Not Found: ${path}`), { code: "ENOENT" });
107
+ }
@@ -3,13 +3,15 @@ import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
3
3
  import type { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
4
4
  import { FileCache } from "../cache/file-cache.js";
5
5
  import { logContentMetric, type MissReason } from "./content-metrics.js";
6
- import { FileListIndex } from "./file-list-index.js";
6
+ import { FileListIndex, type FileListMatchResult } from "./file-list-index.js";
7
7
  import { InFlightRequestDeduper } from "./in-flight-dedupe.js";
8
8
  import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adapter.js";
9
9
  import { PathNormalizer } from "./path-normalizer.js";
10
10
  import {
11
11
  assertProjectSourcePath,
12
+ buildExtensionCandidatePaths,
12
13
  buildReadFetchState,
14
+ createNotFoundLikeError,
13
15
  getResolvedCacheKey,
14
16
  isNotFoundLikeError,
15
17
  READ_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY,
@@ -184,17 +186,23 @@ export class ReadOperations {
184
186
  skipPersistentCaches: boolean,
185
187
  isPreviewMode: boolean,
186
188
  ctx: ResolvedContentContext | null,
187
- ): Promise<string | null> {
189
+ ): Promise<FileListMatchResult> {
188
190
  // File list cache is enabled for BOTH preview and production modes.
189
191
  // The file list is an in-memory index built from API response at init, updated by WebSocket pokes.
190
192
  // This is safe because:
191
193
  // - File list is refreshed on every WebSocket poke (websocket-manager.ts:483-500)
192
194
  // - Request-scoped cache ensures consistency within a single render
193
195
  // - Persistent cache is only written for production mode (to avoid staleness risk in preview)
194
- if (!skipPersistentCaches) {
195
- const fileListContent = await this.fileListIndex.lookup(normalizedPath);
196
- if (!fileListContent) return null;
196
+ if (skipPersistentCaches) {
197
+ logger.debug("Skipping file list cache due to invalidation", {
198
+ path: normalizedPath,
199
+ cacheKeyPrefix,
200
+ });
201
+ return { status: "unavailable", fresh: false };
202
+ }
197
203
 
204
+ const match = await this.fileListIndex.match(normalizedPath);
205
+ if (match.status === "hit" && match.content) {
198
206
  logContentMetric("FILE_LIST_HIT", {
199
207
  path: normalizedPath,
200
208
  mode: ctx?.sourceType ?? "unknown",
@@ -204,24 +212,12 @@ export class ReadOperations {
204
212
  // Only cache to persistent storage for production mode
205
213
  // Preview mode uses file list cache directly without persisting (fresher, WebSocket-driven)
206
214
  if (isProduction) {
207
- this.cache.set(cacheKey, fileListContent);
215
+ this.cache.set(cacheKey, match.content);
208
216
  }
209
- setRequestScopedFile(cacheKey, fileListContent);
210
- return fileListContent;
217
+ setRequestScopedFile(cacheKey, match.content);
211
218
  }
212
219
 
213
- // Skip only happens during cache invalidation (both preview and production)
214
- logContentMetric("CACHE_MISS", {
215
- path: normalizedPath,
216
- mode: ctx?.sourceType ?? "unknown",
217
- missReason: "invalidation" as MissReason,
218
- isPreviewMode,
219
- });
220
- logger.debug("Skipping file list cache due to invalidation", {
221
- path: normalizedPath,
222
- cacheKeyPrefix,
223
- });
224
- return null;
220
+ return match;
225
221
  }
226
222
 
227
223
  private setupInFlightFetch(
@@ -232,6 +228,7 @@ export class ReadOperations {
232
228
  isProduction: boolean,
233
229
  isPreviewMode: boolean,
234
230
  ctx: ResolvedContentContext | null,
231
+ missReason: MissReason,
235
232
  ): Promise<string> {
236
233
  const cleanupResult = this.inFlightRequests.cleanup();
237
234
  if (cleanupResult) {
@@ -249,11 +246,10 @@ export class ReadOperations {
249
246
  }
250
247
 
251
248
  // Track why we're making a network fetch (for optimization analysis)
252
- const hasFileListCache = !!this.getFileListCache;
253
249
  logContentMetric("CACHE_MISS", {
254
250
  path: normalizedPath,
255
251
  mode: ctx?.sourceType ?? "unknown",
256
- missReason: (hasFileListCache ? "not_in_filelist" : "no_filelist_cache") as MissReason,
252
+ missReason,
257
253
  isPreviewMode,
258
254
  });
259
255
 
@@ -377,10 +373,10 @@ export class ReadOperations {
377
373
  isProduction: boolean,
378
374
  ctx: ResolvedContentContext | null,
379
375
  isPreviewMode: boolean,
380
- ): Promise<string | null> {
381
- const candidatePaths = EXTENSION_PRIORITY.map((ext) => `${normalizedPath}${ext}`);
382
- const resolved = await this.fileListIndex.findFirstWithContent(candidatePaths);
383
- if (!resolved) return null;
376
+ ): Promise<FileListMatchResult> {
377
+ const candidatePaths = buildExtensionCandidatePaths(normalizedPath);
378
+ const resolved = await this.fileListIndex.findFirstMatch(candidatePaths);
379
+ if (resolved.status !== "hit" || !resolved.path || !resolved.content) return resolved;
384
380
 
385
381
  const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolved.path);
386
382
 
@@ -402,7 +398,7 @@ export class ReadOperations {
402
398
 
403
399
  this.cacheResolvedContent(cacheKey, resolvedCacheKey, resolved.content, isProduction);
404
400
 
405
- return resolved.content;
401
+ return resolved;
406
402
  }
407
403
 
408
404
  private cacheResolvedContent(
@@ -472,7 +468,7 @@ export class ReadOperations {
472
468
  );
473
469
  if (persistentCached) return persistentCached;
474
470
 
475
- const fileListCached = await this.getFileListCacheHit(
471
+ const fileListMatch = await this.getFileListCacheHit(
476
472
  normalizedPath,
477
473
  cacheKeyPrefix,
478
474
  cacheKey,
@@ -481,7 +477,19 @@ export class ReadOperations {
481
477
  isPreviewMode,
482
478
  ctx,
483
479
  );
484
- if (fileListCached) return fileListCached;
480
+ if (fileListMatch.status === "hit" && fileListMatch.content) return fileListMatch.content;
481
+ if (fileListMatch.status === "present_without_content") {
482
+ return this.setupInFlightFetch(
483
+ normalizedPath,
484
+ apiPath,
485
+ cacheKey,
486
+ isPublished,
487
+ isProduction,
488
+ isPreviewMode,
489
+ ctx,
490
+ "indexed_without_content",
491
+ );
492
+ }
485
493
 
486
494
  if (!hasKnownExt) {
487
495
  if (!skipPersistentCaches) {
@@ -493,7 +501,47 @@ export class ReadOperations {
493
501
  ctx,
494
502
  isPreviewMode,
495
503
  );
496
- if (resolvedFromFileList) return resolvedFromFileList;
504
+ if (resolvedFromFileList.status === "hit" && resolvedFromFileList.content) {
505
+ return resolvedFromFileList.content;
506
+ }
507
+
508
+ if (
509
+ resolvedFromFileList.status === "present_without_content" &&
510
+ resolvedFromFileList.path
511
+ ) {
512
+ const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolvedFromFileList.path);
513
+ const resolvedApiPath = this.getOriginalApiPath?.(resolvedFromFileList.path) ??
514
+ resolvedFromFileList.path;
515
+ const fetchedResolved = await this.setupInFlightFetch(
516
+ resolvedFromFileList.path,
517
+ resolvedApiPath,
518
+ resolvedCacheKey,
519
+ isPublished,
520
+ isProduction,
521
+ isPreviewMode,
522
+ ctx,
523
+ "indexed_without_content",
524
+ );
525
+
526
+ this.extensionResolutionCache.set(normalizedPath, resolvedFromFileList.path);
527
+ this.cacheResolvedContent(
528
+ cacheKey,
529
+ resolvedCacheKey,
530
+ fetchedResolved,
531
+ isProduction && !skipPersistentCaches,
532
+ );
533
+
534
+ return fetchedResolved;
535
+ }
536
+
537
+ if (
538
+ fileListMatch.status === "missing" &&
539
+ fileListMatch.fresh &&
540
+ resolvedFromFileList.status === "missing" &&
541
+ resolvedFromFileList.fresh
542
+ ) {
543
+ throw createNotFoundLikeError(normalizedPath);
544
+ }
497
545
  }
498
546
 
499
547
  const resolved = await this.tryResolveExtensionlessPath(
@@ -506,6 +554,10 @@ export class ReadOperations {
506
554
  if (resolved) return resolved;
507
555
  }
508
556
 
557
+ if (fileListMatch.status === "missing" && fileListMatch.fresh) {
558
+ throw createNotFoundLikeError(normalizedPath);
559
+ }
560
+
509
561
  return this.setupInFlightFetch(
510
562
  normalizedPath,
511
563
  apiPath,
@@ -514,6 +566,11 @@ export class ReadOperations {
514
566
  isProduction,
515
567
  isPreviewMode,
516
568
  ctx,
569
+ skipPersistentCaches
570
+ ? "invalidation"
571
+ : fileListMatch.status === "missing" && fileListMatch.fresh
572
+ ? "not_in_filelist"
573
+ : "no_filelist_cache",
517
574
  );
518
575
  }
519
576
 
@@ -86,15 +86,26 @@ export class PageResolver {
86
86
  primeRouterDetectionCache(cacheKey, "pages");
87
87
  }
88
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/.
91
- pageInfo = await getAppRouteEntity(
89
+ // Auto mode stays structural: detect the dominant router once, then keep
90
+ // pages fallback available for mixed or in-transition projects.
91
+ const useAppRouter = await detectAppRouter(
92
92
  this.projectDir,
93
- slug,
93
+ this.config,
94
94
  this.adapter,
95
- appDirName,
95
+ { projectId: this.projectId },
96
96
  );
97
- if (!pageInfo) {
97
+
98
+ if (useAppRouter) {
99
+ pageInfo = await getAppRouteEntity(
100
+ this.projectDir,
101
+ slug,
102
+ this.adapter,
103
+ appDirName,
104
+ );
105
+ if (!pageInfo) {
106
+ pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
107
+ }
108
+ } else {
98
109
  pageInfo = await getEntityBySlug(this.projectDir, slug, this.adapter);
99
110
  }
100
111
  }
@@ -1,6 +1,6 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.59";
3
+ export const VERSION = "0.1.87";
4
4
 
5
5
  export const SERVER_START_TIME: number = Date.now();
6
6