From e5bda432d9fffd5034c466cee1daafae37ef2f70 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 29 Mar 2026 20:30:06 +0100 Subject: [PATCH 1/3] chore: turn on more lint rules --- benchmarks/generate-app.mjs | 4 +- .../app/api/og/route.tsx | 577 +++++++----------- packages/vinext/src/cli.ts | 2 +- .../vinext/src/cloudflare/kv-cache-handler.ts | 2 +- packages/vinext/src/config/next-config.ts | 1 + packages/vinext/src/entries/app-rsc-entry.ts | 6 +- .../vinext/src/entries/pages-server-entry.ts | 4 +- packages/vinext/src/global.d.ts | 28 +- .../src/plugins/client-reference-dedup.ts | 2 +- .../fix-use-server-closure-collision.ts | 14 +- packages/vinext/src/plugins/fonts.ts | 4 +- .../src/server/app-page-boundary-render.ts | 2 + packages/vinext/src/server/app-ssr-stream.ts | 4 +- .../vinext/src/server/dev-module-runner.ts | 2 +- packages/vinext/src/server/dev-server.ts | 8 +- packages/vinext/src/server/instrumentation.ts | 7 +- packages/vinext/src/server/middleware.ts | 2 + packages/vinext/src/shims/dynamic.ts | 4 +- packages/vinext/src/shims/error-boundary.tsx | 2 +- packages/vinext/src/shims/head.ts | 6 +- packages/vinext/src/shims/link.tsx | 2 +- packages/vinext/src/shims/navigation-state.ts | 8 +- packages/vinext/src/shims/navigation.ts | 4 + tests/api-handler.test.ts | 12 +- tests/app-page-boundary.test.ts | 4 +- tests/app-page-probe.test.ts | 12 +- tests/app-page-render.test.ts | 12 +- tests/app-router.test.ts | 2 +- tests/build-optimization.test.ts | 3 +- tests/e2e/fixtures.ts | 1 + tests/fetch-cache.test.ts | 34 +- tests/font-google.test.ts | 5 +- tests/link.test.ts | 14 +- tests/optimize-imports.test.ts | 17 +- tests/safe-json.test.ts | 12 +- tests/shims.test.ts | 35 +- vite.config.ts | 32 + 37 files changed, 386 insertions(+), 504 deletions(-) diff --git a/benchmarks/generate-app.mjs b/benchmarks/generate-app.mjs index 5dac09bb3..7fcddc42e 100644 --- a/benchmarks/generate-app.mjs +++ b/benchmarks/generate-app.mjs @@ -3,7 +3,7 @@ * Generate the shared 33-route benchmark app. * Shared between the Next.js and vinext benchmark projects. */ -import { mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { mkdirSync, writeFileSync, rmSync, readdirSync, statSync, cpSync } from "node:fs"; import { join, dirname } from "node:path"; // Seeded PRNG (mulberry32) for deterministic source generation across runs. @@ -557,7 +557,6 @@ export default function ${title}Page() { } // Count results -import { readdirSync, statSync } from "node:fs"; function countFiles(dir, name) { let count = 0; for (const entry of readdirSync(dir)) { @@ -575,7 +574,6 @@ console.log( ); // Copy to each benchmark project (symlinks don't work with Turbopack) -import { cpSync } from "node:fs"; const BASE = dirname(new URL(import.meta.url).pathname); for (const project of ["nextjs", "vinext"]) { const dest = join(BASE, project, "app"); diff --git a/examples/app-router-playground/app/api/og/route.tsx b/examples/app-router-playground/app/api/og/route.tsx index 2561a6263..1922d1ce0 100644 --- a/examples/app-router-playground/app/api/og/route.tsx +++ b/examples/app-router-playground/app/api/og/route.tsx @@ -1,55 +1,51 @@ -import type { NextRequest } from 'next/server'; -import type { ReactElement } from 'react'; +import type { NextRequest } from "next/server"; +import type { ReactElement } from "react"; export async function GET(req: NextRequest): Promise { try { // Dynamic import to avoid @vercel/og's eager font loading at module init // which breaks in Cloudflare Workers due to import.meta.url issues - const { ImageResponse } = await import('next/og'); - + const { ImageResponse } = await import("next/og"); + const { searchParams } = new URL(req.url); - const isLight = req.headers.get('Sec-CH-Prefers-Color-Scheme') === 'light'; + const isLight = req.headers.get("Sec-CH-Prefers-Color-Scheme") === "light"; - const title = searchParams.has('title') - ? searchParams.get('title') - : 'App Router Playground'; + const title = searchParams.has("title") ? searchParams.get("title") : "App Router Playground"; // Note: Custom fonts require fetching from a URL in Workers environment // since fs/path APIs are not available. Using default font for now. return new ImageResponse( - ( +
+ {isLight ? : }
- {isLight ? : } -
- {title} -
+ {title}
- ), +
, { width: 843, height: 441, @@ -60,313 +56,206 @@ export async function GET(req: NextRequest): Promise { } catch (e) { if (!(e instanceof Error)) throw e; - // eslint-disable-next-line no-console + // oxlint-disable-next-line no-console console.log(e.message); return new Response(`Failed to generate the image`, { status: 500 }); } } const LIGHT_SVG: ReactElement = ( - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); function LightSvg(): ReactElement { @@ -523,15 +412,7 @@ function DarkSvg(): ReactElement { fill="#0141FF" /> - + - - + + s.replace(/\x1b\[[0-9;]*m/g, ""); // eslint-disable-line no-control-regex + const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ""); // oxlint-disable-line no-control-regex logger.info = (msg: string, options?: import("vite").LogOptions) => { const plain = strip(msg); diff --git a/packages/vinext/src/cloudflare/kv-cache-handler.ts b/packages/vinext/src/cloudflare/kv-cache-handler.ts index f5cd31210..bc2c54ffe 100644 --- a/packages/vinext/src/cloudflare/kv-cache-handler.ts +++ b/packages/vinext/src/cloudflare/kv-cache-handler.ts @@ -113,7 +113,7 @@ function validateTag(tag: string): string | null { // Block control characters and reserved separators used in our own key format. // Slash is allowed because revalidatePath() relies on pathname tags like // "/posts/hello" and "_N_T_/posts/hello". - // eslint-disable-next-line no-control-regex -- intentional: reject control chars in tags + // oxlint-disable-next-line no-control-regex -- intentional: reject control chars in tags if (/[\x00-\x1f\\:]/.test(tag)) return null; return tag; } diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index 418644439..f0e2eba7f 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -671,6 +671,7 @@ async function probeWebpackConfig( }; try { + // oxlint-disable-next-line typescript/no-unsafe-function-type const result = await (config.webpack as Function)(mockConfig, mockOptions); const finalConfig = result ?? mockConfig; const rules: any[] = finalConfig.module?.rules ?? mockModuleRules; diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts index e18ff62f4..730681370 100644 --- a/packages/vinext/src/entries/app-rsc-entry.ts +++ b/packages/vinext/src/entries/app-rsc-entry.ts @@ -174,14 +174,12 @@ export function generateRscEntry( const templateVars = route.templates.map((t) => getImportVar(t)); const notFoundVars = (route.notFoundPaths || []).map((nf) => (nf ? getImportVar(nf) : "null")); const slotEntries = route.parallelSlots.map((slot) => { - const interceptEntries = slot.interceptingRoutes.map((ir) => { - return ` { + const interceptEntries = slot.interceptingRoutes.map((ir) => ` { convention: ${JSON.stringify(ir.convention)}, targetPattern: ${JSON.stringify(ir.targetPattern)}, page: ${getImportVar(ir.pagePath)}, params: ${JSON.stringify(ir.params)}, - }`; - }); + }`); return ` ${JSON.stringify(slot.name)}: { page: ${slot.pagePath ? getImportVar(slot.pagePath) : "null"}, default: ${slot.defaultPath ? getImportVar(slot.defaultPath) : "null"}, diff --git a/packages/vinext/src/entries/pages-server-entry.ts b/packages/vinext/src/entries/pages-server-entry.ts index 419b1bd1f..6ff4b9628 100644 --- a/packages/vinext/src/entries/pages-server-entry.ts +++ b/packages/vinext/src/entries/pages-server-entry.ts @@ -64,9 +64,7 @@ export async function generateServerEntry( return ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`; }); - const apiRouteEntries = apiRoutes.map((r: Route, i: number) => { - return ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`; - }); + const apiRouteEntries = apiRoutes.map((r: Route, i: number) => ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`); // Check for _app and _document const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher); diff --git a/packages/vinext/src/global.d.ts b/packages/vinext/src/global.d.ts index c1a637260..417f935b5 100644 --- a/packages/vinext/src/global.d.ts +++ b/packages/vinext/src/global.d.ts @@ -131,7 +131,7 @@ declare global { * The browser RSC entry monkey-patches this array's `push` method to feed a * `ReadableStream` that is consumed by `react-server-dom-webpack`. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_RSC_CHUNKS__: string[] | undefined; /** @@ -139,7 +139,7 @@ declare global { * emitting all RSC chunks for the current request. * The browser RSC entry closes the `ReadableStream` when it sees this flag. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_RSC_DONE__: boolean | undefined; /** @@ -147,7 +147,7 @@ declare global { * script so they are available synchronously before hydration. * Shape: `Record` (same as Next.js `params`). */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_RSC_PARAMS__: Record | undefined; /** @@ -158,7 +158,7 @@ declare global { * `searchParams` is serialised as an array of `[key, value]` pairs to * preserve duplicate keys (e.g. `?tag=a&tag=b`). */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_RSC_NAV__: { pathname: string; searchParams: [string, string][] } | undefined; /** @@ -171,7 +171,7 @@ declare global { * @deprecated Use `__VINEXT_RSC_CHUNKS__` / `__VINEXT_RSC_DONE__` / * `__VINEXT_RSC_PARAMS__` instead. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_RSC__: { rsc: string[]; params: Record } | undefined; // ── globalThis globals — server-side / Cloudflare Workers ───────────────── @@ -188,7 +188,7 @@ declare global { * Read by `collectAssetTags()` to inject `` and * `` tags into the SSR HTML. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_SSR_MANIFEST__: Record | undefined; /** @@ -198,7 +198,7 @@ declare global { * Injected into the Worker entry at build time; also set at Node.js server * startup by `server/prod-server.ts`. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_LAZY_CHUNKS__: string[] | undefined; /** @@ -208,7 +208,7 @@ declare global { * App Router uses the RSC plugin's `loadBootstrapScriptContent` mechanism * instead. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_CLIENT_ENTRY__: string | undefined; /** @@ -216,13 +216,13 @@ declare global { * (Pages Router with i18n). Mirrors `window.__VINEXT_LOCALE__` for use in * environments where `window` is not available (e.g. Cloudflare Workers). */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_LOCALE__: string | undefined; /** * All configured locales, set on `globalThis` for server-side SSR rendering. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_LOCALES__: string[] | undefined; /** @@ -230,7 +230,7 @@ declare global { * Also read client-side from `globalThis` in `shims/link.tsx` when `window` * is not yet available (e.g. during SSR of Link components). */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_DEFAULT_LOCALE__: string | undefined; /** @@ -238,7 +238,7 @@ declare global { * server-side rendering so `next/link` can resolve cross-domain locale hrefs * before hydration. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_DOMAIN_LOCALES__: | Array<{ domain: string; defaultLocale: string; locales?: string[]; http?: boolean }> | undefined; @@ -248,7 +248,7 @@ declare global { * locale-domain links can decide whether to render relative or absolute * hrefs. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_HOSTNAME__: string | undefined; /** @@ -260,7 +260,7 @@ declare global { * `@cloudflare/vite-plugin` it runs entirely inside the Worker, so * `globalThis` is the Worker's global — also correct. */ - // eslint-disable-next-line no-var + // oxlint-disable-next-line no-var var __VINEXT_onRequestErrorHandler__: OnRequestErrorHandler | undefined; } diff --git a/packages/vinext/src/plugins/client-reference-dedup.ts b/packages/vinext/src/plugins/client-reference-dedup.ts index 3e0400f39..26ac3d60b 100644 --- a/packages/vinext/src/plugins/client-reference-dedup.ts +++ b/packages/vinext/src/plugins/client-reference-dedup.ts @@ -24,7 +24,7 @@ export function extractPackageName(absolutePath: string): string | null { } const DEDUP_PREFIX = "\0vinext:dedup/"; -// eslint-disable-next-line no-control-regex -- null byte prefix is intentional (Vite virtual module convention) +// oxlint-disable-next-line no-control-regex -- null byte prefix is intentional (Vite virtual module convention) const DEDUP_FILTER = /^\0vinext:dedup\//; const PROXY_MARKER = "virtual:vite-rsc/client-in-server-package-proxy/"; diff --git a/packages/vinext/src/plugins/fix-use-server-closure-collision.ts b/packages/vinext/src/plugins/fix-use-server-closure-collision.ts index bd02716a1..93ce02b34 100644 --- a/packages/vinext/src/plugins/fix-use-server-closure-collision.ts +++ b/packages/vinext/src/plugins/fix-use-server-closure-collision.ts @@ -33,7 +33,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // Only JS/TS files if (!/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(id.split("?")[0])) return null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any let ast: any; try { ast = parseAst(code); @@ -41,7 +41,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { return null; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function collectPatternNames(pattern: any, names: Set) { if (!pattern) return; if (pattern.type === "Identifier") { @@ -62,7 +62,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // Check if a block body has 'use server' as its leading directive prologue. // Only the first contiguous run of string-literal expression statements // counts — a "use server" string mid-function is not a directive. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function hasUseServerDirective(body: any[]): boolean { for (const stmt of body) { if ( @@ -91,7 +91,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { const renamedRanges = new Set(); let changed = false; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function visitNode(node: any, ancestorNames: Set) { if (!node || typeof node !== "object") return; @@ -265,7 +265,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // - Never call s.update() on the same source range twice // // `parent` is the direct parent AST node, used to detect property contexts. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function renamingWalk(node: any, from: string, to: string, parent?: any) { if (!node || typeof node !== "object") return; @@ -371,7 +371,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // know if ANY declaration of `from` exists in a nested function's scope // (params already handled separately). - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function collectFunctionScopedNames(node: any, names: Set) { if (!node || typeof node !== "object") return; // FunctionDeclaration: its name is a binding in the enclosing scope. @@ -414,7 +414,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // declaration — regardless of kind — must stop the rename from descending // further, and by the server-function local-decl scan to detect all // possible collision sites (including class declarations in the body). - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any function collectAllDeclaredNames(node: any, names: Set) { if (!node || typeof node !== "object") return; if (node.type === "VariableDeclaration") { diff --git a/packages/vinext/src/plugins/fonts.ts b/packages/vinext/src/plugins/fonts.ts index 75e64ba73..26cee42ad 100644 --- a/packages/vinext/src/plugins/fonts.ts +++ b/packages/vinext/src/plugins/fonts.ts @@ -93,7 +93,7 @@ export function parseStaticObjectLiteral(objectStr: string): Record = {}; try { const parsed = parseStaticObjectLiteral(optionsStr); diff --git a/packages/vinext/src/server/app-page-boundary-render.ts b/packages/vinext/src/server/app-page-boundary-render.ts index d31e918d8..77e711910 100644 --- a/packages/vinext/src/server/app-page-boundary-render.ts +++ b/packages/vinext/src/server/app-page-boundary-render.ts @@ -177,11 +177,13 @@ function wrapRenderedBoundaryElement( renderErrorBoundary(GlobalErrorComponent, children) { return createElement(ErrorBoundary, { fallback: GlobalErrorComponent, + // oxlint-disable-next-line react/no-children-prop children, }); }, renderLayout(LayoutComponent, children, asyncParams) { return createElement(LayoutComponent as AppPageComponent, { + // oxlint-disable-next-line react/no-children-prop children, params: asyncParams, }); diff --git a/packages/vinext/src/server/app-ssr-stream.ts b/packages/vinext/src/server/app-ssr-stream.ts index f5c143c35..f76cb12b4 100644 --- a/packages/vinext/src/server/app-ssr-stream.ts +++ b/packages/vinext/src/server/app-ssr-stream.ts @@ -82,9 +82,7 @@ export function createRscEmbedTransform( * HTML spec requires as="style" for . */ export function fixPreloadAs(html: string): string { - return html.replace(/]*\srel="preload")[^>]*>/g, (tag) => { - return tag.replace(' as="stylesheet"', ' as="style"'); - }); + return html.replace(/]*\srel="preload")[^>]*>/g, (tag) => tag.replace(' as="stylesheet"', ' as="style"')); } /** diff --git a/packages/vinext/src/server/dev-module-runner.ts b/packages/vinext/src/server/dev-module-runner.ts index fd7dbc78c..ae93ac224 100644 --- a/packages/vinext/src/server/dev-module-runner.ts +++ b/packages/vinext/src/server/dev-module-runner.ts @@ -92,7 +92,7 @@ export function createDirectRunner(environment: DevEnvironmentLike | DevEnvironm // { type: "custom", event: "vite:invoke", data: { id, name, data: args } } // normalizeModuleRunnerTransport() unpacks this before calling our impl, // so `payload.data` is already `{ id, name, data: args }`. - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any invoke: async (payload: any) => { const { name, data: args } = payload.data; diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index fc767b5de..5cb8f41b9 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -405,15 +405,13 @@ export function createSSRHandler( // Only allow paths explicitly listed in getStaticPaths const paths: Array<{ params: Record }> = pathsResult?.paths ?? []; - const isValidPath = paths.some((p: { params: Record }) => { - return Object.entries(p.params).every(([key, val]) => { + const isValidPath = paths.some((p: { params: Record }) => Object.entries(p.params).every(([key, val]) => { const actual = params[key]; if (Array.isArray(val)) { return Array.isArray(actual) && val.join("/") === actual.join("/"); } return String(val) === String(actual); - }); - }); + })); if (!isValidPath) { await renderErrorPage( @@ -748,7 +746,7 @@ export function createSSRHandler( } // Try to load _app.tsx if it exists - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any let AppComponent: any = null; const appPath = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPath, matcher)) { diff --git a/packages/vinext/src/server/instrumentation.ts b/packages/vinext/src/server/instrumentation.ts index 08828df2b..5762a3b0a 100644 --- a/packages/vinext/src/server/instrumentation.ts +++ b/packages/vinext/src/server/instrumentation.ts @@ -53,14 +53,15 @@ export interface ModuleImporter { * 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. + * per-call oxlint-disable comments. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// oxlint-disable-next-line @typescript-eslint/no-explicit-any export async function importModule( runner: ModuleImporter, id: string, + // oxlint-disable-next-line @typescript-eslint/no-explicit-any ): Promise> { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // oxlint-disable-next-line @typescript-eslint/no-explicit-any return (await runner.import(id)) as Record; } diff --git a/packages/vinext/src/server/middleware.ts b/packages/vinext/src/server/middleware.ts index 30e0d7d8d..aec4580ff 100644 --- a/packages/vinext/src/server/middleware.ts +++ b/packages/vinext/src/server/middleware.ts @@ -59,6 +59,7 @@ export function isProxyFile(filePath: string): boolean { * @see https://github.com/vercel/next.js/blob/canary/packages/next/src/build/templates/middleware.ts * @see https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts */ +// oxlint-disable-next-line typescript/no-unsafe-function-type export function resolveMiddlewareHandler(mod: Record, filePath: string): Function { const isProxy = isProxyFile(filePath); const handler = isProxy ? (mod.proxy ?? mod.default) : (mod.middleware ?? mod.default); @@ -71,6 +72,7 @@ export function resolveMiddlewareHandler(mod: Record, filePath: ); } + // oxlint-disable-next-line typescript/no-unsafe-function-type return handler as Function; } diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts index 068377048..a74e1f938 100644 --- a/packages/vinext/src/shims/dynamic.ts +++ b/packages/vinext/src/shims/dynamic.ts @@ -97,11 +97,9 @@ function dynamic

( if (!ssr) { if (isServer) { // On the server (SSR or RSC), just render the loading state or nothing - const SSRFalse = (_props: P) => { - return LoadingComponent + const SSRFalse = (_props: P) => LoadingComponent ? React.createElement(LoadingComponent, { isLoading: true, pastDelay: true, error: null }) : null; - }; SSRFalse.displayName = "DynamicSSRFalse"; return SSRFalse; } diff --git a/packages/vinext/src/shims/error-boundary.tsx b/packages/vinext/src/shims/error-boundary.tsx index 536a7bee8..6707af74a 100644 --- a/packages/vinext/src/shims/error-boundary.tsx +++ b/packages/vinext/src/shims/error-boundary.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -- next/navigation is shimmed +// oxlint-disable-next-line @typescript-eslint/no-require-imports -- next/navigation is shimmed import { usePathname } from "next/navigation"; interface ErrorBoundaryProps { diff --git a/packages/vinext/src/shims/head.ts b/packages/vinext/src/shims/head.ts index c86c0c9f1..c87c82b14 100644 --- a/packages/vinext/src/shims/head.ts +++ b/packages/vinext/src/shims/head.ts @@ -178,9 +178,7 @@ function createUniqueHeadFilter(): (child: React.ReactElement) => boolean { export function reduceHeadChildren(headChildren: React.ReactNode[]): React.ReactElement[] { return headChildren - .reduce((flattenedChildren, child) => { - return flattenedChildren.concat(Children.toArray(child)); - }, []) + .reduce((flattenedChildren, child) => flattenedChildren.concat(Children.toArray(child)), []) .reduce(collectHeadElements, []) .reverse() .filter(createUniqueHeadFilter()) @@ -321,7 +319,7 @@ function Head({ children }: HeadProps): null { } // Client path: update the shared head projection after hydration. - // eslint-disable-next-line react-hooks/rules-of-hooks + // oxlint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { const instanceId = headInstanceIdRef.current!; _clientHeadChildren.set(instanceId, children); diff --git a/packages/vinext/src/shims/link.tsx b/packages/vinext/src/shims/link.tsx index abe6ce5e6..9db8b381d 100644 --- a/packages/vinext/src/shims/link.tsx +++ b/packages/vinext/src/shims/link.tsx @@ -490,7 +490,7 @@ const Link = forwardRef(function Link( // Pages Router: use the Router singleton try { const routerModule = await import("next/router"); - // eslint-disable-next-line -- vinext's Router shim accepts (url, as, options) + // oxlint-disable-next-line @typescript-eslint/no-explicit-any -- vinext's Router shim accepts (url, as, options) const Router = routerModule.default as any; if (replace) { await Router.replace(absoluteHref, undefined, { scroll }); diff --git a/packages/vinext/src/shims/navigation-state.ts b/packages/vinext/src/shims/navigation-state.ts index 1931d4e10..0f5db604e 100644 --- a/packages/vinext/src/shims/navigation-state.ts +++ b/packages/vinext/src/shims/navigation-state.ts @@ -12,7 +12,11 @@ */ import { AsyncLocalStorage } from "node:async_hooks"; -import { _registerStateAccessors, type NavigationContext } from "./navigation.js"; +import { + _registerStateAccessors, + type NavigationContext, + GLOBAL_ACCESSORS_KEY, +} from "./navigation.js"; import { isInsideUnifiedScope, getRequestContext, @@ -99,8 +103,6 @@ export function runWithServerInsertedHTMLState(fn: () => T | Promise): T | // for "use client" components due to pre-bundling or env separation. // --------------------------------------------------------------------------- -import { GLOBAL_ACCESSORS_KEY } from "./navigation"; - const _accessors = { getServerContext(): NavigationContext | null { return _getState().serverContext; diff --git a/packages/vinext/src/shims/navigation.ts b/packages/vinext/src/shims/navigation.ts index 82f4ecc71..ccd7fbc81 100644 --- a/packages/vinext/src/shims/navigation.ts +++ b/packages/vinext/src/shims/navigation.ts @@ -92,6 +92,7 @@ function useChildSegments(): string[] { // This branch is only taken in SSR/Browser, never in RSC. // Try/catch for unit tests that call this hook outside a React render tree. try { + // oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks return React.useContext(ctx); } catch { return []; @@ -403,6 +404,7 @@ export function usePathname(): string { return _getServerContext()?.pathname ?? "/"; } // Client-side: use the hook system for reactivity + // oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks return React.useSyncExternalStore( subscribeToNavigation, getPathnameSnapshot, @@ -419,6 +421,7 @@ export function useSearchParams(): ReadonlyURLSearchParams { // Return a safe fallback — the client will hydrate with the real value. return getServerSearchParamsSnapshot(); } + // oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks return React.useSyncExternalStore( subscribeToNavigation, getSearchParamsSnapshot, @@ -436,6 +439,7 @@ export function useParams< // During SSR of "use client" components, the navigation context may not be set. return (_getServerContext()?.params ?? _EMPTY_PARAMS) as T; } + // oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks return React.useSyncExternalStore( subscribeToNavigation, getClientParamsSnapshot as () => T, diff --git a/tests/api-handler.test.ts b/tests/api-handler.test.ts index 5f2f46e89..40f1059ce 100644 --- a/tests/api-handler.test.ts +++ b/tests/api-handler.test.ts @@ -13,12 +13,6 @@ import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; 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 { reportRequestError, @@ -26,6 +20,12 @@ import { } from "../packages/vinext/src/server/instrumentation.js"; import type { Route } from "../packages/vinext/src/routing/pages-router.js"; +vi.mock("../packages/vinext/src/server/instrumentation.js", () => ({ + reportRequestError: vi.fn(() => Promise.resolve()), + importModule: (runner: { import(id: string): Promise }, id: string) => + runner.import(id) as Promise>, +})); + beforeEach(() => { vi.clearAllMocks(); }); diff --git a/tests/app-page-boundary.test.ts b/tests/app-page-boundary.test.ts index efac8656e..36e2b7b29 100644 --- a/tests/app-page-boundary.test.ts +++ b/tests/app-page-boundary.test.ts @@ -186,9 +186,7 @@ describe("app page boundary helpers", () => { it("routes HTML boundary renders through the provided SSR callback", async () => { const createHtmlResponse = vi.fn( - async (rscStream: ReadableStream, status: number) => { - return new Response(`${status}:${await readText(rscStream)}`); - }, + async (rscStream: ReadableStream, status: number) => new Response(`${status}:${await readText(rscStream)}`), ); const response = await renderAppPageBoundaryResponse({ diff --git a/tests/app-page-probe.test.ts b/tests/app-page-probe.test.ts index 10acf9d94..563938ff7 100644 --- a/tests/app-page-probe.test.ts +++ b/tests/app-page-probe.test.ts @@ -5,9 +5,9 @@ describe("app page probe helpers", () => { it("handles layout special errors before probing the page", async () => { const layoutError = new Error("layout failed"); const pageProbe = vi.fn(() => "page"); - const renderLayoutSpecialError = vi.fn(async () => { - return new Response("layout-fallback", { status: 404 }); - }); + const renderLayoutSpecialError = vi.fn( + async () => new Response("layout-fallback", { status: 404 }), + ); const renderPageSpecialError = vi.fn(); const probedLayouts: number[] = []; @@ -85,9 +85,9 @@ describe("app page probe helpers", () => { it("turns special page probe failures into immediate responses", async () => { const pageError = new Error("page failed"); - const renderPageSpecialError = vi.fn(async () => { - return new Response("page-fallback", { status: 307 }); - }); + const renderPageSpecialError = vi.fn( + async () => new Response("page-fallback", { status: 307 }), + ); const response = await probeAppPageBeforeRender({ hasLoadingBoundary: false, diff --git a/tests/app-page-render.test.ts b/tests/app-page-render.test.ts index c74b184f3..5c542b2ba 100644 --- a/tests/app-page-render.test.ts +++ b/tests/app-page-render.test.ts @@ -25,16 +25,12 @@ function createCommonOptions() { const message = error instanceof Error ? error.message : String(error); return new Response(`boundary:${message}`, { status: 200 }); }); - const renderLayoutSpecialError = vi.fn(async (specialError) => { - return new Response(`layout:${specialError.statusCode}`, { + const renderLayoutSpecialError = vi.fn(async (specialError) => new Response(`layout:${specialError.statusCode}`, { status: specialError.statusCode, - }); - }); - const renderPageSpecialError = vi.fn(async (specialError) => { - return new Response(`page:${specialError.statusCode}`, { + })); + const renderPageSpecialError = vi.fn(async (specialError) => new Response(`page:${specialError.statusCode}`, { status: specialError.statusCode, - }); - }); + })); const isrSet = vi.fn(async () => {}); return { diff --git a/tests/app-router.test.ts b/tests/app-router.test.ts index 443954987..249657f8a 100644 --- a/tests/app-router.test.ts +++ b/tests/app-router.test.ts @@ -3006,7 +3006,7 @@ describe("App Router next.config.js features (generateRscEntry)", () => { const onErrorFn = extractFunction(code, "rscOnError"); const body = `${digestFn}\n${onErrorFn}\nreturn rscOnError;`; - // oxlint-disable-next-line typescript-eslint/no-implied-eval -- reconstructing emitted runtime code is the behavior under test + // oxlint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval -- reconstructing emitted runtime code is the behavior under test const factory = new Function("process", body); rscOnError = factory({ env: { NODE_ENV: "development" } }); }); diff --git a/tests/build-optimization.test.ts b/tests/build-optimization.test.ts index dea6c6e6a..f64877aeb 100644 --- a/tests/build-optimization.test.ts +++ b/tests/build-optimization.test.ts @@ -1337,10 +1337,9 @@ describe("vinext:async-hooks-stub", () => { // string shape. This catches subtle syntax errors that string matching misses. // Strip the ES module `export` keyword so we can evaluate with new Function. const cjsSource = source.replace(/^export\s+/m, "") + "\nreturn AsyncLocalStorage;"; - // oxlint-disable-next-line typescript-eslint/no-implied-eval -- evaluating generated source is the behavior under test + // oxlint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval -- evaluating generated source is the behavior under test const ALS = new Function(cjsSource)() as new () => { getStore(): unknown; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type run(store: unknown, fn: Function, ...args: unknown[]): unknown; exit(fn: () => unknown): unknown; }; diff --git a/tests/e2e/fixtures.ts b/tests/e2e/fixtures.ts index 3676def7b..dd0e41ae8 100644 --- a/tests/e2e/fixtures.ts +++ b/tests/e2e/fixtures.ts @@ -30,6 +30,7 @@ function shouldIgnoreError(message: string): boolean { * Extended test fixture that collects console errors and fails if any occur. */ export const test = base.extend<{ consoleErrors: string[] }>({ + // oxlint-disable-next-line eslint-plugin-react-hooks/rules-of-hooks consoleErrors: async ({ page }, use) => { const errors: string[] = []; diff --git a/tests/fetch-cache.test.ts b/tests/fetch-cache.test.ts index 0d27cd9a3..a77c06bc6 100644 --- a/tests/fetch-cache.test.ts +++ b/tests/fetch-cache.test.ts @@ -503,12 +503,13 @@ describe("fetch cache shim", () => { } // Make the upstream return a 500 error for the background refetch - fetchMock.mockImplementationOnce(async () => { - return new Response("Internal Server Error", { - status: 500, - headers: { "content-type": "text/plain" }, - }); - }); + fetchMock.mockImplementationOnce( + async () => + new Response("Internal Server Error", { + status: 500, + headers: { "content-type": "text/plain" }, + }), + ); // Should return stale data immediately (stale-while-revalidate) const res2 = await fetch("https://api.example.com/revalidate-error-test", { @@ -2024,16 +2025,17 @@ describe("fetch cache shim", () => { describe("Set-Cookie header stripping", () => { it("does not include Set-Cookie in cached response headers", async () => { - fetchMock.mockImplementationOnce(async () => { - return new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { - "content-type": "application/json", - "set-cookie": "session=abc123; Path=/; HttpOnly", - "x-custom": "keep-me", - }, - }); - }); + fetchMock.mockImplementationOnce( + async () => + new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { + "content-type": "application/json", + "set-cookie": "session=abc123; Path=/; HttpOnly", + "x-custom": "keep-me", + }, + }), + ); // First request — response has Set-Cookie const res1 = await fetch("https://api.example.com/set-cookie-test", { diff --git a/tests/font-google.test.ts b/tests/font-google.test.ts index 264142c89..f9e315f6f 100644 --- a/tests/font-google.test.ts +++ b/tests/font-google.test.ts @@ -486,12 +486,11 @@ describe("vinext:google-fonts plugin", () => { // Mock fetch for Inter only const originalFetch = globalThis.fetch; - globalThis.fetch = async () => { - return new Response("@font-face { font-family: 'Inter'; }", { + globalThis.fetch = async () => + new Response("@font-face { font-family: 'Inter'; }", { status: 200, headers: { "content-type": "text/css" }, }); - }; try { const transform = unwrapHook(plugin.transform); diff --git a/tests/link.test.ts b/tests/link.test.ts index 320354334..6a1cd42d1 100644 --- a/tests/link.test.ts +++ b/tests/link.test.ts @@ -25,6 +25,13 @@ import { isExternalUrl, isHashOnlyChange } from "../packages/vinext/src/shims/ro import { runWithI18nState } from "../packages/vinext/src/shims/i18n-state.js"; import { setI18nContext } from "../packages/vinext/src/shims/i18n-context.js"; +import { + resolveRelativeHref, + toBrowserNavigationHref, + toSameOriginAppPath, + toSameOriginPath, +} from "../packages/vinext/src/shims/url-utils.js"; + // ─── SSR rendering (mirrors Next.js test/unit/link-rendering.test.ts) ──── describe("Link rendering", () => { @@ -504,13 +511,6 @@ describe("Link i18n ALS isolation", () => { // Tests for the shared same-origin URL normalization utility. // Related to: https://github.com/cloudflare/vinext/issues/335 -import { - resolveRelativeHref, - toBrowserNavigationHref, - toSameOriginAppPath, - toSameOriginPath, -} from "../packages/vinext/src/shims/url-utils.js"; - describe("toSameOriginPath", () => { it("returns null on the server (no window)", () => { // In vitest (Node.js), typeof window === 'undefined' by default diff --git a/tests/optimize-imports.test.ts b/tests/optimize-imports.test.ts index 346682421..445926c88 100644 --- a/tests/optimize-imports.test.ts +++ b/tests/optimize-imports.test.ts @@ -19,7 +19,7 @@ import vinext from "../packages/vinext/src/index.js"; // ── Helpers ─────────────────────────────────────────────────── /** Unwrap a Vite plugin hook that may use the object-with-filter format */ -// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + function unwrapHook(hook: any): ((...args: any[]) => any) | undefined { return typeof hook === "function" ? hook : hook?.handler; } @@ -52,7 +52,7 @@ describe("vinext:optimize-imports plugin", () => { ) as Plugin; const transform = unwrapHook(plugin.transform)!; const code = `import { Slot } from "radix-ui";`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = await (transform as any).call(plugin, code, "\0virtual:something"); expect(result).toBeNull(); }); @@ -64,7 +64,7 @@ describe("vinext:optimize-imports plugin", () => { ) as Plugin; const transform = unwrapHook(plugin.transform)!; const code = `import React from 'react';\nconst x = 1;`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = await (transform as any).call(plugin, code, "/app/page.tsx"); expect(result).toBeNull(); }); @@ -81,7 +81,7 @@ describe("vinext:optimize-imports plugin", () => { // buildStart must be called first to initialize optimizedPackages const buildStart = unwrapHook((plugin as any).buildStart); if (buildStart) buildStart.call(plugin); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = await (transform as any).call( { ...plugin, environment: { name: "rsc" } }, code, @@ -642,7 +642,6 @@ describe("vinext:optimize-imports transform", () => { const transform = unwrapHook(plugin.transform)!; // Return a caller that fakes the environment context as RSC (server) return async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "rsc" } }, code, id); } @@ -751,7 +750,7 @@ describe("vinext:optimize-imports transform", () => { const transform = unwrapHook(plugin.transform)!; // lucide-react is in DEFAULT_OPTIMIZE_PACKAGES — use it to hit the env guard const code = `import { Sun } from "lucide-react";`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = await (transform as any).call( { ...plugin, environment: { name: "client" } }, code, @@ -884,7 +883,6 @@ describe("vinext:optimize-imports transform", () => { if (buildStartHook) await buildStartHook.call(plugin); const transform = unwrapHook(plugin.transform)!; const call = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "rsc" } }, code, id); const code = `import { Listbox as HeadlessListbox } from "antd";`; @@ -929,7 +927,6 @@ describe("vinext:optimize-imports transform", () => { if (buildStartHook) await buildStartHook.call(plugin); const transform = unwrapHook(plugin.transform)!; const call = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "rsc" } }, code, id); const code = `import { Button } from "antd";`; @@ -978,10 +975,8 @@ describe("vinext:optimize-imports transform", () => { const transform = unwrapHook(plugin.transform)!; const rscCall = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "rsc" } }, code, id); const ssrCall = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "ssr" } }, code, id); // RSC processes the barrel first — this registers `rsc:`. @@ -1041,11 +1036,9 @@ describe("vinext:optimize-imports transform", () => { // RSC environment: should use react-server entry → knows about RscButton const rscCall = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "rsc" } }, code, id); // SSR environment: should use import entry → knows about Button, not RscButton const ssrCall = async (code: string, id: string) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-call await (transform as any).call({ ...plugin, environment: { name: "ssr" } }, code, id); // RSC: RscButton is exported from the react-server barrel → rewrite succeeds diff --git a/tests/safe-json.test.ts b/tests/safe-json.test.ts index 194090722..286376210 100644 --- a/tests/safe-json.test.ts +++ b/tests/safe-json.test.ts @@ -219,7 +219,7 @@ describe("safeJsonStringify", () => { // We use Function() here intentionally in the test to verify the output // is valid JS — this is the exact context where it will be used (inside // a " + "y".repeat(100000); const result = safeJsonStringify(long); expect(result).not.toContain(""); - // eslint-disable-next-line no-new-func, typescript-eslint/no-implied-eval + // oxlint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval const parsed = new Function(`return (${result})`)(); expect(parsed).toBe(long); }); @@ -300,7 +300,7 @@ describe("safeJsonStringify", () => { // should remain as-is in the JSON (JSON.stringify already escapes the backslash). const input = "already escaped: \\u003c"; const result = safeJsonStringify(input); - // eslint-disable-next-line no-new-func, typescript-eslint/no-implied-eval + // oxlint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval const parsed = new Function(`return (${result})`)(); expect(parsed).toBe(input); }); @@ -312,7 +312,7 @@ describe("safeJsonStringify", () => { count: 42, }; const result = safeJsonStringify(data); - // eslint-disable-next-line no-new-func, typescript-eslint/no-implied-eval + // oxlint-disable-next-line no-new-func, @typescript-eslint/no-implied-eval const parsed = new Function(`return (${result})`)(); expect(parsed).toEqual(data); }); diff --git a/tests/shims.test.ts b/tests/shims.test.ts index 11cad4425..17baeb8d9 100644 --- a/tests/shims.test.ts +++ b/tests/shims.test.ts @@ -211,6 +211,7 @@ describe("next/navigation shim", () => { const html = renderToStaticMarkup( React.createElement(providerMod.LayoutSegmentProvider, { childSegments: ["explore"], + // oxlint-disable-next-line react/no-children-prop children: React.createElement(Probe), }), ); @@ -1272,7 +1273,7 @@ describe("next/server shim", () => { // unstable_cache scope. If cache.ts was already imported, the existing instance is // reused; if not, this standalone ALS is sufficient for the guard to work. if (!g[key]) g[key] = new AsyncLocalStorage(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (g[key] as any).run(true, () => { expect(() => after(() => {})).toThrow(/unstable_cache/); }); @@ -2797,7 +2798,7 @@ describe("middleware codegen parity", () => { await import("../packages/vinext/src/server/middleware-codegen.js"); // Eval the generated code and test it behaves identically to the runtime const code = generateSafeRegExpCode("modern") + generateMiddlewareMatcherCode("modern"); - // eslint-disable-next-line no-implied-eval -- intentional: eval generated codegen output + // oxlint-disable-next-line no-new-func, no-implied-eval -- intentional: eval generated codegen output const fn = new Function(code + "\nreturn { matchMiddlewarePattern, matchesMiddleware };"); const { matchMiddlewarePattern, matchesMiddleware } = fn(); @@ -2922,7 +2923,7 @@ describe("middleware codegen parity", () => { const { generateSafeRegExpCode, generateMiddlewareMatcherCode } = await import("../packages/vinext/src/server/middleware-codegen.js"); const code = generateSafeRegExpCode("es5") + generateMiddlewareMatcherCode("es5"); - // eslint-disable-next-line no-implied-eval -- intentional: eval generated codegen output + // oxlint-disable-next-line no-new-func, no-implied-eval -- intentional: eval generated codegen output const fn = new Function(code + "\nreturn { matchMiddlewarePattern, matchesMiddleware };"); const { matchMiddlewarePattern, matchesMiddleware } = fn(); @@ -2978,7 +2979,7 @@ describe("middleware codegen parity", () => { const { generateNormalizePathCode } = await import("../packages/vinext/src/server/middleware-codegen.js"); const code = generateNormalizePathCode("modern"); - // eslint-disable-next-line no-implied-eval -- intentional: eval generated codegen output + // oxlint-disable-next-line no-new-func, no-implied-eval -- intentional: eval generated codegen output const fn = new Function(code + "\nreturn __normalizePath;"); const __normalizePath = fn(); @@ -5915,8 +5916,8 @@ describe("proxyExternalRequest", () => { const request = new Request("http://localhost:3000/test"); const originalFetch = globalThis.fetch; - globalThis.fetch = async (_url: any, _init: any) => { - return new Response("ok", { + globalThis.fetch = async (_url: any, _init: any) => + new Response("ok", { status: 200, headers: { "content-type": "text/plain", @@ -5926,7 +5927,6 @@ describe("proxyExternalRequest", () => { // setting them on Response, so we test with headers that can be set. }, }); - }; try { const response = await proxyExternalRequest(request, "https://api.example.com/test"); @@ -5944,9 +5944,7 @@ describe("proxyExternalRequest", () => { const request = new Request("http://localhost:3000/test"); const originalFetch = globalThis.fetch; - globalThis.fetch = async (_url: any, _init: any) => { - return new Response("Not Found", { status: 404 }); - }; + globalThis.fetch = async (_url: any, _init: any) => new Response("Not Found", { status: 404 }); try { const response = await proxyExternalRequest(request, "https://api.example.com/missing"); @@ -6076,10 +6074,8 @@ describe("proxyExternalRequest", () => { const request = new Request("http://localhost:3000/test"); const originalFetch = globalThis.fetch; - globalThis.fetch = async (_url: any, _init: any) => { - // Simulate an upstream that sent gzip-encoded content. - // Node.js fetch() auto-decompresses, but the original headers remain. - return new Response("decompressed body", { + globalThis.fetch = async (_url: any, _init: any) => + new Response("decompressed body", { status: 200, headers: { "content-type": "application/json", @@ -6088,7 +6084,6 @@ describe("proxyExternalRequest", () => { "x-custom": "keep", }, }); - }; try { const response = await proxyExternalRequest(request, "https://api.example.com/data"); @@ -9260,9 +9255,7 @@ describe("handleImageOptimization", () => { transformImage: async ( _body: ReadableStream, options: { width: number; format: string; quality: number }, - ) => { - return new Response("transformed", { headers: { "Content-Type": options.format } }); - }, + ) => new Response("transformed", { headers: { "Content-Type": options.format } }), }; const response = await handleImageOptimization(request, handlers); expect(response.status).toBe(200); @@ -9285,10 +9278,8 @@ describe("handleImageOptimization", () => { status: 200, headers: { "Content-Type": "image/jpeg" }, }), - transformImage: async () => { - // Buggy transform that returns text/html - return new Response("transformed", { headers: { "Content-Type": "text/html" } }); - }, + transformImage: async () => + new Response("transformed", { headers: { "Content-Type": "text/html" } }), }; const response = await handleImageOptimization(request, handlers); expect(response.status).toBe(200); diff --git a/vite.config.ts b/vite.config.ts index a634af9e4..05b53276f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,6 +26,38 @@ export default defineConfig({ typeCheck: true, denyWarnings: true, }, + plugins: ["typescript", "unicorn", "import", "react"], + rules: { + // "@typescript-eslint/no-explicit-any": "error", + // "typescript/consistent-type-definitions": ["error", "type"], + "@typescript-eslint/no-unsafe-function-type": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-redeclare": "error", + "@typescript-eslint/no-implied-eval": "error", + + "unicorn/prefer-node-protocol": "error", + + "import/first": "error", + "import/no-duplicates": "error", + + "no-new-func": "error", + "no-implied-eval": "error", + "arrow-body-style": ["error", "as-needed"], + + "react/exhaustive-deps": "error", + "react/no-array-index-key": "error", + "react/rules-of-hooks": "error", + "react/self-closing-comp": "error", + }, + overrides: [ + { + files: ["**.spec.ts", "**.test.ts"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + }, + }, + ], }, test: { // GitHub Actions reporter adds inline failure annotations in PR diffs. From 60a03c8828afe016bc5d1bc3453b5fb4fd9e1dfd Mon Sep 17 00:00:00 2001 From: James Date: Sun, 29 Mar 2026 20:31:32 +0100 Subject: [PATCH 2/3] fmt --- packages/vinext/src/entries/app-rsc-entry.ts | 6 ++++-- .../vinext/src/entries/pages-server-entry.ts | 5 ++++- packages/vinext/src/server/app-ssr-stream.ts | 4 +++- packages/vinext/src/server/dev-server.ts | 6 ++++-- packages/vinext/src/shims/dynamic.ts | 3 ++- packages/vinext/src/shims/head.ts | 5 ++++- tests/app-page-boundary.test.ts | 3 ++- tests/app-page-render.test.ts | 18 ++++++++++++------ 8 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/vinext/src/entries/app-rsc-entry.ts b/packages/vinext/src/entries/app-rsc-entry.ts index 730681370..7a14d4b1a 100644 --- a/packages/vinext/src/entries/app-rsc-entry.ts +++ b/packages/vinext/src/entries/app-rsc-entry.ts @@ -174,12 +174,14 @@ export function generateRscEntry( const templateVars = route.templates.map((t) => getImportVar(t)); const notFoundVars = (route.notFoundPaths || []).map((nf) => (nf ? getImportVar(nf) : "null")); const slotEntries = route.parallelSlots.map((slot) => { - const interceptEntries = slot.interceptingRoutes.map((ir) => ` { + const interceptEntries = slot.interceptingRoutes.map( + (ir) => ` { convention: ${JSON.stringify(ir.convention)}, targetPattern: ${JSON.stringify(ir.targetPattern)}, page: ${getImportVar(ir.pagePath)}, params: ${JSON.stringify(ir.params)}, - }`); + }`, + ); return ` ${JSON.stringify(slot.name)}: { page: ${slot.pagePath ? getImportVar(slot.pagePath) : "null"}, default: ${slot.defaultPath ? getImportVar(slot.defaultPath) : "null"}, diff --git a/packages/vinext/src/entries/pages-server-entry.ts b/packages/vinext/src/entries/pages-server-entry.ts index 6ff4b9628..44dbf2d1a 100644 --- a/packages/vinext/src/entries/pages-server-entry.ts +++ b/packages/vinext/src/entries/pages-server-entry.ts @@ -64,7 +64,10 @@ export async function generateServerEntry( return ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: page_${i}, filePath: ${JSON.stringify(absPath)} }`; }); - const apiRouteEntries = apiRoutes.map((r: Route, i: number) => ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`); + const apiRouteEntries = apiRoutes.map( + (r: Route, i: number) => + ` { pattern: ${JSON.stringify(r.pattern)}, patternParts: ${JSON.stringify(r.patternParts)}, isDynamic: ${r.isDynamic}, params: ${JSON.stringify(r.params)}, module: api_${i} }`, + ); // Check for _app and _document const appFilePath = findFileWithExts(pagesDir, "_app", fileMatcher); diff --git a/packages/vinext/src/server/app-ssr-stream.ts b/packages/vinext/src/server/app-ssr-stream.ts index f76cb12b4..d39431f71 100644 --- a/packages/vinext/src/server/app-ssr-stream.ts +++ b/packages/vinext/src/server/app-ssr-stream.ts @@ -82,7 +82,9 @@ export function createRscEmbedTransform( * HTML spec requires as="style" for . */ export function fixPreloadAs(html: string): string { - return html.replace(/]*\srel="preload")[^>]*>/g, (tag) => tag.replace(' as="stylesheet"', ' as="style"')); + return html.replace(/]*\srel="preload")[^>]*>/g, (tag) => + tag.replace(' as="stylesheet"', ' as="style"'), + ); } /** diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index 5cb8f41b9..cea7c4407 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -405,13 +405,15 @@ export function createSSRHandler( // Only allow paths explicitly listed in getStaticPaths const paths: Array<{ params: Record }> = pathsResult?.paths ?? []; - const isValidPath = paths.some((p: { params: Record }) => Object.entries(p.params).every(([key, val]) => { + const isValidPath = paths.some((p: { params: Record }) => + Object.entries(p.params).every(([key, val]) => { const actual = params[key]; if (Array.isArray(val)) { return Array.isArray(actual) && val.join("/") === actual.join("/"); } return String(val) === String(actual); - })); + }), + ); if (!isValidPath) { await renderErrorPage( diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts index a74e1f938..b1b8bbfb3 100644 --- a/packages/vinext/src/shims/dynamic.ts +++ b/packages/vinext/src/shims/dynamic.ts @@ -97,7 +97,8 @@ function dynamic

( if (!ssr) { if (isServer) { // On the server (SSR or RSC), just render the loading state or nothing - const SSRFalse = (_props: P) => LoadingComponent + const SSRFalse = (_props: P) => + LoadingComponent ? React.createElement(LoadingComponent, { isLoading: true, pastDelay: true, error: null }) : null; SSRFalse.displayName = "DynamicSSRFalse"; diff --git a/packages/vinext/src/shims/head.ts b/packages/vinext/src/shims/head.ts index c87c82b14..0b42c3057 100644 --- a/packages/vinext/src/shims/head.ts +++ b/packages/vinext/src/shims/head.ts @@ -178,7 +178,10 @@ function createUniqueHeadFilter(): (child: React.ReactElement) => boolean { export function reduceHeadChildren(headChildren: React.ReactNode[]): React.ReactElement[] { return headChildren - .reduce((flattenedChildren, child) => flattenedChildren.concat(Children.toArray(child)), []) + .reduce( + (flattenedChildren, child) => flattenedChildren.concat(Children.toArray(child)), + [], + ) .reduce(collectHeadElements, []) .reverse() .filter(createUniqueHeadFilter()) diff --git a/tests/app-page-boundary.test.ts b/tests/app-page-boundary.test.ts index 36e2b7b29..46c01be2c 100644 --- a/tests/app-page-boundary.test.ts +++ b/tests/app-page-boundary.test.ts @@ -186,7 +186,8 @@ describe("app page boundary helpers", () => { it("routes HTML boundary renders through the provided SSR callback", async () => { const createHtmlResponse = vi.fn( - async (rscStream: ReadableStream, status: number) => new Response(`${status}:${await readText(rscStream)}`), + async (rscStream: ReadableStream, status: number) => + new Response(`${status}:${await readText(rscStream)}`), ); const response = await renderAppPageBoundaryResponse({ diff --git a/tests/app-page-render.test.ts b/tests/app-page-render.test.ts index 5c542b2ba..2e64b626a 100644 --- a/tests/app-page-render.test.ts +++ b/tests/app-page-render.test.ts @@ -25,12 +25,18 @@ function createCommonOptions() { const message = error instanceof Error ? error.message : String(error); return new Response(`boundary:${message}`, { status: 200 }); }); - const renderLayoutSpecialError = vi.fn(async (specialError) => new Response(`layout:${specialError.statusCode}`, { - status: specialError.statusCode, - })); - const renderPageSpecialError = vi.fn(async (specialError) => new Response(`page:${specialError.statusCode}`, { - status: specialError.statusCode, - })); + const renderLayoutSpecialError = vi.fn( + async (specialError) => + new Response(`layout:${specialError.statusCode}`, { + status: specialError.statusCode, + }), + ); + const renderPageSpecialError = vi.fn( + async (specialError) => + new Response(`page:${specialError.statusCode}`, { + status: specialError.statusCode, + }), + ); const isrSet = vi.fn(async () => {}); return { From 6b30d8858e8b6b50fdec10652b7a04324dea922e Mon Sep 17 00:00:00 2001 From: James Date: Sun, 29 Mar 2026 20:31:48 +0100 Subject: [PATCH 3/3] add todos --- vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 05b53276f..b8a94666e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,8 +28,8 @@ export default defineConfig({ }, plugins: ["typescript", "unicorn", "import", "react"], rules: { - // "@typescript-eslint/no-explicit-any": "error", - // "typescript/consistent-type-definitions": ["error", "type"], + // TODO: "@typescript-eslint/no-explicit-any": "error", + // TODO: "typescript/consistent-type-definitions": ["error", "type"], "@typescript-eslint/no-unsafe-function-type": "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-redeclare": "error",