veryfront 0.1.81 → 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 (68) 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/orchestrator/module-loader/index.d.ts.map +1 -1
  30. package/esm/src/rendering/orchestrator/module-loader/index.js +7 -20
  31. package/esm/src/rendering/page-resolution/page-resolver.d.ts.map +1 -1
  32. package/esm/src/rendering/page-resolution/page-resolver.js +19 -6
  33. package/esm/src/rendering/router-detection.d.ts +1 -0
  34. package/esm/src/rendering/router-detection.d.ts.map +1 -1
  35. package/esm/src/rendering/router-detection.js +3 -0
  36. package/esm/src/transforms/esm/bundle-manifest.d.ts +3 -1
  37. package/esm/src/transforms/esm/bundle-manifest.d.ts.map +1 -1
  38. package/esm/src/transforms/esm/bundle-manifest.js +2 -2
  39. package/esm/src/transforms/esm/cached-bundle-validation.d.ts +9 -0
  40. package/esm/src/transforms/esm/cached-bundle-validation.d.ts.map +1 -0
  41. package/esm/src/transforms/esm/cached-bundle-validation.js +25 -0
  42. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.d.ts.map +1 -1
  43. package/esm/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.js +9 -21
  44. package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
  45. package/esm/src/transforms/pipeline/index.js +8 -24
  46. package/esm/src/types/entities/getEntityInfo.d.ts.map +1 -1
  47. package/esm/src/types/entities/getEntityInfo.js +59 -42
  48. package/package.json +1 -1
  49. package/src/deno.js +1 -1
  50. package/src/src/platform/adapters/base.ts +5 -1
  51. package/src/src/platform/adapters/fs/github/adapter.ts +3 -2
  52. package/src/src/platform/adapters/fs/github/stat-operations.ts +3 -2
  53. package/src/src/platform/adapters/fs/veryfront/adapter.ts +24 -3
  54. package/src/src/platform/adapters/fs/veryfront/multi-project-adapter.ts +6 -3
  55. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +1 -0
  56. package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +161 -25
  57. package/src/src/platform/adapters/fs/veryfront/types.ts +2 -1
  58. package/src/src/platform/adapters/fs/wrapper.ts +10 -3
  59. package/src/src/rendering/app-route-resolver.ts +0 -8
  60. package/src/src/rendering/orchestrator/file-resolver/index.ts +19 -0
  61. package/src/src/rendering/orchestrator/module-loader/index.ts +11 -19
  62. package/src/src/rendering/page-resolution/page-resolver.ts +26 -17
  63. package/src/src/rendering/router-detection.ts +7 -0
  64. package/src/src/transforms/esm/bundle-manifest.ts +6 -3
  65. package/src/src/transforms/esm/cached-bundle-validation.ts +40 -0
  66. package/src/src/transforms/mdx/esm-module-loader/module-fetcher/distributed-cache.ts +13 -24
  67. package/src/src/transforms/pipeline/index.ts +8 -26
  68. package/src/src/types/entities/getEntityInfo.ts +78 -59
@@ -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 */
@@ -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\//, "");
@@ -21,8 +21,7 @@ import {
21
21
  setCachedTransformAsync,
22
22
  } from "../../../transforms/esm/transform-cache.js";
23
23
  import { TRANSFORM_DISTRIBUTED_TTL_SEC } from "../../../utils/constants/cache.js";
24
- import { ensureHttpBundlesExist } from "../../../transforms/esm/http-cache.js";
25
- import { validateBundleGroup } from "../../../transforms/esm/bundle-manifest.js";
24
+ import { validateCachedBundlesByManifestOrCode } from "../../../transforms/esm/cached-bundle-validation.js";
26
25
  import { getHttpBundleCacheDir, getMdxEsmCacheDir } from "../../../utils/cache-dir.js";
27
26
  import { dirname, join, normalize } from "../../../platform/compat/path/index.js";
28
27
  import { hashCodeHex } from "../../../utils/hash-utils.js";
@@ -32,7 +31,6 @@ import {
32
31
  lookupMdxEsmCache,
33
32
  saveModulePathCache,
34
33
  } from "../../../transforms/mdx/esm-module-loader/cache/index.js";
35
- import { extractHttpBundlePaths } from "../../../modules/react-loader/ssr-module-loader/http-bundle-helpers.js";
36
34
 
37
35
  const logger = rendererLogger.component("module-loader");
38
36
 
