Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
02f78c9
feat(app-router): let a cacheability manifest path appear in several …
james-elicx Sep 25, 2026
d66f713
refactor(app-router): share completed-response admission's page manif…
james-elicx Sep 25, 2026
3240ad5
feat(app-router): render Workers Cache probes in cache-candidate mode
james-elicx Sep 25, 2026
5880041
feat(cloudflare): emit the Workers Cache request-stage manifest proje…
james-elicx Sep 25, 2026
1440f99
feat(app-router): drop request-scoped RSC headers from shared respons…
james-elicx Sep 25, 2026
d5f0e1a
feat(app-router): drop the query from Workers Cache dispatches of sta…
james-elicx Sep 25, 2026
34162b9
feat(app-router): report dynamic usage and config policy in cacheabil…
james-elicx Sep 25, 2026
8f5024d
feat(cloudflare): mark traffic-picked probe paths and drop their rend…
james-elicx Sep 25, 2026
bb18bbe
feat(cloudflare): classify probed App page routes as Next.js's build …
james-elicx Sep 25, 2026
7413254
feat(cloudflare): certify the RSC representations of static App pages
james-elicx Sep 25, 2026
120bd99
test(cloudflare): assert that Workers Cache shares one entry across a…
james-elicx Sep 25, 2026
51abdc3
fix(app-router): report a route that isn't statically generated as pa…
james-elicx Sep 25, 2026
1dc0bd4
fix(cloudflare): list discovered paths only for static or SSG App pag…
james-elicx Sep 25, 2026
5ef6b74
fix(cloudflare): drop only unlisted App page render failures from the…
james-elicx Sep 25, 2026
46467af
fix(cloudflare): require the request-stage projection module for an A…
james-elicx Sep 25, 2026
62513d4
test(app-router): keep the query of nonce and Pages data dispatches u…
james-elicx Sep 25, 2026
6baacd6
test(cloudflare): cover traffic-picked-only, moved and non-renderer s…
james-elicx Sep 25, 2026
f806dbf
test(cloudflare): assert the shared Workers Cache entry carries no qu…
james-elicx Sep 25, 2026
cf6096c
fix(cloudflare): read discovered routes' runtime from their whole loa…
james-elicx Sep 25, 2026
c9a3376
fix(cloudflare): read an MDX page's segment config from its ESM blocks
james-elicx Sep 25, 2026
a2594a1
fix(cloudflare): ignore type-only generateStaticParams exports in dis…
james-elicx Sep 25, 2026
509c38d
fix(cloudflare): judge an unlisted render failure under the route the…
james-elicx Sep 25, 2026
b0cef70
fix(build): keep the App request stage's chunks beside the server entry
james-elicx Sep 25, 2026
7fcd06d
fix(cloudflare): require the Worker graph to import the request-stage…
james-elicx Sep 25, 2026
42e6075
fix(cloudflare): keep MDX ESM open across blank lines until it parses
james-elicx Sep 25, 2026
93c9edd
feat(app-router): authorize a runtime-checked representation at exact…
james-elicx Sep 25, 2026
f3dd187
fix(cloudflare): keep the loading shell warm when a dynamic API exclu…
james-elicx Sep 25, 2026
71d9dd6
fix(cloudflare): read an MDX page's ESM from the MDX parser
james-elicx Sep 25, 2026
8c2806a
fix(cloudflare): find the MDX parser beside an app's own MDX plugin
james-elicx Sep 25, 2026
74453ea
fix(cloudflare): don't certify an unreadable MDX route's static fallback
james-elicx Sep 25, 2026
103d869
fix(cloudflare): keep a destination's build-time listing fixed while …
james-elicx Sep 25, 2026
cc88a39
fix(cloudflare): keep paired representations when a moved unlisted fa…
james-elicx Sep 25, 2026
60e121d
fix(cloudflare): leave a static page's loading shell runtime-checked
james-elicx Sep 25, 2026
738f57e
fix(cloudflare): keep discovery's unlisted state for a literal App pa…
james-elicx Sep 25, 2026
fa5a1dc
fix(cloudflare): key a path's listing by the route that owns it at ru…
james-elicx Sep 25, 2026
7801a78
test(cloudflare): share the Suspense useSearchParams() page across qu…
james-elicx Sep 25, 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
62 changes: 44 additions & 18 deletions packages/cloudflare/src/cacheability-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import path from "node:path";
import { Buffer } from "node:buffer";
import {
CACHEABILITY_MANIFEST_MODULE,
CACHEABILITY_REQUEST_PROJECTION_MODULE,
projectCacheabilityManifestForRequestStage,
type CacheabilityManifest,
} from "vinext/internal/server/cacheability-manifest";
import {
Expand Down Expand Up @@ -223,7 +225,7 @@ function resolveBuildOutputArtifact(root: string): GeneratedWorkerArtifact {
return { configPath, main, serverDirectory: path.join(path.dirname(configPath), "bundle") };
}

function assertManifestModuleReachable(artifact: GeneratedWorkerArtifact): void {
function assertModuleReachable(artifact: GeneratedWorkerArtifact, moduleName: string): void {
const { main, serverDirectory } = artifact;
const mainPath = path.resolve(serverDirectory, main);
if (!fs.existsSync(mainPath) || !fs.lstatSync(mainPath).isFile()) {
Expand Down Expand Up @@ -253,16 +255,11 @@ function assertManifestModuleReachable(artifact: GeneratedWorkerArtifact): void
if (!entry || typeof entry.file !== "string") continue;
const modulePath = path.resolve(serverDirectory, entry.file);
if (fs.existsSync(modulePath) && fs.lstatSync(modulePath).isFile()) {
const relativeManifest = path
.relative(
path.dirname(modulePath),
path.join(serverDirectory, CACHEABILITY_MANIFEST_MODULE),
)
const relativeModule = path
.relative(path.dirname(modulePath), path.join(serverDirectory, moduleName))
.split(path.sep)
.join("/");
const specifier = relativeManifest.startsWith(".")
? relativeManifest
: `./${relativeManifest}`;
const specifier = relativeModule.startsWith(".") ? relativeModule : `./${relativeModule}`;
reachable = hasStaticModuleSpecifier(fs.readFileSync(modulePath, "utf8"), specifier);
}
for (const references of [entry.imports, entry.dynamicImports]) {
Expand All @@ -273,15 +270,31 @@ function assertManifestModuleReachable(artifact: GeneratedWorkerArtifact): void
}
if (!reachable) {
throw new Error(
`Two-stage CDN warming requires the generated Worker graph to statically import ${CACHEABILITY_MANIFEST_MODULE}.`,
`Two-stage CDN warming requires the generated Worker graph to statically import ${moduleName}.`,
);
}
}

function writeStringModule(modulePath: string, value: string): void {
const source = `export default ${JSON.stringify(value)};\n`;
const pendingPath = `${modulePath}.${process.pid}.tmp`;
try {
fs.writeFileSync(pendingPath, source, "utf8");
fs.renameSync(pendingPath, modulePath);
} finally {
if (fs.existsSync(pendingPath)) fs.unlinkSync(pendingPath);
}
}

/**
* Write the version-specific manifest into the built Worker artifact.
* The application build already imports this stable module asset, so the
* completed dist directory remains the exact input to the final upload.
*
* App Router builds also emit the request stage's projection module. It
* carries the App page routes that can admit a query-free entry, so the
* request stage can strip the query from those dispatches without loading the
* full manifest.
*/
export function writeCacheabilityManifestArtifact(
root: string,
Expand All @@ -293,7 +306,7 @@ export function writeCacheabilityManifestArtifact(
deploymentTool === "cf"
? resolveBuildOutputArtifact(root)
: resolveGeneratedWranglerArtifact(root, configuredPath);
assertManifestModuleReachable(artifact);
assertModuleReachable(artifact, CACHEABILITY_MANIFEST_MODULE);
const { configPath, serverDirectory } = artifact;
const manifestPath = path.join(serverDirectory, CACHEABILITY_MANIFEST_MODULE);
if (!fs.existsSync(manifestPath) || !fs.lstatSync(manifestPath).isFile()) {
Expand All @@ -307,13 +320,26 @@ export function writeCacheabilityManifestArtifact(
throw cacheabilityManifestByteLimitError(manifestBytes);
}

const manifestSource = `export default ${JSON.stringify(serializedManifest)};\n`;
const pendingManifestPath = `${manifestPath}.${process.pid}.tmp`;
try {
fs.writeFileSync(pendingManifestPath, manifestSource, "utf8");
fs.renameSync(pendingManifestPath, manifestPath);
} finally {
if (fs.existsSync(pendingManifestPath)) fs.unlinkSync(pendingManifestPath);
const projectionPath = path.join(serverDirectory, CACHEABILITY_REQUEST_PROJECTION_MODULE);
const hasProjectionModule =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Verify that the Worker graph imports the projection

The build plugin emits this asset for every App build even when no runtime module imports it, so file existence does not establish that the request stage can read the projection. A stale or mismatched artifact can pass this check, get overwritten successfully, and still serve full-query dispatches while the response stage admits query-free identities. Require static reachability from the Worker entry, analogous to assertManifestModuleReachable().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9513136. The writer now requires the projection to exist and to be statically reachable from the Wrangler main module. This exposed a real bug: the request-stage chunks were emitted under _next/static/, so their projection import pointed at a file that doesn't exist. That is fixed in 77154b9.

fs.existsSync(projectionPath) && fs.lstatSync(projectionPath).isFile();
// Without the projection, the request stage would never drop the query for
// the App page paths this manifest certifies.
if (Object.values(manifest.routes).some((route) => route.kind === "app-page")) {
if (!hasProjectionModule) {
throw new Error(
`Two-stage CDN warming requires ${CACHEABILITY_REQUEST_PROJECTION_MODULE} in the generated Worker artifact. Rebuild the app before deploying.`,
);
}
assertModuleReachable(artifact, CACHEABILITY_REQUEST_PROJECTION_MODULE);
}

writeStringModule(manifestPath, serializedManifest);
if (hasProjectionModule) {
writeStringModule(
projectionPath,
JSON.stringify(projectCacheabilityManifestForRequestStage(manifest)),
);
}
return path.relative(root, configPath);
}
Loading
Loading