veryfront 0.1.82 → 0.1.84
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/cli/commands/knowledge/command.d.ts +10 -13
- package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/command.js +294 -110
- package/esm/cli/commands/knowledge/parser-source.d.ts.map +1 -1
- package/esm/cli/commands/knowledge/parser-source.js +52 -0
- package/esm/cli/commands/knowledge/result.d.ts +54 -0
- package/esm/cli/commands/knowledge/result.d.ts.map +1 -0
- package/esm/cli/commands/knowledge/result.js +22 -0
- package/esm/cli/commands/knowledge/source-policy.d.ts +11 -0
- package/esm/cli/commands/knowledge/source-policy.d.ts.map +1 -0
- package/esm/cli/commands/knowledge/source-policy.js +135 -0
- package/esm/deno.js +1 -1
- package/esm/src/jobs/index.d.ts +1 -1
- package/esm/src/jobs/index.d.ts.map +1 -1
- package/esm/src/jobs/index.js +1 -1
- package/esm/src/jobs/schemas.d.ts +1104 -185
- package/esm/src/jobs/schemas.d.ts.map +1 -1
- package/esm/src/jobs/schemas.js +81 -8
- 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/proxy/error-response.d.ts +7 -0
- package/esm/src/proxy/error-response.d.ts.map +1 -0
- package/esm/src/proxy/error-response.js +26 -0
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +25 -0
- package/esm/src/proxy/main.js +2 -23
- 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/transforms/esm/http-cache-helpers.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache-helpers.js +7 -1
- package/esm/src/transforms/import-rewriter/strategies/bare-strategy.d.ts.map +1 -1
- package/esm/src/transforms/import-rewriter/strategies/bare-strategy.js +11 -8
- package/esm/src/transforms/shared/package-specifier.d.ts +7 -0
- package/esm/src/transforms/shared/package-specifier.d.ts.map +1 -0
- package/esm/src/transforms/shared/package-specifier.js +19 -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/cli/commands/knowledge/command.ts +375 -139
- package/src/cli/commands/knowledge/parser-source.ts +52 -0
- package/src/cli/commands/knowledge/result.ts +88 -0
- package/src/cli/commands/knowledge/source-policy.ts +164 -0
- package/src/deno.js +1 -1
- package/src/src/jobs/index.ts +16 -0
- package/src/src/jobs/schemas.ts +105 -8
- 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/proxy/error-response.ts +33 -0
- package/src/src/proxy/handler.ts +43 -0
- package/src/src/proxy/main.ts +2 -27
- 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/transforms/esm/http-cache-helpers.ts +12 -1
- package/src/src/transforms/import-rewriter/strategies/bare-strategy.ts +11 -12
- package/src/src/transforms/shared/package-specifier.ts +29 -0
- package/src/src/types/entities/getEntityInfo.ts +78 -59
|
@@ -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(
|
|
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 {
|
|
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> {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
2
|
+
import { ErrorPages } from "../server/utils/error-html.js";
|
|
3
|
+
import type { ProxyContext } from "./handler.js";
|
|
4
|
+
|
|
5
|
+
type ProxyError = NonNullable<ProxyContext["error"]>;
|
|
6
|
+
|
|
7
|
+
export function jsonErrorResponse(status: number, body: Record<string, unknown>): dntShim.Response {
|
|
8
|
+
return new dntShim.Response(JSON.stringify(body), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createProxyErrorResponse(error: ProxyError): dntShim.Response {
|
|
15
|
+
if (error.redirectUrl) {
|
|
16
|
+
return new dntShim.Response(null, {
|
|
17
|
+
status: 302,
|
|
18
|
+
headers: { Location: error.redirectUrl },
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (error.slug === "release-not-found" || error.slug === "project-not-found") {
|
|
23
|
+
return new dntShim.Response(ErrorPages.notFound(), {
|
|
24
|
+
status: 404,
|
|
25
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return jsonErrorResponse(error.status, {
|
|
30
|
+
error: error.message,
|
|
31
|
+
status: error.status,
|
|
32
|
+
});
|
|
33
|
+
}
|
package/src/src/proxy/handler.ts
CHANGED
|
@@ -175,6 +175,12 @@ function extractUserToken(cookieHeader: string): string | undefined {
|
|
|
175
175
|
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function getStatusFromError(error: unknown): number | null {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
const match = message.match(/failed: (\d+)/);
|
|
181
|
+
return match ? Number(match[1]) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
178
184
|
async function extractUserIdFromToken(
|
|
179
185
|
token: string,
|
|
180
186
|
apiBaseUrl: string,
|
|
@@ -461,6 +467,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
|
461
467
|
const userToken = extractUserToken(cookieHeader);
|
|
462
468
|
|
|
463
469
|
let token: string | undefined;
|
|
470
|
+
let tokenFetchError: unknown;
|
|
464
471
|
|
|
465
472
|
if (isLocalProject) {
|
|
466
473
|
logger?.debug("Local project, skipping token fetch", { localPath });
|
|
@@ -476,6 +483,7 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
|
476
483
|
try {
|
|
477
484
|
token = await tokenManager.getToken(scope, projectSlug, customDomain);
|
|
478
485
|
} catch (error) {
|
|
486
|
+
tokenFetchError = error;
|
|
479
487
|
logger?.error("Token fetch failed", error as Error, { projectSlug, customDomain });
|
|
480
488
|
}
|
|
481
489
|
}
|
|
@@ -488,6 +496,29 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
|
488
496
|
|
|
489
497
|
const base = { scope, host, parsedDomain };
|
|
490
498
|
|
|
499
|
+
if (projectSlug && scope === "preview" && !token) {
|
|
500
|
+
const status = getStatusFromError(tokenFetchError);
|
|
501
|
+
if (status === 404) {
|
|
502
|
+
logger?.info("Preview project not found", { projectSlug, host });
|
|
503
|
+
return makeErrorContext(
|
|
504
|
+
base,
|
|
505
|
+
404,
|
|
506
|
+
"Preview project not found",
|
|
507
|
+
undefined,
|
|
508
|
+
undefined,
|
|
509
|
+
"project-not-found",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
logger?.warn("Preview request has no usable token", {
|
|
514
|
+
projectSlug,
|
|
515
|
+
host,
|
|
516
|
+
hadUserToken: !!userToken,
|
|
517
|
+
hadTokenFetchError: !!tokenFetchError,
|
|
518
|
+
});
|
|
519
|
+
return makeErrorContext(base, 502, "Failed to authenticate preview request");
|
|
520
|
+
}
|
|
521
|
+
|
|
491
522
|
if (isCustomDomain && !projectSlug) {
|
|
492
523
|
if (!token) {
|
|
493
524
|
logger?.error("Cannot process custom domain without token", undefined, { domain: host });
|
|
@@ -590,6 +621,18 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
|
590
621
|
);
|
|
591
622
|
}
|
|
592
623
|
|
|
624
|
+
if (!resolved.projectId) {
|
|
625
|
+
logger?.info("Preview project not found after lookup", { projectSlug, host });
|
|
626
|
+
return makeErrorContext(
|
|
627
|
+
base,
|
|
628
|
+
404,
|
|
629
|
+
"Preview project not found",
|
|
630
|
+
token,
|
|
631
|
+
undefined,
|
|
632
|
+
"project-not-found",
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
593
636
|
projectId = resolved.projectId;
|
|
594
637
|
environmentId = resolved.environmentId;
|
|
595
638
|
|
package/src/src/proxy/main.ts
CHANGED
|
@@ -37,12 +37,12 @@ import {
|
|
|
37
37
|
withSpan,
|
|
38
38
|
} from "./tracing.js";
|
|
39
39
|
import { proxyLogger, runWithProxyRequestContext } from "./logger.js";
|
|
40
|
-
import { ErrorPages } from "../server/utils/error-html.js";
|
|
41
40
|
import { RendererRouter } from "./renderer-router.js";
|
|
42
41
|
import { ServerResolver } from "./server-resolver.js";
|
|
43
42
|
import { parseProjectDomain } from "../server/utils/domain-parser.js";
|
|
44
43
|
import { exit, getEnv, onSignal } from "../platform/compat/process.js";
|
|
45
44
|
import { createHttpServer, upgradeWebSocket } from "../platform/compat/http/index.js";
|
|
45
|
+
import { createProxyErrorResponse, jsonErrorResponse } from "./error-response.js";
|
|
46
46
|
|
|
47
47
|
function getLocalProjects(): Record<string, string> {
|
|
48
48
|
const raw = getEnv("LOCAL_PROJECTS");
|
|
@@ -274,13 +274,6 @@ function handleWebSocketUpgrade(req: dntShim.Request): dntShim.Response {
|
|
|
274
274
|
return response;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
function jsonErrorResponse(status: number, body: Record<string, unknown>): dntShim.Response {
|
|
278
|
-
return new dntShim.Response(JSON.stringify(body), {
|
|
279
|
-
status,
|
|
280
|
-
headers: { "Content-Type": "application/json" },
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
277
|
function forwardToServer(req: dntShim.Request): Promise<dntShim.Response> {
|
|
285
278
|
const startTime = performance.now();
|
|
286
279
|
const url = new URL(req.url);
|
|
@@ -311,25 +304,7 @@ function forwardToServer(req: dntShim.Request): Promise<dntShim.Response> {
|
|
|
311
304
|
const logLevel = ctx.error.status < 500 ? "warn" : "error";
|
|
312
305
|
proxyLogger[logLevel](`${ctx.error.status} ${req.method} ${url.pathname}`, { ms });
|
|
313
306
|
endSpan(spanInfo?.span, ctx.error.status);
|
|
314
|
-
|
|
315
|
-
if (ctx.error.redirectUrl) {
|
|
316
|
-
return new dntShim.Response(null, {
|
|
317
|
-
status: 302,
|
|
318
|
-
headers: { Location: ctx.error.redirectUrl },
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (ctx.error.slug === "release-not-found") {
|
|
323
|
-
return new dntShim.Response(ErrorPages.notFound(), {
|
|
324
|
-
status: 404,
|
|
325
|
-
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return jsonErrorResponse(ctx.error.status, {
|
|
330
|
-
error: ctx.error.message,
|
|
331
|
-
status: ctx.error.status,
|
|
332
|
-
});
|
|
307
|
+
return createProxyErrorResponse(ctx.error);
|
|
333
308
|
}
|
|
334
309
|
|
|
335
310
|
const reqLogger = proxyLogger.child({
|
|
@@ -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;
|
|
@@ -9,6 +9,8 @@ import { cwd } from "../../platform/compat/process.js";
|
|
|
9
9
|
import { rendererLogger } from "../../utils/index.js";
|
|
10
10
|
import { resolveImport } from "../../modules/import-map/resolver.js";
|
|
11
11
|
import type { ImportMapConfig } from "../../modules/import-map/types.js";
|
|
12
|
+
import { buildEsmShUrl } from "../import-rewriter/url-builder.js";
|
|
13
|
+
import { parseBarePackageSpecifier } from "../shared/package-specifier.js";
|
|
12
14
|
import { DEFAULT_REACT_VERSION, getReactImportMap } from "./package-registry.js";
|
|
13
15
|
|
|
14
16
|
const logger = rendererLogger.component("http-cache");
|
|
@@ -149,7 +151,16 @@ export function resolveBareSpecifier(
|
|
|
149
151
|
const mapped = resolveImport(specifier, importMap);
|
|
150
152
|
if (mapped !== specifier) return mapped;
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
const parsed = parseBarePackageSpecifier(specifier);
|
|
155
|
+
if (parsed == null) {
|
|
156
|
+
return `https://esm.sh/${specifier}?target=es2022`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return buildEsmShUrl(
|
|
160
|
+
parsed.packageName,
|
|
161
|
+
parsed.version ?? undefined,
|
|
162
|
+
parsed.subpath ?? undefined,
|
|
163
|
+
);
|
|
153
164
|
}
|
|
154
165
|
|
|
155
166
|
/**
|