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.
Files changed (93) hide show
  1. package/esm/cli/commands/knowledge/command.d.ts +10 -13
  2. package/esm/cli/commands/knowledge/command.d.ts.map +1 -1
  3. package/esm/cli/commands/knowledge/command.js +294 -110
  4. package/esm/cli/commands/knowledge/parser-source.d.ts.map +1 -1
  5. package/esm/cli/commands/knowledge/parser-source.js +52 -0
  6. package/esm/cli/commands/knowledge/result.d.ts +54 -0
  7. package/esm/cli/commands/knowledge/result.d.ts.map +1 -0
  8. package/esm/cli/commands/knowledge/result.js +22 -0
  9. package/esm/cli/commands/knowledge/source-policy.d.ts +11 -0
  10. package/esm/cli/commands/knowledge/source-policy.d.ts.map +1 -0
  11. package/esm/cli/commands/knowledge/source-policy.js +135 -0
  12. package/esm/deno.js +1 -1
  13. package/esm/src/jobs/index.d.ts +1 -1
  14. package/esm/src/jobs/index.d.ts.map +1 -1
  15. package/esm/src/jobs/index.js +1 -1
  16. package/esm/src/jobs/schemas.d.ts +1104 -185
  17. package/esm/src/jobs/schemas.d.ts.map +1 -1
  18. package/esm/src/jobs/schemas.js +81 -8
  19. package/esm/src/platform/adapters/base.d.ts +4 -1
  20. package/esm/src/platform/adapters/base.d.ts.map +1 -1
  21. package/esm/src/platform/adapters/fs/github/adapter.d.ts +2 -1
  22. package/esm/src/platform/adapters/fs/github/adapter.d.ts.map +1 -1
  23. package/esm/src/platform/adapters/fs/github/adapter.js +2 -2
  24. package/esm/src/platform/adapters/fs/github/stat-operations.d.ts +2 -1
  25. package/esm/src/platform/adapters/fs/github/stat-operations.d.ts.map +1 -1
  26. package/esm/src/platform/adapters/fs/github/stat-operations.js +2 -2
  27. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts +2 -2
  28. package/esm/src/platform/adapters/fs/veryfront/adapter.d.ts.map +1 -1
  29. package/esm/src/platform/adapters/fs/veryfront/adapter.js +17 -2
  30. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.d.ts +2 -2
  31. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.d.ts.map +1 -1
  32. package/esm/src/platform/adapters/fs/veryfront/multi-project-adapter.js +2 -2
  33. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts +1 -0
  34. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
  35. package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts +6 -2
  36. package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
  37. package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +131 -21
  38. package/esm/src/platform/adapters/fs/veryfront/types.d.ts +2 -1
  39. package/esm/src/platform/adapters/fs/veryfront/types.d.ts.map +1 -1
  40. package/esm/src/platform/adapters/fs/wrapper.d.ts +2 -2
  41. package/esm/src/platform/adapters/fs/wrapper.d.ts.map +1 -1
  42. package/esm/src/platform/adapters/fs/wrapper.js +2 -2
  43. package/esm/src/proxy/error-response.d.ts +7 -0
  44. package/esm/src/proxy/error-response.d.ts.map +1 -0
  45. package/esm/src/proxy/error-response.js +26 -0
  46. package/esm/src/proxy/handler.d.ts.map +1 -1
  47. package/esm/src/proxy/handler.js +25 -0
  48. package/esm/src/proxy/main.js +2 -23
  49. package/esm/src/rendering/app-route-resolver.js +0 -9
  50. package/esm/src/rendering/orchestrator/file-resolver/index.d.ts.map +1 -1
  51. package/esm/src/rendering/orchestrator/file-resolver/index.js +17 -0
  52. package/esm/src/rendering/page-resolution/page-resolver.d.ts.map +1 -1
  53. package/esm/src/rendering/page-resolution/page-resolver.js +19 -6
  54. package/esm/src/rendering/router-detection.d.ts +1 -0
  55. package/esm/src/rendering/router-detection.d.ts.map +1 -1
  56. package/esm/src/rendering/router-detection.js +3 -0
  57. package/esm/src/transforms/esm/http-cache-helpers.d.ts.map +1 -1
  58. package/esm/src/transforms/esm/http-cache-helpers.js +7 -1
  59. package/esm/src/transforms/import-rewriter/strategies/bare-strategy.d.ts.map +1 -1
  60. package/esm/src/transforms/import-rewriter/strategies/bare-strategy.js +11 -8
  61. package/esm/src/transforms/shared/package-specifier.d.ts +7 -0
  62. package/esm/src/transforms/shared/package-specifier.d.ts.map +1 -0
  63. package/esm/src/transforms/shared/package-specifier.js +19 -0
  64. package/esm/src/types/entities/getEntityInfo.d.ts.map +1 -1
  65. package/esm/src/types/entities/getEntityInfo.js +59 -42
  66. package/package.json +1 -1
  67. package/src/cli/commands/knowledge/command.ts +375 -139
  68. package/src/cli/commands/knowledge/parser-source.ts +52 -0
  69. package/src/cli/commands/knowledge/result.ts +88 -0
  70. package/src/cli/commands/knowledge/source-policy.ts +164 -0
  71. package/src/deno.js +1 -1
  72. package/src/src/jobs/index.ts +16 -0
  73. package/src/src/jobs/schemas.ts +105 -8
  74. package/src/src/platform/adapters/base.ts +5 -1
  75. package/src/src/platform/adapters/fs/github/adapter.ts +3 -2
  76. package/src/src/platform/adapters/fs/github/stat-operations.ts +3 -2
  77. package/src/src/platform/adapters/fs/veryfront/adapter.ts +24 -3
  78. package/src/src/platform/adapters/fs/veryfront/multi-project-adapter.ts +6 -3
  79. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +1 -0
  80. package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +161 -25
  81. package/src/src/platform/adapters/fs/veryfront/types.ts +2 -1
  82. package/src/src/platform/adapters/fs/wrapper.ts +10 -3
  83. package/src/src/proxy/error-response.ts +33 -0
  84. package/src/src/proxy/handler.ts +43 -0
  85. package/src/src/proxy/main.ts +2 -27
  86. package/src/src/rendering/app-route-resolver.ts +0 -8
  87. package/src/src/rendering/orchestrator/file-resolver/index.ts +19 -0
  88. package/src/src/rendering/page-resolution/page-resolver.ts +26 -17
  89. package/src/src/rendering/router-detection.ts +7 -0
  90. package/src/src/transforms/esm/http-cache-helpers.ts +12 -1
  91. package/src/src/transforms/import-rewriter/strategies/bare-strategy.ts +11 -12
  92. package/src/src/transforms/shared/package-specifier.ts +29 -0
  93. 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(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> {
@@ -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
+ }
@@ -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
 
@@ -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 { 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;
@@ -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
- return `https://esm.sh/${specifier}?target=es2022`;
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
  /**