Files
cloudflare__vinext/tests/app-segment-config.test.ts
T
Nathan Nguyen 6b7e7557a9 fix: support revalidate = false for App route segment config (#1046)
* fix: support revalidate = false for App route segment config (#1041)

- Build-time: extractExportConstNumber now matches false and returns Infinity
- Classifiers: classifyLayoutSegmentConfig and classifyAppRoute correctly
  classify revalidate = false as static (indefinite caching)
- Runtime: getAppRouteHandlerRevalidateSeconds returns Infinity for false,
  enabling ISR cache gates that emit STATIC cache-control instead of SWR
- Template: RSC entry resolves revalidate = false from page exports as Infinity
- Cache-Control: buildRouteHandlerCacheControl emits STATIC cache-control
  when revalidateSeconds is Infinity, matching the page-path behavior

Ported from Next.js segment config validation:
https://github.com/vercel/next.js/blob/canary/packages/next/src/build/segment-config/app/app-segment-config.ts

* fix: exclude Infinity from route handler ISR read/write gates

* Update packages/vinext/src/build/report.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-04 17:04:39 +01:00

219 lines
5.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
resolveAppPageFetchCacheMode,
resolveAppPageSegmentConfig,
} from "../packages/vinext/src/server/app-segment-config.js";
describe("resolveAppPageSegmentConfig", () => {
it("returns defaults when no segment config is present", () => {
expect(resolveAppPageSegmentConfig({})).toEqual({
revalidateSeconds: null,
});
});
it("merges route segment config from layouts and the page", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [
{ revalidate: 120, dynamicParams: false, fetchCache: "default-cache" },
{ dynamic: "error", revalidate: 60 },
],
page: { dynamic: "force-static", revalidate: 300 },
}),
).toEqual({
dynamicConfig: "force-static",
dynamicParamsConfig: false,
fetchCache: "default-cache",
revalidateSeconds: 60,
});
});
it("treats force-dynamic from any effective segment as revalidate zero", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ revalidate: 60 }],
page: { dynamic: "force-dynamic" },
}),
).toEqual({
dynamicConfig: "force-dynamic",
fetchCache: "force-no-store",
revalidateSeconds: 0,
});
});
it("derives fetchCache from static-only dynamic modes", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "error" }],
page: {},
}),
).toEqual({
dynamicConfig: "error",
dynamicParamsConfig: false,
fetchCache: "only-cache",
revalidateSeconds: null,
});
});
it("lets explicit fetchCache override the dynamic mode default", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "error" }],
page: { fetchCache: "default-cache" },
}),
).toEqual({
dynamicConfig: "error",
dynamicParamsConfig: false,
fetchCache: "default-cache",
revalidateSeconds: null,
});
});
it("resolves fetchCache force modes with route-level precedence", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ fetchCache: "only-cache" }],
page: { fetchCache: "force-cache" },
}).fetchCache,
).toBe("force-cache");
expect(
resolveAppPageSegmentConfig({
layouts: [{ fetchCache: "force-no-store" }],
page: { fetchCache: "only-no-store" },
}).fetchCache,
).toBe("force-no-store");
});
it("rejects incompatible cross-segment fetchCache modes", () => {
expect(() =>
resolveAppPageSegmentConfig({
layouts: [{ fetchCache: "only-cache" }],
page: { fetchCache: "only-no-store" },
}),
).toThrow(/incompatible fetchCache/);
expect(() =>
resolveAppPageSegmentConfig({
layouts: [{ fetchCache: "force-cache" }],
page: { fetchCache: "force-no-store" },
}),
).toThrow(/incompatible fetchCache/);
expect(() =>
resolveAppPageSegmentConfig({
layouts: [{ fetchCache: "default-no-store" }],
page: { fetchCache: "auto" },
}),
).toThrow(/incompatible fetchCache/);
});
it("ignores unknown dynamic values", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "sometimes" }],
page: { dynamic: "force-static" },
}),
).toEqual({
dynamicConfig: "force-static",
dynamicParamsConfig: false,
revalidateSeconds: null,
});
});
it("defaults dynamicParams to false for static-only dynamic modes", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "error" }],
page: {},
}),
).toEqual({
dynamicConfig: "error",
dynamicParamsConfig: false,
fetchCache: "only-cache",
revalidateSeconds: null,
});
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "force-static" }],
page: {},
}),
).toEqual({
dynamicConfig: "force-static",
dynamicParamsConfig: false,
revalidateSeconds: null,
});
});
it("lets explicit dynamicParams override static-only dynamic defaults", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamic: "error" }],
page: { dynamicParams: true },
}),
).toEqual({
dynamicConfig: "error",
dynamicParamsConfig: true,
fetchCache: "only-cache",
revalidateSeconds: null,
});
});
it("keeps explicit dynamicParams false sticky across child segments", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ dynamicParams: false }],
page: { dynamicParams: true },
}),
).toEqual({
dynamicParamsConfig: false,
revalidateSeconds: null,
});
});
it("resolves revalidate = false as Infinity (cache indefinitely)", () => {
expect(
resolveAppPageSegmentConfig({
page: { revalidate: false },
}),
).toEqual({
revalidateSeconds: Infinity,
});
});
it("resolves shortest-wins: finite revalidate beats false (Infinity)", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ revalidate: 60 }],
page: { revalidate: false },
}).revalidateSeconds,
).toBe(60);
});
it("resolves shortest-wins: false (Infinity) loses to any finite value", () => {
expect(
resolveAppPageSegmentConfig({
layouts: [{ revalidate: false }],
page: { revalidate: 60 },
}).revalidateSeconds,
).toBe(60);
});
it("resolves just the fetchCache mode for route-specific render scopes", () => {
expect(
resolveAppPageFetchCacheMode({
layouts: [{ fetchCache: "only-cache" }],
page: {},
}),
).toBe("only-cache");
expect(
resolveAppPageFetchCacheMode({
layouts: [{ revalidate: 60 }],
page: {},
}),
).toBeNull();
});
});