mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
214 lines
8.0 KiB
TypeScript
214 lines
8.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { generateRscEntry } from "../packages/vinext/src/entries/app-rsc-entry.js";
|
|
import type { AppRoute } from "../packages/vinext/src/routing/app-router.js";
|
|
|
|
describe("generateRscEntry ISR code generation", () => {
|
|
// Minimal route list — only the generated ISR guard logic matters here
|
|
const minimalRoutes = [
|
|
{
|
|
pattern: "/",
|
|
pagePath: "/tmp/test/app/page.tsx",
|
|
routePath: null,
|
|
layouts: ["/tmp/test/app/layout.tsx"],
|
|
templates: [],
|
|
parallelSlots: [],
|
|
loadingPath: null,
|
|
errorPath: null,
|
|
layoutErrorPaths: [null],
|
|
notFoundPath: null,
|
|
forbiddenPaths: [],
|
|
forbiddenPath: null,
|
|
unauthorizedPaths: [],
|
|
unauthorizedPath: null,
|
|
routeSegments: [],
|
|
layoutTreePositions: [0],
|
|
isDynamic: false,
|
|
params: [],
|
|
},
|
|
] as any[];
|
|
|
|
it('generated code contains process.env.NODE_ENV === "production" guard for ISR cache read', () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes);
|
|
expect(code).toContain('process.env.NODE_ENV === "production"');
|
|
});
|
|
|
|
it("generated handler delegates request and ctx handling to createAppRscHandler", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes);
|
|
expect(code).toContain("createAppRscHandler");
|
|
expect(code).toContain("export default createAppRscHandler({");
|
|
});
|
|
|
|
it("configures the cache through the lightweight handler runtime", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes);
|
|
expect(code).toContain(
|
|
'configureMemoryCacheHandler as __configureMemoryCacheHandler } from "vinext/shims/cache-handler"',
|
|
);
|
|
expect(code).not.toContain(
|
|
'configureMemoryCacheHandler as __configureMemoryCacheHandler } from "next/cache"',
|
|
);
|
|
});
|
|
|
|
it("generated code stores root layout params separately from leaf params", () => {
|
|
const routes = [
|
|
{
|
|
...minimalRoutes[0],
|
|
pattern: "/[lang]/[locale]/other/[slug]",
|
|
patternParts: [":lang", ":locale", "other", ":slug"],
|
|
params: ["lang", "locale", "slug"],
|
|
rootParamNames: ["lang", "locale"],
|
|
routeSegments: ["[lang]", "[locale]", "other", "[slug]"],
|
|
layoutTreePositions: [2],
|
|
},
|
|
] as any[];
|
|
|
|
const code = generateRscEntry("/tmp/test/app", routes);
|
|
|
|
// The user-declared rootParamNames must flow through to the route's entry,
|
|
// narrower than the full leaf params list. The typed RSC handler owns
|
|
// setting the per-request root params from this route shape.
|
|
expect(code).toContain('rootParamNames: ["lang","locale"]');
|
|
expect(code).not.toContain('rootParamNames: ["lang","locale","slug"]');
|
|
expect(code).toContain("rootParamNamesByPattern: rootParamNamesMap");
|
|
expect(code).not.toContain("__setRootParams(__pickRootParams(params, route.rootParamNames));");
|
|
expect(code).toContain("clearAppRequestContext as __clearRequestContext");
|
|
expect(code).toContain("server/app-request-context.js");
|
|
expect(code).not.toContain("function __clearRequestContext() {");
|
|
});
|
|
|
|
it("root params runtime getter returns current request values", async () => {
|
|
const { getRootParam, pickRootParams, setRootParams } =
|
|
await import("../packages/vinext/src/shims/root-params.js");
|
|
|
|
expect(pickRootParams({ lang: "en", locale: "us", slug: "post" }, ["lang", "locale"])).toEqual({
|
|
lang: "en",
|
|
locale: "us",
|
|
});
|
|
|
|
setRootParams({ lang: "en", locale: "us" });
|
|
|
|
await expect(getRootParam("lang")).resolves.toBe("en");
|
|
await expect(getRootParam("locale")).resolves.toBe("us");
|
|
await expect(getRootParam("slug")).resolves.toBeUndefined();
|
|
|
|
setRootParams(null);
|
|
});
|
|
|
|
it("generated code threads intercept layout modules through slot overrides", () => {
|
|
const routeWithInterceptLayouts: AppRoute = {
|
|
errorPath: null,
|
|
forbiddenPaths: [],
|
|
forbiddenPath: null,
|
|
isDynamic: false,
|
|
layoutErrorPaths: [null],
|
|
layouts: ["/tmp/test/app/layout.tsx"],
|
|
layoutTreePositions: [0],
|
|
loadingPath: null,
|
|
notFoundPath: null,
|
|
notFoundPaths: [null],
|
|
pagePath: "/tmp/test/app/page.tsx",
|
|
parallelSlots: [
|
|
{
|
|
defaultPath: "/tmp/test/app/@modal/default.tsx",
|
|
errorPath: null,
|
|
interceptingRoutes: [
|
|
{
|
|
convention: ".",
|
|
layoutPaths: ["/tmp/test/app/@modal/(.)explicit-layout/layout.tsx"],
|
|
pagePath: "/tmp/test/app/@modal/(.)explicit-layout/deeper/page.tsx",
|
|
params: [],
|
|
targetPattern: "/explicit-layout/deeper",
|
|
sourceMatchPattern: "/",
|
|
},
|
|
],
|
|
key: "modal@@modal",
|
|
layoutIndex: 0,
|
|
layoutPath: "/tmp/test/app/@modal/layout.tsx",
|
|
loadingPath: null,
|
|
name: "modal",
|
|
hasPage: false,
|
|
ownerDir: "/tmp/test/app/@modal",
|
|
ownerTreePath: "/",
|
|
pagePath: null,
|
|
routeSegments: null,
|
|
},
|
|
],
|
|
params: [],
|
|
pattern: "/",
|
|
patternParts: [],
|
|
routePath: null,
|
|
routeSegments: [],
|
|
templates: [],
|
|
templateTreePositions: [],
|
|
unauthorizedPaths: [],
|
|
unauthorizedPath: null,
|
|
siblingIntercepts: [],
|
|
};
|
|
|
|
const code = generateRscEntry("/tmp/test/app", [routeWithInterceptLayouts]);
|
|
|
|
// Intercept-layout modules must be wired into the route's intercept entry
|
|
// (mod_N is the generator's import alias scheme — `interceptLayouts: [mod_`
|
|
// confirms a module reference, not the original layout path string).
|
|
expect(code).toContain("interceptLayouts: [mod_");
|
|
expect(code).not.toMatch(/interceptLayouts:\s*\[\s*"\/tmp\/test\/app/);
|
|
});
|
|
|
|
it("generated code seeds root params around prerender generateStaticParams", () => {
|
|
const routeWithRootParams: AppRoute = {
|
|
errorPath: null,
|
|
forbiddenPath: null,
|
|
forbiddenPaths: [],
|
|
isDynamic: true,
|
|
layoutErrorPaths: [null],
|
|
layouts: ["/tmp/test/app/[locale]/layout.tsx"],
|
|
layoutTreePositions: [1],
|
|
loadingPath: null,
|
|
notFoundPath: null,
|
|
notFoundPaths: [null],
|
|
pagePath: "/tmp/test/app/[locale]/blog/[slug]/page.tsx",
|
|
parallelSlots: [],
|
|
params: ["locale", "slug"],
|
|
pattern: "/:locale/blog/:slug",
|
|
patternParts: [":locale", "blog", ":slug"],
|
|
rootParamNames: ["locale"],
|
|
routePath: null,
|
|
routeSegments: ["[locale]", "blog", "[slug]"],
|
|
templates: [],
|
|
templateTreePositions: [],
|
|
unauthorizedPaths: [],
|
|
unauthorizedPath: null,
|
|
siblingIntercepts: [],
|
|
};
|
|
|
|
const code = generateRscEntry("/tmp/test/app", [routeWithRootParams]);
|
|
|
|
// The user-declared dynamic-segment names must flow into the generated
|
|
// entry so prerender static-params know which params are root-scoped.
|
|
expect(code).toContain('"/:locale/blog/:slug"');
|
|
expect(code).toContain('["locale"]');
|
|
});
|
|
|
|
it("generated code exposes prerender cache seeding from the RSC module graph", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes);
|
|
|
|
expect(code).toMatch(/await import\("[^"]*seed-cache\.js"\)/);
|
|
expect(code).not.toMatch(
|
|
/import \{\s*seedMemoryCacheFromPrerender as __seedMemoryCacheFromPrerender/,
|
|
);
|
|
expect(code).toContain("isrSetPrerenderedAppPage as __isrSetPrerenderedAppPage");
|
|
expect(code).toContain("export async function seedMemoryCacheFromPrerender(serverDir)");
|
|
expect(code).toContain("buildAppPageHtmlKey(pathname)");
|
|
expect(code).toContain("return __isrHtmlKey(pathname)");
|
|
expect(code).toContain("buildAppPageRscKey(pathname)");
|
|
expect(code).toContain("return __isrRscKey(pathname)");
|
|
expect(code).toContain("writeAppPageEntry(key, data, metadata)");
|
|
expect(code).toContain("return __isrSetPrerenderedAppPage(key, data, metadata)");
|
|
});
|
|
|
|
it("generated code delegates server-action header handling to the typed handler", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes);
|
|
expect(code).toContain("handleServerActionRequest({");
|
|
expect(code).toContain("actionId,");
|
|
});
|
|
});
|