mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
3a1e56748f
* fix(app-router): emit pages client entry for hybrid builds * fix pages router navigation params readiness * fix(pages-router): track hashed fallback client entry * fix(pages-router): honor navigation readiness in hooks * fix(pages-router): derive server router.isReady from SSR readiness Address review feedback on #1741: - getRouterSnapshot() now reads navigationIsReady from the SSR context on the server instead of unconditionally returning true. For pre-ready Pages routes (auto-export dynamic / query-string / rewrite-capable builds) this keeps useRouter().isReady consistent between SSR and client hydration, avoiding a hydration mismatch for components that read it in JSX. Mirrors Next.js render.tsx's server readiness rule. - Drop the redundant guard branch in getClientParamsSnapshot. - Add a regression test asserting server useRouter().isReady reflects the SSR navigationIsReady context. * fix(pages-router): align param resolution and dev/prod readiness parity Address review feedback (ask-bonk + self-review) on #1741: - Align client Pages navigation params to query-first, matching the server snapshot and Next.js's adaptForPathParams (derives path params from router.query). __NEXT_DATA__.query is kept current on client navigation, so the two snapshots are now provably identical and avoid a latent post- hydration mismatch. - Populate window.__VINEXT_PAGE_PATTERNS__ in the dev server (own script tag) so the next/navigation compat hooks can resolve a dynamic pattern from a resolved path in dev exactly as in production. Closes a dev/prod parity gap (prod set this via the client entry; dev never did). - Drop the redundant pre-readiness setSSRContext in dev; the single post- module-load call now publishes navigationIsReady before rendering, matching the prod handler. - Document that installPagesClientAssetGlobals is intentionally inert for App-only builds, and that getReadonlyPagesSearchParams' string-keyed cache is safe to share across concurrent SSR requests (immutable wrappers). - Add a dev regression test asserting __VINEXT_PAGE_PATTERNS__ exposure. * fix(pages-router): prefer URL params for client snapshots * fix(app-router): gate pages asset globals * test(app-router): add production test for hybrid basePath + assetPrefix * fix(navigation): widen usePathname return type to string | null * refactor(pages-router): dedupe navigation readiness modeling and cache pattern scan Addresses code-review cleanup findings on the Pages Router navigation readiness work: - Extract buildPagesReadinessNextData() so the dev SSR handler and the production Pages page handler derive the gssp/gsp/gip/appGip/autoExport __NEXT_DATA__ readiness flags from one place, instead of two hand-kept copies that could drift and produce a dev/prod hydration mismatch. - Extract resolvePagesNavigationPathname() shared by the client and SSR branches of getPagesNavigationContext, so the pathname null-ness rule (fallback / pre-ready auto-export dynamic) cannot diverge between them. - Memoize resolvePagesRoutePatternForPath()'s pattern scan, which ran an uncached O(routes) loop on every useSyncExternalStore snapshot read. - In getServerSearchParamsSnapshot, serialize the source only when the object-identity fast path misses, avoiding a toString() on every read. * refactor(pages-router): consistent ISR readiness data + robustness cleanups Addresses further code-review findings: - Thread the readiness flags (gssp/gsp/gip/appGip/autoExport) through the ISR background-regeneration path: renderPagesIsrHtml now serializes the same __NEXT_DATA__ readiness state the initial render emits, so a regenerated, re-cached page hydrates with the identical initial router.isReady the server computed. Previously these flags were dropped on that path — benign today (ISR-eligible getStaticProps routes never trigger the flag-gated early-ready branches) but a latent dev/prod hydration-readiness drift. Adds a shared PagesNextDataExtras type so every render path stays in lockstep. - readSsrManifest: degrade a malformed/unparseable SSR manifest to "no SSR manifest" with a warning instead of throwing and aborting server startup, restoring the previous tolerant behavior. - pages-readiness: project PagesReadinessNextData from the canonical VinextNextData and accept PagesPageModule, instead of bespoke types that could drift from the __NEXT_DATA__ shape. - getReadonlyPagesSearchParams: drop the redundant string-keyed cache slot; the per-object WeakMap is authoritative and concurrency-safe on its own. - isPagesRouterReady: drop the redundant server-side window check. * fix(pages-router): restore string-keyed readonly search-params cache Reverts the cache simplification from the previous commit. The string-keyed slot is load-bearing, not redundant: across the Pages Router pre-ready → ready transition the navigation context swaps in a new URLSearchParams instance even when the query string is unchanged, so the per-object WeakMap alone returns a fresh ReadonlyURLSearchParams wrapper and useSearchParams() loses Object.is stability — re-firing a [searchParams] effect. Caught by the rewrite-driven pre-ready static page e2e (search-params-snapshots saw ['',''] not ['']). Keeps the improved comment explaining why both cache levels are needed. --------- Co-authored-by: James <james@eli.cx>
432 lines
17 KiB
TypeScript
432 lines
17 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { createServer } from "vite";
|
|
import { describe, expect, it } from "vitest";
|
|
import { generateRscEntry } from "../packages/vinext/src/entries/app-rsc-entry.js";
|
|
import { generateSsrEntry } from "../packages/vinext/src/entries/app-ssr-entry.js";
|
|
import vinext from "../packages/vinext/src/index.js";
|
|
|
|
describe("App Router next.config.js features (generateRscEntry)", () => {
|
|
// Use a minimal route list for testing — we only care about the generated config handling code
|
|
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: [],
|
|
},
|
|
{
|
|
pattern: "/about",
|
|
pagePath: "/tmp/test/app/about/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: [],
|
|
},
|
|
{
|
|
pattern: "/blog/:slug",
|
|
pagePath: "/tmp/test/app/blog/[slug]/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: true,
|
|
params: ["slug"],
|
|
},
|
|
] as any[];
|
|
|
|
it("generates redirect handling code when redirects are provided", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
redirects: [
|
|
{ source: "/old-about", destination: "/about", permanent: true },
|
|
{ source: "/old-blog/:slug", destination: "/blog/:slug", permanent: false },
|
|
],
|
|
});
|
|
expect(code).toContain("createAppRscHandler");
|
|
expect(code).toContain("__configRedirects");
|
|
expect(code).toContain("configRedirects: __configRedirects");
|
|
expect(code).toContain("/old-about");
|
|
expect(code).toContain("/old-blog/:slug");
|
|
expect(code).toContain("permanent");
|
|
});
|
|
|
|
it("generates rewrite handling code when rewrites are provided", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [{ source: "/before-rewrite", destination: "/about" }],
|
|
afterFiles: [{ source: "/after-rewrite", destination: "/about" }],
|
|
fallback: [{ source: "/fallback-rewrite", destination: "/about" }],
|
|
},
|
|
});
|
|
expect(code).toContain("__configRewrites");
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain("beforeFiles");
|
|
expect(code).toContain("afterFiles");
|
|
expect(code).toContain("fallback");
|
|
expect(code).toContain("/before-rewrite");
|
|
expect(code).toContain("/after-rewrite");
|
|
expect(code).toContain("/fallback-rewrite");
|
|
});
|
|
|
|
it("generates custom header handling code when headers are provided", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
headers: [{ source: "/api/(.*)", headers: [{ key: "X-Custom-Header", value: "vinext" }] }],
|
|
});
|
|
expect(code).toContain("__configHeaders");
|
|
expect(code).toContain("configHeaders: __configHeaders");
|
|
expect(code).toContain("X-Custom-Header");
|
|
expect(code).toContain("vinext");
|
|
});
|
|
|
|
it("routes hybrid Pages API misses through the Pages server entry", () => {
|
|
// Ported from Next.js: test/e2e/og-api/index.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/og-api/index.test.ts
|
|
//
|
|
// In a mixed app/ + pages/ project, a Pages Router API route such as
|
|
// pages/api/og.js remains a real route even though the production server
|
|
// enters through the App Router handler.
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
hasPagesDir: true,
|
|
});
|
|
|
|
expect(code).toContain("renderPagesFallback as __renderPagesFallback");
|
|
expect(code).toContain("server/app-pages-bridge.js");
|
|
expect(code).toContain("return __renderPagesFallback(");
|
|
expect(code).toContain('return import.meta.viteRsc.loadModule("ssr", "index");');
|
|
expect(code).toContain("buildRequestHeaders: __buildRequestHeadersFromMiddlewareResponse");
|
|
expect(code).toContain(
|
|
"applyRouteHandlerMiddlewareContext: __applyRouteHandlerMiddlewareContext",
|
|
);
|
|
});
|
|
|
|
it("exports whether the App Router build includes Pages Router routes", () => {
|
|
const appOnlyCode = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false);
|
|
const hybridCode = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
hasPagesDir: true,
|
|
});
|
|
|
|
expect(appOnlyCode).toContain("export const __hasPagesDir = false;");
|
|
expect(hybridCode).toContain("export const __hasPagesDir = true;");
|
|
});
|
|
|
|
it("re-exports Pages API handling from the hybrid SSR entry", () => {
|
|
// Ported from Next.js: test/e2e/og-api/index.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/og-api/index.test.ts
|
|
//
|
|
// The production App Router entry loads the SSR environment's "index"
|
|
// module for Pages fallbacks. That bridge must expose the Pages API
|
|
// dispatcher as well as page rendering.
|
|
const code = generateSsrEntry(true);
|
|
|
|
expect(code).toContain(
|
|
'export { handleApiRoute, pageRoutes, renderPage } from "virtual:vinext-server-entry";',
|
|
);
|
|
});
|
|
|
|
it("embeds basePath and trailingSlash alongside config", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "/app", true, {
|
|
redirects: [{ source: "/old", destination: "/new", permanent: true }],
|
|
});
|
|
// User-provided basePath and trailingSlash must flow through into the
|
|
// generated entry alongside redirect config.
|
|
expect(code).toContain('"/app"');
|
|
expect(code).toContain("/old");
|
|
});
|
|
|
|
it("includes config pattern matching function for regex patterns", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
redirects: [{ source: "/docs/:path*", destination: "/wiki/:path*", permanent: false }],
|
|
});
|
|
expect(code).toContain("configRedirects: __configRedirects");
|
|
expect(code).toContain(":path*");
|
|
});
|
|
|
|
it("delegates request lifecycle to the typed App RSC handler", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
redirects: [{ source: "/old", destination: "/new", permanent: true }],
|
|
});
|
|
expect(code).toContain("export default __createAppRscHandler({");
|
|
expect(code).toContain("configRedirects: __configRedirects");
|
|
expect(code).toContain("dispatchMatchedPage({");
|
|
expect(code).toContain(" clientReuseManifest,");
|
|
expect(code).toContain(" rootParams,\n request,");
|
|
expect(code).toContain(" clientReuseManifest,");
|
|
expect(code).toContain(" rootParams,\n probeLayoutAt");
|
|
expect(code).toContain("dispatchMatchedRouteHandler({");
|
|
expect(code).toContain("matchRoute,");
|
|
});
|
|
|
|
it("describes beforeFiles rewrites in the generated app shape", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [{ source: "/old", destination: "/new" }],
|
|
afterFiles: [],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain('"beforeFiles":[{"source":"/old","destination":"/new"}]');
|
|
});
|
|
|
|
it("passes the typed handler the generated route matcher and config", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [{ source: "/old", destination: "/new" }],
|
|
afterFiles: [],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain("const __routeMatcher = __createAppRscRouteMatcher(routes);");
|
|
expect(code).toContain("matchRoute,");
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
});
|
|
|
|
it("describes afterFiles rewrites in the generated app shape", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [],
|
|
afterFiles: [{ source: "/old", destination: "/new" }],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain('"afterFiles":[{"source":"/old","destination":"/new"}]');
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
});
|
|
|
|
it("applies fallback rewrites when no route matches", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [],
|
|
afterFiles: [],
|
|
fallback: [{ source: "/fallback", destination: "/about" }],
|
|
},
|
|
});
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain('"fallback":[{"source":"/fallback","destination":"/about"}]');
|
|
});
|
|
|
|
it("describes external beforeFiles rewrites in the generated config", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [{ source: "/ph/:path*", destination: "https://us.i.posthog.com/:path*" }],
|
|
afterFiles: [],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain("https://us.i.posthog.com/:path*");
|
|
});
|
|
|
|
it("describes external afterFiles rewrites in the generated config", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [],
|
|
afterFiles: [{ source: "/api/:path*", destination: "https://api.example.com/:path*" }],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain("https://api.example.com/:path*");
|
|
});
|
|
|
|
it("describes external fallback rewrites in the generated config", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [],
|
|
afterFiles: [],
|
|
fallback: [
|
|
{ source: "/fallback/:path*", destination: "https://fallback.example.com/:path*" },
|
|
],
|
|
},
|
|
});
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
expect(code).toContain("https://fallback.example.com/:path*");
|
|
});
|
|
|
|
it("passes basePath and redirect config to the generated handler", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "/app", false, {
|
|
redirects: [{ source: "/old", destination: "/new", permanent: true }],
|
|
});
|
|
expect(code).toContain('const __basePath = "/app"');
|
|
expect(code).toContain("basePath: __basePath");
|
|
expect(code).toContain("configRedirects: __configRedirects");
|
|
});
|
|
|
|
it("passes server action handlers and afterFiles rewrites to the typed handler", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
rewrites: {
|
|
beforeFiles: [],
|
|
afterFiles: [{ source: "/x", destination: "/y" }],
|
|
fallback: [],
|
|
},
|
|
});
|
|
expect(code).toContain("handleServerActionRequest({");
|
|
expect(code).toContain("loadServerAction");
|
|
expect(code).toContain('"afterFiles":[{"source":"/x","destination":"/y"}]');
|
|
expect(code).toContain("configRewrites: __configRewrites");
|
|
});
|
|
|
|
it("embeds allowedOrigins when provided", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
allowedOrigins: ["my-proxy.com", "*.my-domain.com"],
|
|
});
|
|
expect(code).toContain("__allowedOrigins");
|
|
expect(code).toContain("my-proxy.com");
|
|
expect(code).toContain("*.my-domain.com");
|
|
});
|
|
|
|
it("keeps allowedDevOrigins separate from allowedOrigins", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
allowedOrigins: ["actions.example.com"],
|
|
allowedDevOrigins: ["allowed.example.com"],
|
|
});
|
|
expect(code).toContain("actions.example.com");
|
|
expect(code).toContain("allowed.example.com");
|
|
});
|
|
|
|
it("origin validation does not use x-forwarded-host", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false);
|
|
const actionStart = code.indexOf("handleServerActionRequest({");
|
|
const actionEnd = code.indexOf("i18nConfig: __i18nConfig", actionStart);
|
|
const actionOptions = code.slice(actionStart, actionEnd);
|
|
|
|
// CSRF behavior belongs to the shared action helper. The generated entry
|
|
// should only pass the original Request and configured origins through.
|
|
expect(actionOptions).toContain("request,");
|
|
expect(actionOptions).toContain("allowedOrigins: __allowedOrigins");
|
|
expect(actionOptions).not.toContain("x-forwarded-host");
|
|
expect(code).not.toContain("validateCsrfOrigin(request, __allowedOrigins)");
|
|
expect(code).not.toContain("function __validateCsrfOrigin");
|
|
});
|
|
|
|
// ── Dev origin check code generation ────────────────────────────────
|
|
it("generates dev origin validation code in RSC entry", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false);
|
|
// Should include the dev origin validation function definition
|
|
expect(code).toContain("__validateDevRequestOrigin");
|
|
expect(code).toContain("__safeDevHosts");
|
|
expect(code).toContain("validateDevRequestOrigin: __validateDevRequestOrigin");
|
|
});
|
|
|
|
it("embeds allowedDevOrigins in dev origin check code", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false, {
|
|
allowedDevOrigins: ["staging.example.com", "*.preview.dev"],
|
|
});
|
|
expect(code).toContain("staging.example.com");
|
|
expect(code).toContain("*.preview.dev");
|
|
expect(code).toContain("__allowedDevOrigins");
|
|
});
|
|
|
|
it("loads allowedDevOrigins from next.config into the virtual RSC entry", async () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-app-rsc-allowed-dev-origins-"));
|
|
try {
|
|
fs.mkdirSync(path.join(tmpDir, "app"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "app", "layout.tsx"),
|
|
"export default function Layout({ children }) { return <html><body>{children}</body></html>; }",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "app", "page.tsx"),
|
|
"export default function Page() { return <div>allowed-dev-origins</div>; }",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, "next.config.mjs"),
|
|
`export default {
|
|
allowedDevOrigins: ["allowed.example.com"],
|
|
experimental: {
|
|
serverActions: {
|
|
allowedOrigins: ["actions.example.com"],
|
|
},
|
|
},
|
|
};`,
|
|
);
|
|
fs.symlinkSync(
|
|
path.resolve(__dirname, "..", "node_modules"),
|
|
path.join(tmpDir, "node_modules"),
|
|
"junction",
|
|
);
|
|
|
|
const testServer = await createServer({
|
|
root: tmpDir,
|
|
configFile: false,
|
|
plugins: [vinext({ appDir: tmpDir })],
|
|
server: { port: 0 },
|
|
logLevel: "silent",
|
|
});
|
|
|
|
try {
|
|
const resolved = await testServer.pluginContainer.resolveId("virtual:vinext-rsc-entry");
|
|
expect(resolved).toBeTruthy();
|
|
const loaded = await testServer.pluginContainer.load(resolved!.id);
|
|
const code = typeof loaded === "string" ? loaded : ((loaded as any)?.code ?? "");
|
|
|
|
expect(code).toContain('const __allowedDevOrigins = ["allowed.example.com"]');
|
|
expect(code).toContain('const __allowedOrigins = ["actions.example.com"]');
|
|
} finally {
|
|
await testServer.close();
|
|
}
|
|
} finally {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("RSC error runtime delegation", () => {
|
|
it("imports RSC error helpers from a normal server module", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false);
|
|
|
|
expect(code).toContain("sanitizeErrorForClient as __sanitizeErrorForClient");
|
|
expect(code).toContain("server/app-rsc-errors.js");
|
|
expect(code).toContain("createAppRscOnErrorHandler");
|
|
expect(code).toContain("server/app-rsc-error-handler.js");
|
|
});
|
|
|
|
it("keeps request-specific onError wiring in the generated entry", () => {
|
|
const code = generateRscEntry("/tmp/test/app", minimalRoutes, null, [], null, "", false);
|
|
|
|
expect(code).toContain("createRscOnErrorHandler(pathname, routePath)");
|
|
expect(code).toContain(
|
|
"createAppRscOnErrorHandler(_reportRequestError, request, pathname, routePath)",
|
|
);
|
|
expect(code).not.toContain("function createRscOnErrorHandler(request, pathname, routePath)");
|
|
expect(code).not.toContain("return __createRscOnErrorHandler({");
|
|
});
|
|
});
|
|
});
|