Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7f4f8eb
Extract app page route wiring helpers
NathanDrake2406 Apr 2, 2026
5d8525b
Add slot client primitives
NathanDrake2406 Apr 2, 2026
be33773
Fix app page error boundary serialization
NathanDrake2406 Apr 2, 2026
ca40d05
Fix client error boundary pathname reset
NathanDrake2406 Apr 2, 2026
bddda39
Document Next.js error boundary verification
NathanDrake2406 Apr 2, 2026
38d33ca
Merge local PR 2a into PR 2c base
NathanDrake2406 Apr 2, 2026
8c22db3
Merge local PR 2b into PR 2c base
NathanDrake2406 Apr 2, 2026
d488978
Implement flat App Router payload for layout persistence
NathanDrake2406 Apr 2, 2026
ec008fa
fix: address review findings in flat payload implementation
NathanDrake2406 Apr 2, 2026
5395efc
fix: normalize flat payload after use(), not before
NathanDrake2406 Apr 2, 2026
955f577
fix: produce flat RSC payload on all rendering paths
NathanDrake2406 Apr 2, 2026
ce76239
test: update unit tests for flat RSC payload on all paths
NathanDrake2406 Apr 2, 2026
c7a03d5
fix: wrap Flight thenable in Promise.resolve() before chaining .then()
NathanDrake2406 Apr 2, 2026
7fead69
fix: eliminate Promise from ElementsContext to fix React 19 hydration
NathanDrake2406 Apr 2, 2026
311b10a
test: update slot and browser state tests for resolved ElementsContext
NathanDrake2406 Apr 2, 2026
2b5f68c
ci: retrigger
NathanDrake2406 Apr 2, 2026
7554b20
fix: address code review findings (P1-P3)
NathanDrake2406 Apr 2, 2026
1014aed
fix: avoid serializing app render dependency wrappers
NathanDrake2406 Apr 2, 2026
5e516bd
Fix flat payload dependency barriers
NathanDrake2406 Apr 2, 2026
ee2fbdd
Fix template-only route wrappers
NathanDrake2406 Apr 2, 2026
9bf09a8
test: add E2E verification for layout persistence flat payload pipeline
NathanDrake2406 Apr 2, 2026
3a27ea0
feat: encode interception context in App Router payload IDs and caches
NathanDrake2406 Apr 2, 2026
e1f1be6
test: update App Router entry snapshots for interception encoding
NathanDrake2406 Apr 2, 2026
b9efbe8
docs: refresh stale app browser entry comments
NathanDrake2406 Apr 2, 2026
7f22e74
feat: track previousNextUrl for intercepted App Router entries
NathanDrake2406 Apr 2, 2026
2d41ed5
feat: add classifyLayoutSegmentConfig for layout segment config detec…
NathanDrake2406 Apr 3, 2026
d822a7d
feat: add module graph layout classification (Layer 2)
NathanDrake2406 Apr 3, 2026
a551142
feat: per-layout dynamic detection in probe phase (Layer 3)
NathanDrake2406 Apr 3, 2026
bef79ec
feat: add __layoutFlags payload metadata and thread through router state
NathanDrake2406 Apr 3, 2026
36344ec
refactor: group classification options into single LayoutClassificati…
NathanDrake2406 Apr 3, 2026
a212cf5
fix: default to dynamic flag when layout probe throws non-special error
NathanDrake2406 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions packages/vinext/src/build/layout-classification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Layout classification — determines whether each layout in an App Router
* route tree is static or dynamic via two complementary detection layers:
*
* Layer 1: Segment config (`export const dynamic`, `export const revalidate`)
* Layer 2: Module graph traversal (checks for transitive dynamic shim imports)
*
* Layer 3 (probe-based runtime detection) is handled separately in
* `app-page-execution.ts` at request time.
*/

import { classifyLayoutSegmentConfig } from "./report.js";
import { createAppPageTreePath } from "../server/app-page-route-wiring.js";

export type ModuleGraphClassification = "static" | "needs-probe";
export type LayoutClassificationResult = "static" | "dynamic" | "needs-probe";

export type ModuleInfoProvider = {
getModuleInfo(id: string): {
importedIds: string[];
dynamicImportedIds: string[];
} | null;
};

