From 315dabde905492a0ad4c182a81085669ee236d6f Mon Sep 17 00:00:00 2001 From: James Date: Mon, 16 Mar 2026 20:41:27 +0000 Subject: [PATCH 1/2] refactor: migrate ssrLoadModule to moduleRunner.import Replace all server.ssrLoadModule() calls in dev-server.ts and api-handler.ts with runner.import() via the ModuleImporter interface, following the Vite Module Runner migration guide. Remove ssrFixStacktrace calls which are not needed with Module Runner APIs. Update call sites in index.ts to pass the existing getPagesRunner() lazy factory as the runner argument. Update tests to use ModuleImporter-shaped mocks instead of the old ViteDevServer mock. --- packages/vinext/src/index.ts | 9 ++- packages/vinext/src/server/api-handler.ts | 13 ++-- packages/vinext/src/server/dev-server.ts | 79 +++++++++++++-------- tests/api-handler.test.ts | 25 +++---- tests/pages-router.test.ts | 85 ++++++++++++----------- 5 files changed, 119 insertions(+), 92 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 3df85cf0b1..f3a90a1c59 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -2967,7 +2967,13 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { applyRequestHeadersToNodeRequest(middlewareRequestHeaders); } } - const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes); + const handled = await handleApiRoute( + getPagesRunner(), + req, + res, + resolvedUrl, + apiRoutes, + ); if (handled) return; // No API route matched — if app dir exists, let the RSC plugin handle it @@ -3003,6 +3009,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const handler = createSSRHandler( server, + getPagesRunner(), routes, pagesDir, nextConfig?.i18n, diff --git a/packages/vinext/src/server/api-handler.ts b/packages/vinext/src/server/api-handler.ts index 226e5e7653..cc53bfe565 100644 --- a/packages/vinext/src/server/api-handler.ts +++ b/packages/vinext/src/server/api-handler.ts @@ -7,11 +7,10 @@ * The req/res objects are Node.js IncomingMessage/ServerResponse with * Next.js extensions: req.query, req.body, res.json(), res.status(), etc. */ -import type { ViteDevServer } from "vite"; import type { IncomingMessage, ServerResponse } from "node:http"; import { decode as decodeQueryString } from "node:querystring"; import { type Route, matchRoute } from "../routing/pages-router.js"; -import { reportRequestError } from "./instrumentation.js"; +import { reportRequestError, type ModuleImporter } from "./instrumentation.js"; import { addQueryParam } from "../utils/query.js"; /** @@ -192,7 +191,7 @@ function enhanceApiObjects( * Returns true if the request was handled, false if no API route matched. */ export async function handleApiRoute( - server: ViteDevServer, + runner: ModuleImporter, req: IncomingMessage, res: ServerResponse, url: string, @@ -204,8 +203,9 @@ export async function handleApiRoute( const { route, params } = match; try { - // Load the API route module through Vite - const apiModule = await server.ssrLoadModule(route.filePath); + // Load the API route module through the ModuleRunner + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const apiModule = (await runner.import(route.filePath)) as Record; const handler = apiModule.default; if (typeof handler !== "function") { @@ -242,7 +242,8 @@ export async function handleApiRoute( return true; } - server.ssrFixStacktrace(e as Error); + // Stack traces are automatically rewritten by the ModuleRunner, + // so server.ssrFixStacktrace() does not need to be called here. console.error(e); void reportRequestError( e instanceof Error ? e : new Error(String(e)), diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index 2228ce1b35..01f0ba8745 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -2,6 +2,7 @@ import type { ViteDevServer } from "vite"; import type { IncomingMessage, ServerResponse } from "node:http"; import type { Route } from "../routing/pages-router.js"; import { matchRoute, patternToNextFormat } from "../routing/pages-router.js"; +import type { ModuleImporter } from "./instrumentation.js"; import type { NextI18nConfig } from "../config/next-config.js"; import { isrGet, @@ -235,13 +236,14 @@ export function parseCookieLocale(req: IncomingMessage, i18nConfig: NextI18nConf * * For each request: * 1. Match the URL against discovered routes - * 2. Load the page module via Vite's SSR module loader + * 2. Load the page module via the ModuleRunner * 3. Call getServerSideProps/getStaticProps if present * 4. Render the component to HTML * 5. Wrap in _document shell and send response */ export function createSSRHandler( server: ViteDevServer, + runner: ModuleImporter, routes: Route[], pagesDir: string, i18nConfig?: NextI18nConfig | null, @@ -253,10 +255,10 @@ export function createSSRHandler( // Register ALS-backed accessors in the SSR module graph so head and // router state are per-request isolated under concurrent load. - // This is a one-time side-effect; ssrLoadModule caches internally. + // runner.import() caches internally. const _alsRegistration = Promise.all([ - server.ssrLoadModule("vinext/head-state"), - server.ssrLoadModule("vinext/router-state"), + runner.import("vinext/head-state"), + runner.import("vinext/router-state"), ]); // Suppress unhandled-rejection if the server closes before the first // request (common in tests). Errors still propagate when the first @@ -323,7 +325,7 @@ export function createSSRHandler( if (!match) { // No route matched — try to render custom 404 page - await renderErrorPage(server, req, res, url, pagesDir, 404, undefined, matcher); + await renderErrorPage(server, runner, req, res, url, pagesDir, 404, undefined, matcher); return; } @@ -338,7 +340,7 @@ export function createSSRHandler( // Set SSR context for the router shim so useRouter() returns // the correct URL and params during server-side rendering. - const routerShim = await server.ssrLoadModule("next/router"); + const routerShim = (await runner.import("next/router")) as Record; if (typeof routerShim.setSSRContext === "function") { routerShim.setSSRContext({ pathname: patternToNextFormat(route.pattern), @@ -352,14 +354,14 @@ export function createSSRHandler( } // Set per-request i18n context for Link component locale - // prop support during SSR. Use ssrLoadModule to set it on + // prop support during SSR. Use runner.import to set it on // the SSR environment's module instance (same pattern as // setSSRContext above). if (i18nConfig) { // Register ALS-backed i18n accessors in the SSR module graph so // next/link and other SSR imports read from the unified store. - await server.ssrLoadModule("vinext/i18n-state"); - const i18nCtx = await server.ssrLoadModule("vinext/i18n-context"); + await runner.import("vinext/i18n-state"); + const i18nCtx = (await runner.import("vinext/i18n-context")) as Record; if (typeof i18nCtx.setI18nContext === "function") { i18nCtx.setI18nContext({ locale: locale ?? currentDefaultLocale, @@ -373,7 +375,8 @@ export function createSSRHandler( // Load the page module through Vite's SSR pipeline // This gives us HMR and transform support for free - const pageModule = await server.ssrLoadModule(route.filePath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pageModule = (await runner.import(route.filePath)) as Record; // Mark end of compile phase: everything from here is rendering. _compileEnd = now(); @@ -416,6 +419,7 @@ export function createSSRHandler( if (!isValidPath) { await renderErrorPage( server, + runner, req, res, url, @@ -486,6 +490,7 @@ export function createSSRHandler( if (result && "notFound" in result && result.notFound) { await renderErrorPage( server, + runner, req, res, url, @@ -520,11 +525,13 @@ export function createSSRHandler( let earlyFontLinkHeader = ""; try { const earlyPreloads: Array<{ href: string; type: string }> = []; - const fontGoogleEarly = await server.ssrLoadModule("next/font/google"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fontGoogleEarly = (await runner.import("next/font/google")) as Record; if (typeof fontGoogleEarly.getSSRFontPreloads === "function") { earlyPreloads.push(...fontGoogleEarly.getSSRFontPreloads()); } - const fontLocalEarly = await server.ssrLoadModule("next/font/local"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fontLocalEarly = (await runner.import("next/font/local")) as Record; if (typeof fontLocalEarly.getSSRFontPreloads === "function") { earlyPreloads.push(...fontLocalEarly.getSSRFontPreloads()); } @@ -606,8 +613,12 @@ export function createSSRHandler( }); } if (i18nConfig) { - await server.ssrLoadModule("vinext/i18n-state"); - const i18nCtx = await server.ssrLoadModule("vinext/i18n-context"); + await runner.import("vinext/i18n-state"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const i18nCtx = (await runner.import("vinext/i18n-context")) as Record< + string, + any + >; if (typeof i18nCtx.setI18nContext === "function") { i18nCtx.setI18nContext({ locale: locale ?? currentDefaultLocale, @@ -625,7 +636,7 @@ export function createSSRHandler( const appPath = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPath, matcher)) { try { - const appMod = await server.ssrLoadModule(appPath); + const appMod = (await runner.import(appPath)) as Record; RegenApp = appMod.default ?? null; } catch { // _app failed to load @@ -726,6 +737,7 @@ export function createSSRHandler( if (result && "notFound" in result && result.notFound) { await renderErrorPage( server, + runner, req, res, url, @@ -748,7 +760,7 @@ export function createSSRHandler( const appPath = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPath, matcher)) { try { - const appModule = await server.ssrLoadModule(appPath); + const appModule = (await runner.import(appPath)) as Record; AppComponent = appModule.default ?? null; } catch { // _app exists but failed to load @@ -779,13 +791,15 @@ export function createSSRHandler( } // Reset SSR head collector before rendering so tags are captured - const headShim = await server.ssrLoadModule("next/head"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const headShim = (await runner.import("next/head")) as Record; if (typeof headShim.resetSSRHead === "function") { headShim.resetSSRHead(); } // Flush any pending dynamic() preloads so components are ready - const dynamicShim = await server.ssrLoadModule("next/dynamic"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const dynamicShim = (await runner.import("next/dynamic")) as Record; if (typeof dynamicShim.flushPreloads === "function") { await dynamicShim.flushPreloads(); } @@ -799,7 +813,8 @@ export function createSSRHandler( const allFontStyles: string[] = []; const allFontPreloads: Array<{ href: string; type: string }> = []; try { - const fontGoogle = await server.ssrLoadModule("next/font/google"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fontGoogle = (await runner.import("next/font/google")) as Record; if (typeof fontGoogle.getSSRFontLinks === "function") { const fontUrls = fontGoogle.getSSRFontLinks(); for (const fontUrl of fontUrls) { @@ -818,7 +833,8 @@ export function createSSRHandler( // next/font/google not used — skip } try { - const fontLocal = await server.ssrLoadModule("next/font/local"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fontLocal = (await runner.import("next/font/local")) as Record; if (typeof fontLocal.getSSRFontStyles === "function") { allFontStyles.push(...fontLocal.getSSRFontStyles()); } @@ -904,7 +920,7 @@ hydrate(); let DocumentComponent: any = null; if (findFileWithExtensions(docPath, matcher)) { try { - const docModule = await server.ssrLoadModule(docPath); + const docModule = (await runner.import(docPath)) as Record; DocumentComponent = docModule.default ?? null; } catch { // _document exists but failed to load @@ -982,8 +998,8 @@ hydrate(); setRevalidateDuration(cacheKey, isrRevalidateSeconds); } } catch (e) { - // Let Vite fix the stack trace for better dev experience - server.ssrFixStacktrace?.(e as Error); + // Stack traces are automatically rewritten by the ModuleRunner, + // so server.ssrFixStacktrace() does not need to be called here. console.error(e); // Report error via instrumentation hook if registered reportRequestError( @@ -1008,7 +1024,7 @@ hydrate(); }); // Try to render custom 500 error page try { - await renderErrorPage(server, req, res, url, pagesDir, 500, undefined, matcher); + await renderErrorPage(server, runner, req, res, url, pagesDir, 500, undefined, matcher); } catch (fallbackErr) { // If error page itself fails, fall back to plain text. // This is a dev-only code path (prod uses prod-server.ts), so @@ -1033,6 +1049,7 @@ hydrate(); */ async function renderErrorPage( server: ViteDevServer, + runner: ModuleImporter, _req: IncomingMessage, res: ServerResponse, url: string, @@ -1051,7 +1068,8 @@ async function renderErrorPage( const candidatePath = path.join(pagesDir, candidate); if (!findFileWithExtensions(candidatePath, matcher)) continue; - const errorModule = await server.ssrLoadModule(candidatePath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errorModule = (await runner.import(candidatePath)) as Record; const ErrorComponent = errorModule.default; if (!ErrorComponent) continue; @@ -1060,7 +1078,8 @@ async function renderErrorPage( const appPathErr = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPathErr, matcher)) { try { - const appModule = await server.ssrLoadModule(appPathErr); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const appModule = (await runner.import(appPathErr)) as Record; AppComponent = appModule.default ?? null; } catch { // _app exists but failed to load @@ -1071,11 +1090,12 @@ async function renderErrorPage( const errorProps = { statusCode }; // If the caller didn't supply wrapWithRouterContext, load it now. - // ssrLoadModule caches internally so the cost is negligible. + // runner.import() caches internally so the cost is negligible. let wrapFn = wrapWithRouterContext; if (!wrapFn) { try { - const errRouterShim = await server.ssrLoadModule("next/router"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errRouterShim = (await runner.import("next/router")) as Record; wrapFn = errRouterShim.wrapWithRouterContext; } catch { // router shim not available — continue without it @@ -1104,7 +1124,8 @@ async function renderErrorPage( const docPathErr = path.join(pagesDir, "_document"); if (findFileWithExtensions(docPathErr, matcher)) { try { - const docModule = await server.ssrLoadModule(docPathErr); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const docModule = (await runner.import(docPathErr)) as Record; DocumentComponent = docModule.default ?? null; } catch { // _document exists but failed to load diff --git a/tests/api-handler.test.ts b/tests/api-handler.test.ts index af84eddafc..05b17c31be 100644 --- a/tests/api-handler.test.ts +++ b/tests/api-handler.test.ts @@ -17,14 +17,11 @@ vi.mock("../packages/vinext/src/server/instrumentation.js", () => ({ reportRequestError: vi.fn(() => Promise.resolve()), })); import { handleApiRoute } from "../packages/vinext/src/server/api-handler.js"; -import { reportRequestError } from "../packages/vinext/src/server/instrumentation.js"; +import { + reportRequestError, + type ModuleImporter, +} from "../packages/vinext/src/server/instrumentation.js"; import type { Route } from "../packages/vinext/src/routing/pages-router.js"; -import type { ViteDevServer } from "vite-plus"; - -type MockServer = ViteDevServer & { - ssrLoadModule: ReturnType; - ssrFixStacktrace: ReturnType; -}; beforeEach(() => { vi.clearAllMocks(); @@ -132,13 +129,12 @@ function route(pattern: string, filePath = "/fake/api/handler.ts"): Route { } /** - * Build a minimal mock ViteDevServer with configurable ssrLoadModule behavior. + * Build a minimal mock ModuleImporter with configurable import behavior. */ -function mockServer(moduleExport: Record): MockServer { +function mockServer(moduleExport: Record): ModuleImporter { return { - ssrLoadModule: vi.fn().mockResolvedValue(moduleExport), - ssrFixStacktrace: vi.fn(), - } as unknown as MockServer; + import: vi.fn().mockResolvedValue(moduleExport), + }; } // ── Tests ──────────────────────────────────────────────────────────────── @@ -211,7 +207,6 @@ describe("handleApiRoute", () => { expect(res._statusCode).toBe(400); expect(res.statusMessage).toBe("Invalid JSON"); expect(res._body).toBe("Invalid JSON"); - expect(server.ssrFixStacktrace.mock.calls).toHaveLength(0); expect(errorSpy).not.toHaveBeenCalled(); expect(reportRequestError).not.toHaveBeenCalled(); errorSpy.mockRestore(); @@ -811,7 +806,7 @@ describe("handleApiRoute", () => { expect(res._body).toBe("Internal Server Error"); }); - it("calls ssrFixStacktrace on handler errors", async () => { + it("still returns 500 on handler errors (no ssrFixStacktrace needed with Module Runner)", async () => { const error = new Error("test error"); const handler = vi.fn(() => { throw error; @@ -822,7 +817,7 @@ describe("handleApiRoute", () => { await handleApiRoute(server, req, res, "/api/users", [route("/api/users")]); - expect(server.ssrFixStacktrace.mock.calls).toContainEqual([error]); + expect(res._statusCode).toBe(500); }); }); }); diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index 02f6319065..19081893c8 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -2911,53 +2911,56 @@ describe("Pages Router dev ISR regeneration", () => { }; const routeFile = path.join(FIXTURE_DIR, "pages", "isr-test.tsx"); - const server = { - transformIndexHtml: vi.fn(async (_url: string, html: string) => html), - ssrLoadModule: vi.fn(async (id: string) => { - // ALS registration side-effects loaded at createSSRHandler startup - if (id === "vinext/head-state" || id === "vinext/router-state") { - return {}; - } - - if (id === "next/router") { - return { - setSSRContext() { - getRequestContext().currentRequestTags.push("outer-tag"); - parentRequestTags = [...getRequestContext().currentRequestTags]; - }, - wrapWithRouterContext(element: unknown) { - return element; - }, - }; - } + const loadModule = async (id: string) => { + // ALS registration side-effects loaded at createSSRHandler startup + if (id === "vinext/head-state" || id === "vinext/router-state") { + return {}; + } + + if (id === "next/router") { + return { + setSSRContext() { + getRequestContext().currentRequestTags.push("outer-tag"); + parentRequestTags = [...getRequestContext().currentRequestTags]; + }, + wrapWithRouterContext(element: unknown) { + return element; + }, + }; + } - if (id === routeFile) { - return { - default() { - return null; - }, - async getStaticProps() { - regenSawUnifiedScope = isInsideUnifiedScope(); - regenTags = [...getRequestContext().currentRequestTags]; - regenExecutionContext = getRequestExecutionContext(); - regenUnifiedExecutionContext = getRequestContext().executionContext; - return { - props: { - timestamp: Date.now(), - message: "fresh", - }, - revalidate: 1, - }; - }, - }; - } + if (id === routeFile) { + return { + default() { + return null; + }, + async getStaticProps() { + regenSawUnifiedScope = isInsideUnifiedScope(); + regenTags = [...getRequestContext().currentRequestTags]; + regenExecutionContext = getRequestExecutionContext(); + regenUnifiedExecutionContext = getRequestContext().executionContext; + return { + props: { + timestamp: Date.now(), + message: "fresh", + }, + revalidate: 1, + }; + }, + }; + } - throw new Error(`Unexpected module load: ${id}`); - }), + throw new Error(`Unexpected module load: ${id}`); + }; + const server = { + transformIndexHtml: vi.fn(async (_url: string, html: string) => html), + ssrLoadModule: vi.fn(loadModule), } as unknown as ViteDevServer; + const runner = { import: loadModule }; const handler = createSSRHandler( server, + runner, [ { pattern: "/isr-test", From 0ad4a4fe5f51691e5024c1256c154d88f62e89dc Mon Sep 17 00:00:00 2001 From: James Date: Tue, 17 Mar 2026 13:42:32 +0000 Subject: [PATCH 2/2] refactor: address bonk review comments - Fix misleading stack trace comments: ssrFixStacktrace is not applicable to ModuleRunner (not that it rewrites automatically) - Update ALS-ARCHITECTURE.md to reference runner.import() instead of server.ssrLoadModule() - Add importModule() typed helper to instrumentation.ts to centralise the Record cast, removing 12 inline eslint-disable comments - Remove dead ssrLoadModule mock from pages-router ISR test (createSSRHandler no longer calls server.ssrLoadModule) --- packages/vinext/src/server/api-handler.ts | 9 ++-- packages/vinext/src/server/dev-server.ts | 51 +++++++------------ packages/vinext/src/server/instrumentation.ts | 15 ++++++ packages/vinext/src/shims/ALS-ARCHITECTURE.md | 8 +-- tests/api-handler.test.ts | 3 ++ tests/pages-router.test.ts | 1 - 6 files changed, 44 insertions(+), 43 deletions(-) diff --git a/packages/vinext/src/server/api-handler.ts b/packages/vinext/src/server/api-handler.ts index cc53bfe565..64d1120761 100644 --- a/packages/vinext/src/server/api-handler.ts +++ b/packages/vinext/src/server/api-handler.ts @@ -10,7 +10,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { decode as decodeQueryString } from "node:querystring"; import { type Route, matchRoute } from "../routing/pages-router.js"; -import { reportRequestError, type ModuleImporter } from "./instrumentation.js"; +import { reportRequestError, importModule, type ModuleImporter } from "./instrumentation.js"; import { addQueryParam } from "../utils/query.js"; /** @@ -204,8 +204,7 @@ export async function handleApiRoute( try { // Load the API route module through the ModuleRunner - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const apiModule = (await runner.import(route.filePath)) as Record; + const apiModule = await importModule(runner, route.filePath); const handler = apiModule.default; if (typeof handler !== "function") { @@ -242,8 +241,8 @@ export async function handleApiRoute( return true; } - // Stack traces are automatically rewritten by the ModuleRunner, - // so server.ssrFixStacktrace() does not need to be called here. + // ssrFixStacktrace() is specific to ssrLoadModule and is not applicable + // when using ModuleRunner — no stack trace fixup is needed here. console.error(e); void reportRequestError( e instanceof Error ? e : new Error(String(e)), diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index 01f0ba8745..4e494ea1e0 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -3,6 +3,7 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Route } from "../routing/pages-router.js"; import { matchRoute, patternToNextFormat } from "../routing/pages-router.js"; import type { ModuleImporter } from "./instrumentation.js"; +import { importModule, reportRequestError } from "./instrumentation.js"; import type { NextI18nConfig } from "../config/next-config.js"; import { isrGet, @@ -23,7 +24,6 @@ import { createRequestContext, runWithRequestContext } from "../shims/unified-re import "../shims/router-state.js"; import { runWithHeadState } from "../shims/head-state.js"; import { runWithServerInsertedHTMLState } from "../shims/navigation-state.js"; -import { reportRequestError } from "./instrumentation.js"; import { safeJsonStringify } from "./html.js"; import { parseQueryString as parseQuery } from "../utils/query.js"; import path from "node:path"; @@ -340,7 +340,7 @@ export function createSSRHandler( // Set SSR context for the router shim so useRouter() returns // the correct URL and params during server-side rendering. - const routerShim = (await runner.import("next/router")) as Record; + const routerShim = await importModule(runner, "next/router"); if (typeof routerShim.setSSRContext === "function") { routerShim.setSSRContext({ pathname: patternToNextFormat(route.pattern), @@ -361,7 +361,7 @@ export function createSSRHandler( // Register ALS-backed i18n accessors in the SSR module graph so // next/link and other SSR imports read from the unified store. await runner.import("vinext/i18n-state"); - const i18nCtx = (await runner.import("vinext/i18n-context")) as Record; + const i18nCtx = await importModule(runner, "vinext/i18n-context"); if (typeof i18nCtx.setI18nContext === "function") { i18nCtx.setI18nContext({ locale: locale ?? currentDefaultLocale, @@ -375,8 +375,7 @@ export function createSSRHandler( // Load the page module through Vite's SSR pipeline // This gives us HMR and transform support for free - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pageModule = (await runner.import(route.filePath)) as Record; + const pageModule = await importModule(runner, route.filePath); // Mark end of compile phase: everything from here is rendering. _compileEnd = now(); @@ -525,13 +524,11 @@ export function createSSRHandler( let earlyFontLinkHeader = ""; try { const earlyPreloads: Array<{ href: string; type: string }> = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fontGoogleEarly = (await runner.import("next/font/google")) as Record; + const fontGoogleEarly = await importModule(runner, "next/font/google"); if (typeof fontGoogleEarly.getSSRFontPreloads === "function") { earlyPreloads.push(...fontGoogleEarly.getSSRFontPreloads()); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fontLocalEarly = (await runner.import("next/font/local")) as Record; + const fontLocalEarly = await importModule(runner, "next/font/local"); if (typeof fontLocalEarly.getSSRFontPreloads === "function") { earlyPreloads.push(...fontLocalEarly.getSSRFontPreloads()); } @@ -614,11 +611,7 @@ export function createSSRHandler( } if (i18nConfig) { await runner.import("vinext/i18n-state"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const i18nCtx = (await runner.import("vinext/i18n-context")) as Record< - string, - any - >; + const i18nCtx = await importModule(runner, "vinext/i18n-context"); if (typeof i18nCtx.setI18nContext === "function") { i18nCtx.setI18nContext({ locale: locale ?? currentDefaultLocale, @@ -760,7 +753,7 @@ export function createSSRHandler( const appPath = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPath, matcher)) { try { - const appModule = (await runner.import(appPath)) as Record; + const appModule = await importModule(runner, appPath); AppComponent = appModule.default ?? null; } catch { // _app exists but failed to load @@ -791,15 +784,13 @@ export function createSSRHandler( } // Reset SSR head collector before rendering so tags are captured - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const headShim = (await runner.import("next/head")) as Record; + const headShim = await importModule(runner, "next/head"); if (typeof headShim.resetSSRHead === "function") { headShim.resetSSRHead(); } // Flush any pending dynamic() preloads so components are ready - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const dynamicShim = (await runner.import("next/dynamic")) as Record; + const dynamicShim = await importModule(runner, "next/dynamic"); if (typeof dynamicShim.flushPreloads === "function") { await dynamicShim.flushPreloads(); } @@ -813,8 +804,7 @@ export function createSSRHandler( const allFontStyles: string[] = []; const allFontPreloads: Array<{ href: string; type: string }> = []; try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fontGoogle = (await runner.import("next/font/google")) as Record; + const fontGoogle = await importModule(runner, "next/font/google"); if (typeof fontGoogle.getSSRFontLinks === "function") { const fontUrls = fontGoogle.getSSRFontLinks(); for (const fontUrl of fontUrls) { @@ -833,8 +823,7 @@ export function createSSRHandler( // next/font/google not used — skip } try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const fontLocal = (await runner.import("next/font/local")) as Record; + const fontLocal = await importModule(runner, "next/font/local"); if (typeof fontLocal.getSSRFontStyles === "function") { allFontStyles.push(...fontLocal.getSSRFontStyles()); } @@ -998,8 +987,8 @@ hydrate(); setRevalidateDuration(cacheKey, isrRevalidateSeconds); } } catch (e) { - // Stack traces are automatically rewritten by the ModuleRunner, - // so server.ssrFixStacktrace() does not need to be called here. + // ssrFixStacktrace() is specific to ssrLoadModule and is not applicable + // when using ModuleRunner — no stack trace fixup is needed here. console.error(e); // Report error via instrumentation hook if registered reportRequestError( @@ -1068,8 +1057,7 @@ async function renderErrorPage( const candidatePath = path.join(pagesDir, candidate); if (!findFileWithExtensions(candidatePath, matcher)) continue; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorModule = (await runner.import(candidatePath)) as Record; + const errorModule = await importModule(runner, candidatePath); const ErrorComponent = errorModule.default; if (!ErrorComponent) continue; @@ -1078,8 +1066,7 @@ async function renderErrorPage( const appPathErr = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPathErr, matcher)) { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const appModule = (await runner.import(appPathErr)) as Record; + const appModule = await importModule(runner, appPathErr); AppComponent = appModule.default ?? null; } catch { // _app exists but failed to load @@ -1094,8 +1081,7 @@ async function renderErrorPage( let wrapFn = wrapWithRouterContext; if (!wrapFn) { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errRouterShim = (await runner.import("next/router")) as Record; + const errRouterShim = await importModule(runner, "next/router"); wrapFn = errRouterShim.wrapWithRouterContext; } catch { // router shim not available — continue without it @@ -1124,8 +1110,7 @@ async function renderErrorPage( const docPathErr = path.join(pagesDir, "_document"); if (findFileWithExtensions(docPathErr, matcher)) { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const docModule = (await runner.import(docPathErr)) as Record; + const docModule = await importModule(runner, docPathErr); DocumentComponent = docModule.default ?? null; } catch { // _document exists but failed to load diff --git a/packages/vinext/src/server/instrumentation.ts b/packages/vinext/src/server/instrumentation.ts index 0f1d84c332..0e084f0382 100644 --- a/packages/vinext/src/server/instrumentation.ts +++ b/packages/vinext/src/server/instrumentation.ts @@ -48,6 +48,21 @@ export interface ModuleImporter { import(id: string): Promise; } +/** + * Import a module via the runner and cast the result to `Record`. + * + * Centralises the `as Record` cast so callers don't need + * per-call eslint-disable comments. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function importModule( + runner: ModuleImporter, + id: string, +): Promise> { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (await runner.import(id)) as Record; +} + /** Possible instrumentation file names. */ const INSTRUMENTATION_FILES = [ "instrumentation.ts", diff --git a/packages/vinext/src/shims/ALS-ARCHITECTURE.md b/packages/vinext/src/shims/ALS-ARCHITECTURE.md index fb0dd82f63..f81d5d956f 100644 --- a/packages/vinext/src/shims/ALS-ARCHITECTURE.md +++ b/packages/vinext/src/shims/ALS-ARCHITECTURE.md @@ -78,8 +78,8 @@ each shim module (e.g. `head-state.ts`, `router-state.ts`) follows this pattern: in dev, vite has separate module graphs for different environments (node vs ssr). the state module must be loaded in each environment that uses it. the dev server -calls `server.ssrLoadModule("vinext/head-state")` to make sure registration -happens in the ssr module graph. +calls `runner.import("vinext/head-state")` (via the `ModuleImporter` interface) +to make sure registration happens in the ssr module graph. in prod, bundling collapses everything into one module graph, so registration happens naturally through static imports. @@ -92,7 +92,7 @@ happens naturally through static imports. 4. in your shim, use `isInsideUnifiedScope()` to read from the unified store, falling back to standalone als when outside 5. if the state is accessed by react components during ssr in dev, load the - state module via `server.ssrLoadModule()` in `dev-server.ts` (node-side-only - state does not need this) + state module via `runner.import()` (using the `ModuleImporter` interface) + in `dev-server.ts` (node-side-only state does not need this) 6. if the state is per-call rather than per-request (like cache scopes), keep it in its own als - don't add it to the unified context diff --git a/tests/api-handler.test.ts b/tests/api-handler.test.ts index 05b17c31be..5f2f46e894 100644 --- a/tests/api-handler.test.ts +++ b/tests/api-handler.test.ts @@ -15,6 +15,9 @@ import { PassThrough } from "node:stream"; import http from "node:http"; vi.mock("../packages/vinext/src/server/instrumentation.js", () => ({ reportRequestError: vi.fn(() => Promise.resolve()), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + importModule: (runner: { import(id: string): Promise }, id: string) => + runner.import(id) as Promise>, })); import { handleApiRoute } from "../packages/vinext/src/server/api-handler.js"; import { diff --git a/tests/pages-router.test.ts b/tests/pages-router.test.ts index 19081893c8..d7e8f9fe7d 100644 --- a/tests/pages-router.test.ts +++ b/tests/pages-router.test.ts @@ -2954,7 +2954,6 @@ describe("Pages Router dev ISR regeneration", () => { }; const server = { transformIndexHtml: vi.fn(async (_url: string, html: string) => html), - ssrLoadModule: vi.fn(loadModule), } as unknown as ViteDevServer; const runner = { import: loadModule };