akanjs 2.2.12 → 2.2.13-rc.1

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 (38) hide show
  1. package/client/csrTypes.ts +37 -6
  2. package/client/makePageProto.tsx +8 -8
  3. package/client/router.ts +5 -2
  4. package/fetch/requestStorage.ts +41 -11
  5. package/package.json +1 -1
  6. package/server/akanApp.ts +55 -0
  7. package/server/cachePolicy.ts +192 -0
  8. package/server/metadata.tsx +114 -0
  9. package/server/routeElementComposer.tsx +21 -1
  10. package/server/routeTreeBuilder.ts +44 -5
  11. package/server/rscClient.tsx +127 -50
  12. package/server/rscHttp.ts +120 -0
  13. package/server/rscNavigationState.ts +95 -0
  14. package/server/rscWorker.tsx +318 -121
  15. package/server/rscWorkerHost.ts +281 -66
  16. package/server/rscWorkerReplay.ts +40 -0
  17. package/server/ssrFromRscRenderer.tsx +462 -77
  18. package/server/ssrTypes.ts +11 -1
  19. package/server/webRouter.ts +173 -88
  20. package/service/ipcTypes.ts +1 -0
  21. package/types/client/csrTypes.d.ts +37 -6
  22. package/types/dictionary/base.dictionary.d.ts +1 -1
  23. package/types/dictionary/dictionary.d.ts +8 -8
  24. package/types/fetch/requestStorage.d.ts +16 -6
  25. package/types/server/cachePolicy.d.ts +55 -0
  26. package/types/server/metadata.d.ts +13 -0
  27. package/types/server/routeElementComposer.d.ts +6 -1
  28. package/types/server/rscHttp.d.ts +16 -0
  29. package/types/server/rscNavigationState.d.ts +35 -0
  30. package/types/server/rscWorkerHost.d.ts +38 -0
  31. package/types/server/rscWorkerReplay.d.ts +29 -0
  32. package/types/server/ssrFromRscRenderer.d.ts +20 -1
  33. package/types/server/ssrTypes.d.ts +10 -1
  34. package/types/server/webRouter.d.ts +27 -1
  35. package/types/service/ipcTypes.d.ts +1 -0
  36. package/types/webkit/useCsrValues.d.ts +1 -1
  37. package/ui/Link/SsrLink.tsx +0 -2
  38. package/webkit/bootCsr.tsx +16 -2