type RouteForClassification = {
layouts: string[];
layoutTreePositions: number[];
routeSegments: string[];
layoutSegmentConfigs?: ReadonlyArray<{ code: string } | null>;
};

/**
* BFS traversal of a layout's dependency tree. If any transitive import
* resolves to a dynamic shim path (headers, cache, server), the layout
* cannot be proven static at build time and needs a runtime probe.
*/
export function classifyLayoutByModuleGraph(
layoutModuleId: string,
dynamicShimPaths: ReadonlySet<string>,
moduleInfo: ModuleInfoProvider,
): ModuleGraphClassification {
const visited = new Set<string>();
const queue: string[] = [layoutModuleId];

while (queue.length > 0) {
const currentId = queue.shift()!;

if (visited.has(currentId)) continue;
visited.add(currentId);

if (dynamicShimPaths.has(currentId)) return "needs-probe";

const info = moduleInfo.getModuleInfo(currentId);
if (!info) continue;

for (const importedId of info.importedIds) {
if (!visited.has(importedId)) queue.push(importedId);
}
for (const dynamicId of info.dynamicImportedIds) {
if (!visited.has(dynamicId)) queue.push(dynamicId);
}
}

return "static";
}

/**
* Classifies all layouts across all routes using a two-layer strategy:
*
* 1. Segment config (Layer 1) — short-circuits to "static" or "dynamic"
* 2. Module graph (Layer 2) — BFS for dynamic shim imports → "static" or "needs-probe"
*
* Shared layouts (same file appearing in multiple routes) are classified once
* and deduplicated by layout ID.
*/
export function classifyAllRouteLayouts(
routes: readonly RouteForClassification[],
dynamicShimPaths: ReadonlySet<string>,
moduleInfo: ModuleInfoProvider,
): Map<string, LayoutClassificationResult> {
const result = new Map<string, LayoutClassificationResult>();

for (const route of routes) {
for (let i = 0; i < route.layouts.length; i++) {
const treePosition = route.layoutTreePositions[i] ?? 0;
const layoutId = `layout:${createAppPageTreePath(route.routeSegments, treePosition)}`;

if (result.has(layoutId)) continue;

// Layer 1: segment config
const segmentConfig = route.layoutSegmentConfigs?.[i];
if (segmentConfig) {
const configResult = classifyLayoutSegmentConfig(segmentConfig.code);
if (configResult !== null) {
result.set(layoutId, configResult);
continue;
}
}

// Layer 2: module graph
const graphResult = classifyLayoutByModuleGraph(
route.layouts[i],
dynamicShimPaths,
moduleInfo,
);
result.set(layoutId, graphResult);
}
}

return result;
}
31 changes: 31 additions & 0 deletions packages/vinext/src/build/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,37 @@ function findMatchingToken(
return -1;
}

// ─── Layout segment config classification ────────────────────────────────────

/**
* Classification result for layout segment config analysis.
* "static" means the layout is confirmed static via segment config.
* "dynamic" means the layout is confirmed dynamic via segment config.
*/
export type LayoutClassification = "static" | "dynamic";

/**
* Classifies a layout file by its segment config exports (`dynamic`, `revalidate`).
*
* Returns `"static"` or `"dynamic"` when the config is decisive, or `null`
* when no segment config is present (deferring to module graph analysis).
*
* Unlike page classification, positive `revalidate` values are not meaningful
* for layout skip decisions — ISR is a page-level concept. Only the extremes
* (`revalidate = 0` → dynamic, `revalidate = Infinity` → static) are decisive.
*/
export function classifyLayoutSegmentConfig(code: string): LayoutClassification | null {
const dynamicValue = extractExportConstString(code, "dynamic");
if (dynamicValue === "force-dynamic") return "dynamic";
if (dynamicValue === "force-static" || dynamicValue === "error") return "static";

const revalidateValue = extractExportConstNumber(code, "revalidate");
if (revalidateValue === Infinity) return "static";
if (revalidateValue === 0) return "dynamic";

return null;
}

// ─── Route classification ─────────────────────────────────────────────────────

/**
Expand Down
Loading