mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
d0839903e8
* fix(app-router): emit x-edge-runtime marker on edge RSC route response Closes #1531 The Next.js edge-runtime app template (edge-ssr-app.ts) sets `x-edge-runtime: 1` on every App Router response. vinext serves App Router responses from a Cloudflare Worker (an edge-equivalent runtime) but was not emitting this marker, so Next.js' upstream test suite (`test/e2e/app-dir/app/index.test.ts`) saw `null` for the header. Add the header to both the RSC and HTML response builders so the assertion passes for any App Router page, regardless of which payload shape the client requests. * fix(app-router): gate x-edge-runtime marker on configured runtime Only emit `x-edge-runtime: 1` when the route's resolved segment config opts into the edge runtime (`export const runtime = "edge"` or `"experimental-edge"`). Plumb the resolved runtime from the segment config through dispatch and the render lifecycle so both RSC and HTML response builders see it. Without the gate, nodejs-runtime routes also advertise the marker, which hides the configured runtime from downstream consumers.
247 lines
6.5 KiB
TypeScript
247 lines
6.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
isEdgeRuntime,
|
|
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();
|
|
});
|
|
|
|
it("captures the runtime export and lets child segments override parents", () => {
|
|
expect(
|
|
resolveAppPageSegmentConfig({
|
|
layouts: [{ runtime: "nodejs" }],
|
|
page: { runtime: "edge" },
|
|
}).runtime,
|
|
).toBe("edge");
|
|
|
|
expect(
|
|
resolveAppPageSegmentConfig({
|
|
layouts: [{ runtime: "edge" }],
|
|
page: {},
|
|
}).runtime,
|
|
).toBe("edge");
|
|
|
|
expect(resolveAppPageSegmentConfig({ page: {} }).runtime).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("isEdgeRuntime", () => {
|
|
it("matches Next.js' edge-runtime values", () => {
|
|
expect(isEdgeRuntime("edge")).toBe(true);
|
|
expect(isEdgeRuntime("experimental-edge")).toBe(true);
|
|
expect(isEdgeRuntime("nodejs")).toBe(false);
|
|
expect(isEdgeRuntime(undefined)).toBe(false);
|
|
});
|
|
});
|