@@ -0,0 +1,55 @@
1
+ import { type AkanDynamicUsage, type AkanRequestPolicy } from "akanjs/fetch";
2
+ export declare const DEFAULT_ROUTE_CACHE_TTL_SECONDS = 30;
3
+ export interface RouteCacheKeyInput {
4
+ request: Request;
5
+ url: URL;
6
+ theme?: string;
7
+ }
8
+ export interface RouteCacheRenderState {
9
+ cacheable: boolean;
10
+ revalidate?: number | false;
11
+ tags?: string[];
12
+ dynamicUsage?: AkanDynamicUsage;
13
+ reason?: string;
14
+ }
15
+ export interface RouteCacheEntry {
16
+ key: string;
17
+ ttl: number;
18
+ }
19
+ export type RouteCacheRenderControlType = "redirect" | "not-found" | "error";
20
+ export declare function parsePositiveInt(value: string | undefined | null): number | null;
21
+ export declare function normalizeRouteCacheTtl(value: unknown, fallback?: number): number | null;
22
+ export declare function resolveAutoRouteCacheTtl(input: {
23
+ enabled?: string | null;
24
+ ttl?: string | null;
25
+ defaultTtl?: number;
26
+ }): number | null;
27
+ export declare function combineMinRevalidate(...values: Array<number | false | null | undefined>): number | false | undefined;
28
+ export declare function getClientFacingOrigin(request: Request, url?: URL): string;
29
+ export declare function isPublicRouteCacheableRequest(request: Request): boolean;
30
+ export declare function isRouteCachePathAllowed(pathname: string, options?: {
31
+ allow?: string | null;
32
+ deny?: string | null;
33
+ }): boolean;
34
+ export declare function createRouteCacheKey({ request, url, theme }: RouteCacheKeyInput): string;
35
+ export declare function createRouteCacheEntry(input: RouteCacheKeyInput & {
36
+ ttl: number;
37
+ }): RouteCacheEntry;
38
+ export declare function resolveRouteCacheStoreTtl(baseTtl: number, state: RouteCacheRenderState): number | null;
39
+ export declare function shouldStoreRouteCache(input: {
40
+ policy?: AkanRequestPolicy;
41
+ dynamicUsage?: AkanDynamicUsage;
42
+ renderControlType?: RouteCacheRenderControlType;
43
+ lateRedirect?: boolean;
44
+ }): RouteCacheRenderState;
45
+ export declare class LruTtlCache<T> {
46
+ #private;
47
+ readonly maxEntries: number;
48
+ constructor(maxEntries?: number);
49
+ get size(): number;
50
+ get(key: string): T | null;
51
+ set(key: string, value: T, ttlSeconds: number): void;
52
+ delete(key: string): boolean;
53
+ invalidate(predicate: (key: string, value: T) => boolean): number;
54
+ clear(): void;
55
+ }
@@ -0,0 +1,13 @@
1
+ import type { AkanMetadata, Head, ResolvedHead, ResolveHeadResult } from "akanjs/client";
2
+ export declare function isAkanMetadata(value: unknown): value is AkanMetadata;
3
+ export declare function renderMetadata(metadata: AkanMetadata): Head;
4
+ export declare function hasExplicitLanguageAlternates(metadata: AkanMetadata | null | undefined): boolean;
5
+ export declare function shouldRenderLocaleAlternates(options: {
6
+ isSpecialRoute?: boolean;
7
+ hasExplicitLanguageAlternates?: boolean;
8
+ }): boolean;
9
+ export declare function isResolvedHead(value: unknown): value is ResolvedHead;
10
+ export declare function resolveMetadataHead(metadata: AkanMetadata): ResolvedHead;
11
+ export declare function resolveHeadExport(value: Head | AkanMetadata | null | undefined): ResolvedHead;
12
+ export declare function resolveHeadResult(value: ResolveHeadResult): ResolvedHead;
13
+ export declare function normalizeHead(value: Head | AkanMetadata | null | undefined): Head | null | undefined;
@@ -1,4 +1,4 @@
1
- import type { Head, LayoutFallbackRoute, PathRoute, RouteRender } from "akanjs/client";
1
+ import type { Head, LayoutFallbackRoute, PathRoute, ResolvedHead, RouteRender } from "akanjs/client";
2
2
  import { type ReactElement, type ReactNode } from "react";
3
3
  export declare class RouteElementComposer {
4
4
  #private;
@@ -12,6 +12,11 @@ export declare class RouteElementComposer {
12
12
  params: Record<string, string>;
13
13
  searchParams: Record<string, string | string[]>;
14
14
  }): Promise<Head | null | undefined>;