@@ -344,28 +342,22 @@ export async function transformModuleWithDeps(
344
342
  const cacheDir = getHttpBundleCacheDir();
345
343
  let bundlesValid = true;
346
344
 
347
- if (transformResult.cacheHit && transformResult.bundleManifestId) {
348
- const validation = await validateBundleGroup(transformResult.bundleManifestId, cacheDir);
345
+ if (transformResult.cacheHit) {
346
+ const validation = await validateCachedBundlesByManifestOrCode(
347
+ transformedCode,
348
+ transformResult.bundleManifestId,
349
+ cacheDir,
350
+ );
349
351
  if (!validation.valid) {
350
- logger.warn("Bundle manifest validation failed, re-transforming", {
352
+ logger.warn("Cached HTTP bundle validation failed, re-transforming", {
351
353
  filePath,
352
- manifestId: transformResult.bundleManifestId.slice(0, 12),
354
+ manifestId: transformResult.bundleManifestId?.slice(0, 12),
353
355
  failedHashes: validation.failedHashes,
356
+ reason: validation.reason,
357
+ source: validation.source,
354
358
  });
355
359
  bundlesValid = false;
356
360
  }
357
- } else {
358
- const bundlePaths = extractHttpBundlePaths(transformedCode);
359
- if (bundlePaths.length > 0) {
360
- const failed = await ensureHttpBundlesExist(bundlePaths, cacheDir);
361
- if (failed.length > 0) {
362
- logger.warn("HTTP bundle recovery failed, re-transforming", {
363
- filePath,
364
- failed,
365
- });
366
- bundlesValid = false;
367
- }
368
- }
369
361
  }
370
362
 
371
363
  if (!bundlesValid) {
@@ -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;
@@ -38,10 +38,13 @@ interface BundleManifest {
38
38
  ttlSeconds: number;
39
39
  }
40
40
 
41
+ export type ManifestValidationReason = "manifest_missing" | "bundle_missing";
42
+
41
43
  /** Result of manifest validation. */
42
- interface ManifestValidationResult {
44
+ export interface ManifestValidationResult {
43
45
  valid: boolean;
44
46
  failedHashes: string[];
47
+ reason?: ManifestValidationReason;
45
48
  }
46
49
 
47
50
  /**
@@ -148,7 +151,7 @@ export async function validateBundleGroup(
148
151
  logger.debug(`${LOG_PREFIX} Manifest not found in distributed cache`, {
149
152
  manifestId: manifestId.slice(0, 12),
150
153
  });
151
- return { valid: false, failedHashes: [] };
154
+ return { valid: false, failedHashes: [], reason: "manifest_missing" };
152
155
  }
153
156
 
154
157
  const missingBundles: Array<{ path: string; hash: string }> = [];
@@ -178,7 +181,7 @@ export async function validateBundleGroup(
178
181
  manifestId: manifestId.slice(0, 12),
179
182
  unrecoverable: unrecoverableHashes,
180
183
  });
181
- return { valid: false, failedHashes: unrecoverableHashes };
184
+ return { valid: false, failedHashes: unrecoverableHashes, reason: "bundle_missing" };
182
185
  }
183
186
 
184
187
  logger.info(`${LOG_PREFIX} All missing bundles recovered successfully`, {
@@ -0,0 +1,40 @@
1
+ import { extractHttpBundlePaths } from "../../modules/react-loader/ssr-module-loader/http-bundle-helpers.js";
2
+ import { ensureHttpBundlesExist } from "./http-cache.js";
3
+ import { type ManifestValidationReason, validateBundleGroup } from "./bundle-manifest.js";
4
+
5
+ export interface CachedBundleValidationResult {
6
+ valid: boolean;
7
+ failedHashes: string[];
8
+ reason?: ManifestValidationReason;
9
+ source: "manifest" | "code";
10
+ }
11
+
12
+ export async function validateCachedBundlesByManifestOrCode(
13
+ code: string,
14
+ bundleManifestId: string | undefined,
15
+ cacheDir: string,
16
+ ): Promise<CachedBundleValidationResult> {
17
+ if (bundleManifestId) {
18
+ const validation = await validateBundleGroup(bundleManifestId, cacheDir);
19
+ if (validation.valid || validation.reason === "bundle_missing") {
20
+ return { ...validation, source: "manifest" };
21
+ }
22
+ }
23
+
24
+ const bundlePaths = extractHttpBundlePaths(code);
25
+ if (bundlePaths.length === 0) {
26
+ return { valid: true, failedHashes: [], source: "code" };
27
+ }
28
+
29
+ const failedHashes = await ensureHttpBundlesExist(bundlePaths, cacheDir);
30
+ if (failedHashes.length === 0) {
31
+ return { valid: true, failedHashes: [], source: "code" };
32
+ }
33
+
34
+ return {
35
+ valid: false,
36
+ failedHashes,
37
+ reason: "bundle_missing",
38
+ source: "code",
39
+ };
40
+ }