Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
79abe38
feat(build): add transport-neutral worker stages
james-elicx Aug 25, 2026
ad9fe26
feat(cache): admit responses within cacheable stages
james-elicx Sep 1, 2026
337115c
fix(cache): preserve staged prewarming semantics
james-elicx Sep 1, 2026
cfae9ec
fix(cache): preserve staged routing parity
james-elicx Sep 1, 2026
3b90c46
fix(cache): shape completed staged responses
james-elicx Sep 1, 2026
58f56d3
fix(cache): reconcile outer private policies
james-elicx Sep 1, 2026
365e79d
fix(cache): reconcile hybrid Pages outer policy
james-elicx Sep 1, 2026
d7b71bf
fix(cache): isolate request-stage cacheability
james-elicx Sep 1, 2026
7b089b9
fix(build): keep client asset import beside server entry
james-elicx Sep 1, 2026
e29ce05
fix(cache): preserve staged HEAD and probe semantics
james-elicx Sep 1, 2026
cc60853
fix(cache): purge Pages CDN entries after revalidation
james-elicx Sep 1, 2026
871e85c
test(cache): preserve staged execution context coverage
james-elicx Sep 1, 2026
b81aeaf
fix(cache): preserve staged request identity
james-elicx Sep 1, 2026
7f3c02b
fix(cache): preserve regenerated response metadata
james-elicx Sep 1, 2026
9570212
style(cache): format staged cache helpers
james-elicx Sep 1, 2026
26a8b2f
fix(cache): bypass shared stages after middleware request overrides
james-elicx Sep 2, 2026
d8aef7d
fix(cache): admit explicitly public Pages APIs
james-elicx Sep 2, 2026
3c5abef
fix(cache): preserve outer Pages cache policy
james-elicx Sep 2, 2026
cb143c4
refactor(cache): extract app request stage dispatch
james-elicx Sep 2, 2026
f6fb6f3
fix(build): classify both staged app graphs
james-elicx Sep 2, 2026
2e6e873
fix(cache): preserve staged response policy precedence
james-elicx Sep 2, 2026
77032d1
fix(cache): honor adapter-owned response policies
james-elicx Sep 2, 2026
a2ba36d
fix(cache): preserve staged pre-handler response headers
james-elicx Sep 2, 2026
d214996
test(cache): verify Pages pre-handler response headers
james-elicx Sep 2, 2026
0a61fc6
fix(cache): thread adapter policy through admission
james-elicx Sep 2, 2026
bcd7dab
fix(cache): preserve static stage reuse
james-elicx Sep 2, 2026
6f874e3
fix(cache): preserve normalized stage representations
james-elicx Sep 2, 2026
f2a072e
fix(cache): preserve Pages initial props policy
james-elicx Sep 2, 2026
4f5e566
fix(build): detect default initial props assignments
james-elicx Sep 2, 2026
8be9937
fix(cache): derive Pages policy ownership at runtime
james-elicx Sep 2, 2026
301ef7f
test(build): cover transport-neutral stage outputs
james-elicx Sep 2, 2026
216e695
test(cache): provide staged Pages runtime classification
james-elicx Sep 2, 2026
1f9007a
fix(cache): make draft transitions bypass shared stages
james-elicx Sep 2, 2026
8e77cba
fix(cache): keep cookie responses private after staged composition
james-elicx Sep 2, 2026
d857178
fix(cache): preserve staged late-response vetoes
james-elicx Sep 2, 2026
7913939
fix(cache): preserve only completed staged vetoes
james-elicx Sep 2, 2026
61b323d
fix(cache): preserve late request-stage policy
james-elicx Sep 2, 2026
244eee1
test(cache): align staged browser policy
james-elicx Sep 2, 2026
e3941ff
fix(cache): keep transport core independently valid
james-elicx Sep 2, 2026
516e812
fix(cache): preserve core admission semantics
james-elicx Sep 2, 2026
2f4ba86
fix(cache): verify multi-stage prerender readiness
james-elicx Sep 2, 2026
4989cd5
fix(cache): validate App response-stage readiness
james-elicx Sep 2, 2026
b9d50af
fix(cache): warm routes resolved by uncached stages
james-elicx Sep 2, 2026
9cac117
fix(cache): retain staged readiness version proof
james-elicx Sep 2, 2026
eee868c
fix(cache): confine Pages readiness credentials
james-elicx Sep 2, 2026
c68d7d4
fix(cache): preserve env-bound adapter registration
james-elicx Sep 2, 2026
fa50a5b
fix(cache): initialize configured adapters once
james-elicx Sep 2, 2026
e11bab6
fix(cache): preserve explicit data adapters
james-elicx Sep 2, 2026
0e3c84c
fix(cache): mark rewrite-resolved warm routes
james-elicx Sep 2, 2026
61b9b1b
fix(cache): mark middleware-resolved warm routes
james-elicx Sep 2, 2026
ef41ed0
fix(cache): exclude externally rewritten warm paths
james-elicx Sep 2, 2026
029e396
fix(cache): classify terminal request-stage probes
james-elicx Sep 2, 2026
e6b2271
fix(cache): encode probe route metadata
james-elicx Sep 2, 2026
5b65155
fix(cache): preserve staged request semantics
james-elicx Sep 2, 2026
7fa477e
fix(cache): authenticate Worker prerender state
james-elicx Sep 2, 2026
e219581
fix(cache): isolate request-stage cache adapters
james-elicx Sep 2, 2026
0ce5b6d
fix(cache): preserve staged Pages response boundaries
james-elicx Sep 2, 2026
9665720
fix(cache): probe request-stage terminal routes
james-elicx Sep 2, 2026
34922be
fix(cache): report resolved probe pathnames
james-elicx Sep 2, 2026
e46fe4d
fix(cache): bypass hybrid custom Documents
james-elicx Sep 2, 2026
1321ccb
fix(cache): preserve adapter policy probe coverage
james-elicx Sep 2, 2026
86fb9d4
fix(build): preserve prewarm state in response stages
james-elicx Sep 2, 2026
5a9f4c6
fix(cache): fail closed on wrapped Documents
james-elicx Sep 2, 2026
69029fd
fix(cache): bypass stale Pages previews
james-elicx Sep 2, 2026
3520ecd
fix(pages): keep invalid preview cleanup private
james-elicx Sep 2, 2026
9e5d0c2
docs(pages): explain stale preview cache policy
james-elicx Sep 2, 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
3 changes: 3 additions & 0 deletions knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export default {
"src/server/app-page-element-builder.ts",
"src/server/app-hook-warning-suppression.ts",
"src/server/app-post-middleware-context.ts",
"src/server/app-request-stage-context.ts",
"src/server/app-request-stage-independent-entry.ts",
"src/server/app-request-context.ts",
"src/server/app-route-handler-middleware-context.ts",
"src/server/app-rsc-error-handler.ts",
"src/server/isr-cache.ts",
"src/server/rsc-stream-hints.ts",
Expand Down
4 changes: 4 additions & 0 deletions packages/cloudflare/src/cache/cdn-adapter.runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ function formatCacheTag(tags: readonly string[]): string | null {

export class CloudflareCdnCacheAdapter implements CdnCacheAdapter {
readonly requiresCompletedResponseAdmission = true;
readonly responsePolicyHeaderNames = [
"CDN-Cache-Control",
"Cloudflare-CDN-Cache-Control",
] as const;
readonly responseVary = "verbatim" as const;

constructor(
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/src/cache/cdn-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function cdnAdapter(options?: CdnAdapterOptions) {
options,
capabilities: {
buildIdentity: "response-header" as const,
responsePolicyHeaderNames: ["CDN-Cache-Control", "Cloudflare-CDN-Cache-Control"] as const,
responseVary: "verbatim" as const,
routeCacheability: "probe-manifest" as const,
},
Expand Down
7 changes: 7 additions & 0 deletions packages/cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
findVinextPrerenderConfigInPlugins,
findVinextRouteRootConfigInPlugins,
formatVinextPrerenderLabel,
getConfiguredCdnResponsePolicyHeaderNames,
hasBuildIdentityResponseHeader,
hasVerbatimResponseVary,
requiresRouteCacheabilityProbeManifest,
Expand Down Expand Up @@ -1921,6 +1922,9 @@ export async function deploy(options: DeployOptions): Promise<void> {
nextConfig,
buildIdentity: hasBuildIdentityHeader ? "response-header" : undefined,
responseVary: hasStrictResponseVary ? "verbatim" : undefined,
responsePolicyHeaderNames: getConfiguredCdnResponsePolicyHeaderNames(
viteConfigMetadata.cacheConfig,
),
routeRootConfig: viteConfigMetadata.routeRootConfig,
});
}
Expand Down Expand Up @@ -1993,6 +1997,9 @@ export async function deploy(options: DeployOptions): Promise<void> {
nextConfig,
buildIdentity: hasBuildIdentityHeader ? "response-header" : undefined,
responseVary: hasStrictResponseVary ? "verbatim" : undefined,
responsePolicyHeaderNames: getConfiguredCdnResponsePolicyHeaderNames(
viteConfigMetadata.cacheConfig,
),
routeRootConfig: viteConfigMetadata.routeRootConfig,
pathDiscoveryTarget: {
baseUrl: targetUrl,
Expand Down
12 changes: 12 additions & 0 deletions packages/vinext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@
"types": "./dist/server/fetch-handler.d.ts",
"import": "./dist/server/fetch-handler.js"
},
"./server/multi-stage": {
"types": "./dist/server/multi-stage.d.ts",
"import": "./dist/server/multi-stage.js"
},
"./server/request-stage": {
"types": "./dist/server/request-stage.d.ts",
"import": "./dist/server/request-stage.js"
},
"./server/response-stage": {
"types": "./dist/server/response-stage.d.ts",
"import": "./dist/server/response-stage.js"
},
"./server/app-router-entry": {
"types": "./dist/server/app-router-entry.d.ts",
"import": "./dist/server/app-router-entry.js"
Expand Down
82 changes: 82 additions & 0 deletions packages/vinext/src/build/client-build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,23 @@ export function isRscFrameworkModule(id: string): boolean {
return pkg !== null && (FRAMEWORK_PACKAGES as readonly string[]).includes(pkg);
}

/**
* Keep virtual entry ids out of emitted RSC chunk filenames.
*
* Rolldown's entries-aware chunk names can contain the `\\0` virtual-id marker,
* which is not a portable module-specifier or filesystem name. Preserve every
* other character and remove both the printable and actual-NUL forms.
*/
export function sanitizeRscChunkFileName(name: string): string {
const withoutVirtualMarkers = name.replaceAll("\\0", "");
const invalid = new Set(["<", ">", ":", '"', "/", "\\", "|", "?", "*"]);
let sanitized = "";
for (const character of withoutVirtualMarkers) {
sanitized += character.charCodeAt(0) <= 31 || invalid.has(character) ? "_" : character;
}
return sanitized;
}

/**
* Output config that isolates React (and the RSC flight runtime) into a
* dedicated "framework" chunk in the RSC server build. See
Expand All @@ -245,6 +262,7 @@ export function isRscFrameworkModule(id: string): boolean {
*/
export function createRscFrameworkChunkOutputConfig() {
return {
sanitizeFileName: sanitizeRscChunkFileName,
codeSplitting: {
groups: [
{
Expand Down Expand Up @@ -287,3 +305,67 @@ export function withBuildBundlerOptions(
): Partial<VinextBuildConfig> {
return { rolldownOptions: bundlerOptions };
}

type VinextBuildOutput = Exclude<
NonNullable<VinextBuildBundlerOptions["output"]>,
readonly unknown[]
>;
type VinextCodeSplittingConfig = Exclude<NonNullable<VinextBuildOutput["codeSplitting"]>, boolean>;
type ChunkFileNames = NonNullable<VinextBuildOutput["chunkFileNames"]>;
type ChunkFileNameFunction = Exclude<ChunkFileNames, string>;

/**
* Keep vinext modules partitioned by the stage entries that actually use them.
* Without an entry-aware catch-all, Rolldown may merge a small helper shared by
* request/response entries into a response-heavy chunk; importing that helper
* then evaluates React and renderer code on a request-stage cache hit.
*/
export function createMultiStageCodeSplittingConfig(
existing: VinextBuildOutput["codeSplitting"],
): VinextCodeSplittingConfig & { groups: NonNullable<VinextCodeSplittingConfig["groups"]> } {
const base = existing && typeof existing === "object" ? existing : {};
return {
...base,
groups: [
{
name: "vinext-stage-runtime",
test: /(?:^|[/\\])(?:packages[/\\]vinext[/\\]src|(?:packages[/\\]vinext|node_modules[/\\](?:\.pnpm[/\\][^/\\]+[/\\]node_modules[/\\])?vinext)[/\\]dist)[/\\]/,
entriesAware: true,
},
...(base.groups ?? []),
],
};
}

/**
* Keep router stage chunks beside the server entry so their generated
* `./vinext-client-assets.js` external continues to resolve. Other chunks keep
* the host's existing output pattern (or vinext's server-assets default).
*/
export function createMultiStageChunkFileNames(
assetsDir: string,
existing: VinextBuildOutput["chunkFileNames"],
): ChunkFileNameFunction {
return (chunk) => {
const name = sanitizeRscChunkFileName(chunk.name);
if (
chunk.moduleIds?.some((id) =>
/\/server\/app-ssr-entry\.[cm]?[jt]sx?$/.test(toSlash(id.split("?", 1)[0] ?? "")),
) ||
[
"app-router-entry",
"pages-router-entry",
"app-response-stage-entry",
"pages-request-stage-entry",
"pages-response-stage-entry",
"virtual_vinext-rsc-entry",
"virtual_vinext-response-stage",
].some((entryName) => name.includes(entryName))
) {
return `${name}-[hash].js`;
}
if (typeof existing === "function") return existing({ ...chunk, name });
const pattern = existing ?? joinAssetFileNamePattern(assetsDir, "[name]-[hash].js");
return pattern.replaceAll("[name]", name);
};
}
32 changes: 26 additions & 6 deletions packages/vinext/src/build/inject-pregenerated-paths.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "pathslash";
import { readPrerenderManifest } from "../server/prerender-manifest.js";
import { PREGENERATED_CONCRETE_PATHS_MODULE } from "../server/pregenerated-concrete-paths.js";
import { escapeRegExp } from "../utils/regex.js";

declare global {
Expand All @@ -15,14 +16,33 @@ const VINEXT_PREGEN_RE = new RegExp(
);

export function injectPregeneratedConcretePaths(root: string): void {
const workerEntry = path.resolve(root, "dist", "server", "index.js");
if (!fs.existsSync(workerEntry)) return;
const serverDir = path.resolve(root, "dist", "server");
const workerEntry = path.join(serverDir, "index.js");
const manifest = readPrerenderManifest(path.join(serverDir, "vinext-prerender.json"));
const table = manifest?.pregeneratedConcretePaths ?? [];

let code = fs.readFileSync(workerEntry, "utf-8").replace(VINEXT_PREGEN_RE, "");
const manifest = readPrerenderManifest(
path.join(root, "dist", "server", "vinext-prerender.json"),
// Response-stage entries can be deployed independently of index.js. Keep the
// table in a stable side-effect module imported by every generated App
// response graph so those deployments retain the same PPR fallback guard.
// The file is emitted during the server build; writing it after prerendering
// updates the artifact without rebuilding or coupling core to a host
// transport.
const runtimeModule = path.join(serverDir, PREGENERATED_CONCRETE_PATHS_MODULE);
fs.mkdirSync(serverDir, { recursive: true });
fs.writeFileSync(
runtimeModule,
table.length > 0
? `globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = ${JSON.stringify(table)};\n`
: "delete globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS;\n",
);
const table = manifest?.pregeneratedConcretePaths ?? [];

if (!fs.existsSync(workerEntry)) {
if (table.length > 0) globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = table;
else delete globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS;
return;
}

let code = fs.readFileSync(workerEntry, "utf-8").replace(VINEXT_PREGEN_RE, "");

if (table.length > 0) {
globalThis.__VINEXT_PREGENERATED_CONCRETE_PATHS = table;
Expand Down
Loading
Loading