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