mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
e372ff336a
Vitest integration CI shards by test file, so the 340-test App Router integration file becomes one oversized scheduling unit. That makes the slowest shard depend on one monolithic file even after adding native shards. Split the App Router integration coverage into focused files and increase the integration matrix to four native Vitest shards. This keeps fileParallelism disabled for shared-fixture safety while giving CI smaller scheduling units to distribute. Validation: vp check tests/app-router-*.test.ts vite.config.ts .github/workflows/ci.yml; vp test run --project integration tests/app-router-client-preloading.test.ts tests/app-router-dev-server.test.ts tests/app-router-external-rewrite.test.ts tests/app-router-font-google-prod.test.ts tests/app-router-isr-codegen.test.ts tests/app-router-malformed-url.test.ts tests/app-router-metadata-routes.test.ts tests/app-router-middleware-next-request.test.ts tests/app-router-next-config-codegen.test.ts tests/app-router-next-config-dev.test.ts tests/app-router-origin-check.test.ts tests/app-router-production-build.test.ts tests/app-router-production-server.test.ts tests/app-router-rsc-flight-hint.test.ts tests/app-router-rsc-plugin.test.ts tests/app-router-static-export.test.ts tests/app-router-worker-entry.test.ts
199 lines
7.5 KiB
TypeScript
199 lines
7.5 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("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,
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
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).toContain("seedMemoryCacheFromPrerender as __seedMemoryCacheFromPrerender");
|
|
expect(code).toContain("isrSetPrerenderedAppPage as __isrSetPrerenderedAppPage");
|
|
expect(code).toContain("export 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,");
|
|
});
|
|
});
|