15
+ static resolveHeadWithMetadata({ pathRoute, params, searchParams, }: {
16
+ pathRoute: PathRoute;
17
+ params: Record<string, string>;
18
+ searchParams: Record<string, string[] | string>;
19
+ }): Promise<ResolvedHead>;
15
20
  static composeFallback({ kind, route, params, searchParams, pathname, error, digest, }: {
16
21
  kind: "not-found" | "error";
17
22
  route: PathRoute | LayoutFallbackRoute;
@@ -1,2 +1,18 @@
1
1
  export declare const RSC_CONTENT_TYPE = "text/x-component; charset=utf-8";
2
+ export interface RscRedirectRow {
3
+ rowId: string;
4
+ location?: string;
5
+ method?: "replace" | "push";
6
+ status?: 303 | 307 | 308;
7
+ }
8
+ export declare function encodeAkanRedirectDigest(input: {
9
+ location: string;
10
+ method: "replace" | "push";
11
+ status: 303 | 307 | 308;
12
+ }): string;
13
+ export declare function decodeAkanRedirectDigest(digest: unknown): Omit<RscRedirectRow, "rowId">;
2
14
  export declare function isRscPayloadResponse(res: Response): boolean;
15
+ export declare function getRscPayloadStream(res: Response): ReadableStream<Uint8Array> | null;
16
+ export declare function guardRscRedirectRows(stream: ReadableStream<Uint8Array>, options?: {
17
+ onRedirect?: (redirect: RscRedirectRow) => void;
18
+ }): ReadableStream<Uint8Array>;
@@ -0,0 +1,35 @@
1
+ export interface RscNavigationCache<T> {
2
+ get(key: string): T | undefined;
3
+ set(key: string, value: T): void;
4
+ delete(key: string): boolean;
5
+ keys(): IterableIterator<string>;
6
+ readonly size: number;
7
+ }
8
+ export declare function rememberRscCacheEntry<T>(cache: RscNavigationCache<T>, href: string, thenable: T, maxEntries: number): void;
9
+ export declare function deleteRscCacheEntryIfCurrent<T>(cache: RscNavigationCache<T>, href: string, thenable: T): boolean;
10
+ interface CommitRscNavigationInput<T> {
11
+ cache: RscNavigationCache<T>;
12
+ href: string;
13
+ thenable: T;
14
+ maxEntries: number;
15
+ startTransition: (callback: () => void) => void;
16
+ commitThenable: (thenable: T) => void;
17
+ updateHistory?: () => void;
18
+ scrollToTop?: boolean;
19
+ bumpScrollToTop?: () => void;
20
+ }
21
+ export declare function commitRscNavigation<T>({ cache, href, thenable, maxEntries, startTransition, commitThenable, updateHistory, scrollToTop, bumpScrollToTop, }: CommitRscNavigationInput<T>): void;
22
+ export declare function commitLatestRscNavigation<T>({ navId, getCurrentNavId, ...input }: CommitRscNavigationInput<T> & {
23
+ navId: number;
24
+ getCurrentNavId: () => number;
25
+ }): boolean;
26
+ export declare function observeRscNavigation<T extends PromiseLike<unknown>>({ cache, href, thenable, navId, getCurrentNavId, isExpectedNavigationError, onLatestError, }: {
27
+ cache: RscNavigationCache<T>;
28
+ href: string;
29
+ thenable: T;
30
+ navId: number;
31
+ getCurrentNavId: () => number;
32
+ isExpectedNavigationError?: (error: unknown) => boolean;
33
+ onLatestError: (error: unknown) => void;
34
+ }): void;
35
+ export {};
@@ -2,14 +2,36 @@ import { type AkanI18nConfig } from "akanjs/common";
2
2
  import type { AkanTheme } from "akanjs/fetch";
3
3
  import type { AkanMetricsReport } from "akanjs/service";
4
4
  import type { ClientManifest } from "./artifact.d.ts";
5
+ import type { RouteCacheRenderState } from "./cachePolicy.d.ts";
6
+ import type { SsrLateRedirect } from "./ssrTypes.d.ts";
5
7
  import type { BaseBuildArtifact, CssAsset } from "./types.d.ts";
8
+ export interface RscPending {
9
+ onChunk: (data: Uint8Array) => void;
10
+ onEnd: () => void;
11
+ onError: (message: string) => void;
12
+ onMeta?: (meta: {
13
+ theme?: AkanTheme;
14
+ status?: number;
15
+ }) => void;
16
+ onCacheState?: (state: RouteCacheRenderState) => void;
17
+ onRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
18
+ onLateRedirect?: (location: string, method: RscRedirectMethod, status: RscRedirectStatus) => void;
19
+ onNotFound?: () => void;
20
+ }
6
21
  export type RscRedirectMethod = "replace" | "push";
7
22
  export type RscRedirectStatus = 303 | 307 | 308;
23
+ export interface RscWorkerInvalidateCacheMessage {
24
+ type: "invalidate-cache";
25
+ reason?: string;
26
+ }
8
27
  export type RscRenderResult = {
9
28
  type: "stream";
10
29
  stream: ReadableStream<Uint8Array>;
11
30
  theme?: AkanTheme;
12
31
  status?: number;
32
+ lateControl: Promise<SsrLateRedirect | null>;
33
+ cacheState: Promise<RouteCacheRenderState>;
34
+ cancel: (reason?: unknown) => void;
13
35
  } | {
14
36
  type: "redirect";
15
37
  location: string;
@@ -18,6 +40,20 @@ export type RscRenderResult = {
18
40
  } | {
19
41
  type: "not-found";
20
42
  };
43
+ export declare function getRscHostMaxPendingChunks(value?: string | undefined): number;
44
+ export declare function nextRscHostPendingChunkCount(currentPendingChunks: number, desiredSize: number | null): number;
45
+ export declare function isRscHostPendingChunkOverflow(pendingChunks: number, maxPendingChunks: number): boolean;
46
+ export declare function createIdempotentRscRenderCancel(onCancel: (reason?: unknown) => void): (reason?: unknown) => void;
47
+ export declare function createRscWorkerInvalidateCacheMessage(reason?: string): RscWorkerInvalidateCacheMessage;
48
+ export declare function createRscHostRenderStream(input: {
49
+ setPending: (pending: RscPending) => void;
50
+ deletePending: () => void;
51
+ sendRenderOrQueue: () => void;
52
+ cancelRender: (reason?: unknown) => void;
53
+ maxPendingChunks?: number;
54
+ signal?: AbortSignal;
55
+ onPendingChunkOverflow?: () => void;
56
+ }): Promise<RscRenderResult>;
21
57
  export interface RscWorkerReloadInput {
22
58
  clientManifest: ClientManifest;
23
59
  cssAssets?: Record<string, CssAsset>;
@@ -64,7 +100,9 @@ export declare class RscWorker {
64
100
  render(req: Request): ReadableStream<Uint8Array>;
65
101
  renderWithMeta(req: Request, options?: {
66
102
  clientManifest?: ClientManifest;
103
+ signal?: AbortSignal;
67
104
  }): Promise<RscRenderResult>;
105
+ invalidateRouteResultCache(reason?: string): void;
68
106
  kill(): void;
69
107
  getMetrics(): AkanMetricsReport;
70
108
  restartWhenIdle(reason: string): boolean;
@@ -0,0 +1,29 @@
1
+ import type { RouteCacheRenderState } from "./cachePolicy.d.ts";
2
+ export type CachedRscReplayMessage = {
3
+ type: "meta";
4
+ requestId: string;
5
+ theme?: string;
6
+ status?: number;
7
+ } | {
8
+ type: "cache-state";
9
+ requestId: string;
10
+ state: RouteCacheRenderState;
11
+ } | {
12
+ type: "chunk";
13
+ requestId: string;
14
+ data: Uint8Array;
15
+ } | {
16
+ type: "end";
17
+ requestId: string;
18
+ };
19
+ export declare function replayCachedRscResult(input: {
20
+ requestId: string;
21
+ chunks: readonly Uint8Array[];
22
+ theme?: string;
23
+ status?: number;
24
+ cacheState?: RouteCacheRenderState;
25
+ send: (message: CachedRscReplayMessage) => void;
26
+ isCancelled: () => boolean;
27
+ yieldEveryChunks?: number;
28
+ yieldToHost?: () => Promise<void>;
29
+ }): Promise<boolean>;
@@ -1,4 +1,5 @@
1
- import type { SsrChunkRegistryStats, SsrFromRscInput } from "./ssrTypes.d.ts";
1
+ import { type AkanRequestStore } from "akanjs/fetch";
2
+ import type { SsrChunkRegistryStats, SsrFromRscInput, SsrLateRedirect } from "./ssrTypes.d.ts";
2
3
  export declare class SsrChunkRegistry<T> {
3
4
  #private;
4
5
  readonly maxEntries: number;
@@ -12,6 +13,24 @@ export type InlineRscChunk = readonly [1, string] | readonly [3, string];
12
13
  export declare function encodeInlineRscChunk(chunk: Uint8Array): InlineRscChunk;
13
14
  export declare function htmlEscapeJsonString(value: string): string;
14
15
  export declare function createInlineRscScript(chunk: Uint8Array): string;
16
+ export declare function createSoftRedirectScript(redirect: SsrLateRedirect): string;
17
+ export declare function sanitizeFlightForClientStream(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array>;
18
+ export declare class ExpectedLateRedirectStderrSuppressor {
19
+ #private;
20
+ private constructor();
21
+ static start(lateControl?: Promise<SsrLateRedirect | null>): ExpectedLateRedirectStderrSuppressor | null;
22
+ stop(): void;
23
+ }
24
+ export declare function interleaveRscScriptsWithHtml(htmlStream: ReadableStream<Uint8Array>, rscClientStream: ReadableStream<Uint8Array>, options?: {
25
+ bootstrapModuleScripts?: string;
26
+ lateControl?: Promise<SsrLateRedirect | null>;
27
+ maxPendingRscScripts?: number;
28
+ onPendingRscScriptsSize?: (size: number) => void;
29
+ onComplete?: () => void;
30
+ onCancel?: (reason?: unknown) => void;
31
+ request?: Request;
32
+ requestStore?: AkanRequestStore;
33
+ }): ReadableStream<Uint8Array>;
15
34
  export declare class SsrFromRscRenderer {
16
35
  #private;
17
36
  static getChunkRegistryStats(): SsrChunkRegistryStats;
@@ -1,4 +1,4 @@
1
- import type { AkanTheme } from "akanjs/fetch";
1
+ import type { AkanRequestStore, AkanTheme } from "akanjs/fetch";
2
2
  export interface SsrManifestEntry {
3
3
  id: string;
4
4
  chunks: string[];
@@ -18,8 +18,15 @@ export interface SsrChunkRegistryStats {
18
18
  ssrChunkCacheHitCount: number;
19
19
  ssrChunkEvictionCount: number;
20
20
  }
21
+ export interface SsrLateRedirect {
22
+ type: "redirect";
23
+ location: string;
24
+ method: "replace" | "push";
25
+ status: 303 | 307 | 308;
26
+ }
21
27
  export interface SsrFromRscInput {
22
28
  request?: Request;
29
+ requestStore?: AkanRequestStore;
23
30
  rscStream: ReadableStream<Uint8Array>;
24
31
  ssrManifest: SsrManifest;
25
32
  bootstrapModules?: string[];
@@ -44,4 +51,6 @@ export interface SsrFromRscInput {
44
51
  importmap?: Record<string, string>;
45
52
  theme?: AkanTheme;
46
53
  injectThemeInitScript?: boolean;
54
+ lateControl?: Promise<SsrLateRedirect | null>;
55
+ onCancel?: (reason?: unknown) => void;
47
56
  }
@@ -1,11 +1,35 @@
1
1
  import { type AkanI18nConfig } from "akanjs/common";
2
+ import { type AkanRequestStore } from "akanjs/fetch";
2
3
  import type { AkanMetricsReport } from "akanjs/service";
3
4
  import { type BuilderRpc, type RouteSeedIndex } from "./artifact.d.ts";
5
+ import { type RouteCacheRenderState } from "./cachePolicy.d.ts";
4
6
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub.d.ts";
5
- import { type RscRedirectMethod, type RscRedirectStatus, RscWorker } from "./rscWorkerHost.d.ts";
7
+ import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, RscWorker } from "./rscWorkerHost.d.ts";
6
8
  import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types.d.ts";
7
9
  export declare function createRscRedirectResponse(location: string, method: RscRedirectMethod, status?: RscRedirectStatus): Response;
8
10
  export declare function createRscStreamResponse(stream: BodyInit, status?: number): Response;
11
+ export declare function createRscNotFoundFallbackResponse(): Response;
12
+ export declare function cacheHtmlWhileStreaming(stream: ReadableStream<Uint8Array>, onComplete: (html: string) => void, options?: {
13
+ shouldCache?: () => boolean | Promise<boolean>;
14
+ maxBodyBytes?: number | null;
15
+ }): ReadableStream<Uint8Array>;
16
+ export declare function cancelStreamForHeadResponse(stream: ReadableStream<Uint8Array>, reason: unknown): void;
17
+ export declare function resolveHtmlRouteCacheStoreTtl(input: {
18
+ baseTtl: number;
19
+ workerCacheState: RouteCacheRenderState;
20
+ hostRequestStore: AkanRequestStore;
21
+ lateControl?: {
22
+ type: "redirect";
23
+ } | null;
24
+ }): number | null;
25
+ export declare function isHtmlRouteCachePathAllowed(pathname: string, env?: {
26
+ [key: string]: string | undefined;
27
+ AKAN_HTML_RESULT_CACHE_PATHS?: string;
28
+ AKAN_HTML_RESULT_CACHE_EXCLUDE_PATHS?: string;
29
+ }): boolean;
30
+ export declare function createRscNavigationStreamResponse(result: Extract<RscRenderResult, {
31
+ type: "stream";
32
+ }>): Promise<Response>;
9
33
  export declare function normalizeRscTargetUrlForHostBasePath(targetUrl: URL, options: {
10
34
  basePath: string | null;
11
35
  basePaths?: readonly string[];
@@ -41,6 +65,8 @@ export declare class WebRouter {
41
65
  }>;
42
66
  dispose(): void;
43
67
  getMetrics(): AkanMetricsReport;
68
+ /** @internal Clears local route result caches owned by the host and RSC worker. */
69
+ invalidateRouteCaches(reason?: string): void;
44
70
  static create({ upgradeHmrWs }: SsrRoutesInputs): Promise<WebRouter>;
45
71
  }
46
72
  export {};
@@ -61,6 +61,7 @@ export interface AkanMetricsReport {
61
61
  rscWorkerLastRecycleReason?: string;
62
62
  rscPendingRenderCount?: number;
63
63
  rscQueuedSendCount?: number;
64
+ rscHostPendingChunkOverflowCount?: number;
64
65
  rscRenderCount?: number;
65
66
  rscInFlightRenderCount?: number;
66
67
  rscLastRenderedPath?: string;
@@ -7,7 +7,7 @@ export declare const useCsrValues: (rootRouteGuide: RouteGuide, pathRoutes: Path
7
7
  bottomInset: import("akanjs/client").ContainerTransition | null;
8
8
  topLeftAction: import("akanjs/client").ContainerTransition | null;
9
9
  bottomSafeArea: import("akanjs/client").SafeAreaTransition | null;
10
- pageBind: (...args: any[]) => import("@use-gesture/react/dist/declarations/src/types").ReactDOMAttributes;
10
+ pageBind: (...args: unknown[]) => import("@use-gesture/react/dist/declarations/src/types").ReactDOMAttributes;
11
11
  pageClassName: string;
12
12
  transDirection: "vertical" | "horizontal" | "none";
13
13
  transUnitRange: number[];
@@ -61,8 +61,6 @@ export default function SsrLink({
61
61
  if (!router.isInitialized || !rscNavigationReady) return;
62
62
  event.preventDefault();
63
63
  try {
64
- Logger.log(`pathChange-start:${requestHref}`);
65
- window.parent.postMessage({ type: "pathChange", href: requestHref }, "*");
66
64
  if (replace) router.replace(href, { scrollToTop });
67
65
  else router.push(href, { scrollToTop });
68
66
  } catch (error) {
@@ -283,12 +283,14 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
283
283
  const parsed = parseRouteModuleKey(key);
284
284
  const allowed =
285
285
  parsed.kind === "page"
286
- ? new Set(["default", "pageConfig", "head", "generateHead", "Loading"])
286
+ ? new Set(["default", "pageConfig", "head", "metadata", "generateHead", "generateMetadata", "Loading"])
287
287
  : parsed.isInternalRootLayout
288
288
  ? new Set([
289
289
  "default",
290
290
  "head",
291
+ "metadata",
291
292
  "generateHead",
293
+ "generateMetadata",
292
294
  "fonts",
293
295
  "manifest",
294
296
  "theme",
@@ -300,7 +302,7 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
300
302
  "NotFound",
301
303
  "Error",
302
304
  ])
303
- : new Set(["default", "head", "generateHead", "Loading", "NotFound", "Error"]);
305
+ : new Set(["default", "head", "metadata", "generateHead", "generateMetadata", "Loading", "NotFound", "Error"]);
304
306
  for (const exportName of Object.keys(mod)) {
305
307
  if (!allowed.has(exportName)) {
306
308
  throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);
@@ -309,4 +311,16 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
309
311
  if ("head" in mod && "generateHead" in mod) {
310
312
  throw new Error(`[route-convention] head and generateHead cannot both be exported in ${key}`);
311
313
  }
314
+ if (
315
+ !parsed.isInternalRootLayout &&
316
+ ("head" in mod || "generateHead" in mod) &&
317
+ ("metadata" in mod || "generateMetadata" in mod)
318
+ ) {
319
+ throw new Error(
320
+ `[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${key}`,
321
+ );
322
+ }
323
+ if ("metadata" in mod && "generateMetadata" in mod) {
324
+ throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${key}`);
325
+ }
312
326
  }