From 39ca7601c6b81cca37dfc9c4900e759e44d3f77c Mon Sep 17 00:00:00 2001 From: James Date: Sun, 29 Mar 2026 21:38:13 +0100 Subject: [PATCH 1/2] chore: turn on no-explicit-any lint rule --- packages/vinext/src/cli.ts | 14 +-- .../vinext/src/cloudflare/kv-cache-handler.ts | 1 + packages/vinext/src/config/config-matchers.ts | 4 +- packages/vinext/src/config/next-config.ts | 7 ++ packages/vinext/src/index.ts | 92 ++++++++++++------- .../fix-use-server-closure-collision.ts | 4 +- packages/vinext/src/plugins/fonts.ts | 2 +- .../src/server/app-page-boundary-render.ts | 2 + packages/vinext/src/server/dev-server.ts | 8 +- packages/vinext/src/server/middleware.ts | 4 +- packages/vinext/src/shims/cache-runtime.ts | 9 +- packages/vinext/src/shims/cache.ts | 1 + packages/vinext/src/shims/dynamic.ts | 2 + packages/vinext/src/shims/error-boundary.tsx | 8 +- packages/vinext/src/shims/form.tsx | 8 +- .../src/shims/layout-segment-context.tsx | 2 +- packages/vinext/src/shims/link.tsx | 6 +- packages/vinext/src/shims/next-shims.d.ts | 1 + .../src/shims/unified-request-context.ts | 1 + tests/e2e/helpers.ts | 2 +- tests/helpers.ts | 3 +- vite.config.ts | 2 +- 22 files changed, 119 insertions(+), 64 deletions(-) diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index 82d7d5e62..d89db6efa 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -424,19 +424,19 @@ async function buildApp() { userTransformPlugins = flat.filter( (p): p is import("vite").Plugin => !!p && - typeof (p as any).name === "string" && + typeof p.name === "string" && // vinext and its sub-plugins — re-registered below - !(p as any).name.startsWith("vinext:") && + !p.name.startsWith("vinext:") && // @vitejs/plugin-react — auto-registered by vinext - !(p as any).name.startsWith("vite:react") && + !p.name.startsWith("vite:react") && // @vitejs/plugin-rsc and its sub-plugins — App Router only - !(p as any).name.startsWith("rsc:") && - (p as any).name !== "vite-rsc-load-module-dev-proxy" && + !p.name.startsWith("rsc:") && + p.name !== "vite-rsc-load-module-dev-proxy" && // vite-tsconfig-paths — auto-registered by vinext - (p as any).name !== "vite-tsconfig-paths" && + p.name !== "vite-tsconfig-paths" && // cloudflare() — injects multi-env environments block which // conflicts with the plain SSR build config below - !(p as any).name.startsWith("vite-plugin-cloudflare"), + !p.name.startsWith("vite-plugin-cloudflare"), ); } } diff --git a/packages/vinext/src/cloudflare/kv-cache-handler.ts b/packages/vinext/src/cloudflare/kv-cache-handler.ts index bc2c54ffe..b4d6ccf1f 100644 --- a/packages/vinext/src/cloudflare/kv-cache-handler.ts +++ b/packages/vinext/src/cloudflare/kv-cache-handler.ts @@ -287,6 +287,7 @@ export class KVCacheHandler implements CacheHandler { // revalidate: 0 means "don't cache", so skip storage entirely. let effectiveRevalidate: number | undefined; if (ctx) { + // oxlint-disable-next-line @typescript-eslint/no-explicit-any const revalidate = (ctx as any).cacheControl?.revalidate ?? (ctx as any).revalidate; if (typeof revalidate === "number") { effectiveRevalidate = revalidate; diff --git a/packages/vinext/src/config/config-matchers.ts b/packages/vinext/src/config/config-matchers.ts index c45a34837..3f8605405 100644 --- a/packages/vinext/src/config/config-matchers.ts +++ b/packages/vinext/src/config/config-matchers.ts @@ -1080,8 +1080,8 @@ export async function proxyExternalRequest( let upstreamResponse: Response; try { upstreamResponse = await fetch(targetUrl.href, { ...init, signal: controller.signal }); - } catch (e: any) { - if (e?.name === "AbortError") { + } catch (e) { + if (e instanceof Error && e.name === "AbortError") { console.error("[vinext] External rewrite proxy timeout:", targetUrl.href); return new Response("Gateway Timeout", { status: 504 }); } diff --git a/packages/vinext/src/config/next-config.ts b/packages/vinext/src/config/next-config.ts index f0e2eba7f..15afbcb9c 100644 --- a/packages/vinext/src/config/next-config.ts +++ b/packages/vinext/src/config/next-config.ts @@ -316,6 +316,7 @@ async function resolveConfigValue( * Unwrap the config value from a loaded module namespace. */ async function unwrapConfig( + // oxlint-disable-next-line typescript/no-explicit-any mod: any, phase: string = PHASE_DEVELOPMENT_SERVER, ): Promise { @@ -656,11 +657,13 @@ async function probeWebpackConfig( return { aliases: {}, mdx: null }; } + // oxlint-disable-next-line typescript/no-explicit-any const mockModuleRules: any[] = []; const mockConfig = { context: root, resolve: { alias: {} as Record }, module: { rules: mockModuleRules }, + // oxlint-disable-next-line typescript/no-explicit-any plugins: [] as any[], }; const mockOptions = { @@ -674,6 +677,7 @@ async function probeWebpackConfig( // oxlint-disable-next-line typescript/no-unsafe-function-type const result = await (config.webpack as Function)(mockConfig, mockOptions); const finalConfig = result ?? mockConfig; + // oxlint-disable-next-line typescript/no-explicit-any const rules: any[] = finalConfig.module?.rules ?? mockModuleRules; return { aliases: normalizeAliasEntries(finalConfig.resolve?.alias, root), @@ -762,6 +766,7 @@ export function detectNextIntlConfig(root: string, resolved: ResolvedNextConfig) } } +// oxlint-disable-next-line typescript/no-explicit-any function extractMdxOptionsFromRules(rules: any[]): MdxOptions | null { // Search through webpack rules for the MDX loader injected by @next/mdx for (const rule of rules) { @@ -775,6 +780,7 @@ function extractMdxOptionsFromRules(rules: any[]): MdxOptions | null { * Recursively search a webpack rule (which may have nested `oneOf` arrays) * for an MDX loader and extract its remark/rehype/recma plugin options. */ +// oxlint-disable-next-line typescript/no-explicit-any function extractMdxLoaders(rule: any): MdxOptions | null { if (!rule) return null; @@ -814,6 +820,7 @@ function isMdxLoader(loaderPath: string): boolean { ); } +// oxlint-disable-next-line typescript/no-explicit-any function extractPluginsFromOptions(opts: any): MdxOptions | null { if (!opts || typeof opts !== "object") return null; diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index 07ee7143e..ccdedf239 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -85,6 +85,8 @@ import fs from "node:fs"; import { randomBytes } from "node:crypto"; import commonjs from "vite-plugin-commonjs"; +type ASTNode = ReturnType["body"][number]["parent"]; + const __dirname = import.meta.dirname; type VitePluginReactModule = typeof import("@vitejs/plugin-react"); @@ -321,7 +323,8 @@ const POSTCSS_CONFIG_FILES = [ * Stores the Promise itself so concurrent calls (RSC/SSR/Client config() hooks firing in * parallel) all await the same in-flight scan rather than each starting their own. */ -const _postcssCache = new Map>(); +// oxlint-disable-next-line typescript/no-explicit-any +const _postcssCache = new Map>(); // Cache materialized tsconfig/jsconfig aliases so Vite's glob and dynamic-import // transforms can see them via resolve.alias without re-reading config files per env. const _tsconfigAliasCache = new Map>(); @@ -355,7 +358,10 @@ function resolveTsconfigAliases(projectRoot: string): Record { * Returns the resolved PostCSS config object to inject into Vite's * `css.postcss`, or `undefined` if no resolution is needed. */ -function resolvePostcssStringPlugins(projectRoot: string): Promise<{ plugins: any[] } | undefined> { +// oxlint-disable-next-line typescript/no-explicit-any +function resolvePostcssStringPlugins( + projectRoot: string, +): Promise<{ plugins: unknown[] } | undefined> { if (_postcssCache.has(projectRoot)) return _postcssCache.get(projectRoot)!; const promise = _resolvePostcssStringPluginsUncached(projectRoot); @@ -365,7 +371,8 @@ function resolvePostcssStringPlugins(projectRoot: string): Promise<{ plugins: an async function _resolvePostcssStringPluginsUncached( projectRoot: string, -): Promise<{ plugins: any[] } | undefined> { + // oxlint-disable-next-line typescript/no-explicit-any +): Promise<{ plugins: unknown[] } | undefined> { // Find the PostCSS config file let configPath: string | null = null; for (const name of POSTCSS_CONFIG_FILES) { @@ -380,6 +387,7 @@ async function _resolvePostcssStringPluginsUncached( } // Load the config file + // oxlint-disable-next-line typescript/no-explicit-any let config: any; try { if ( @@ -411,7 +419,7 @@ async function _resolvePostcssStringPluginsUncached( return undefined; } const hasStringPlugins = config.plugins.some( - (p: any) => typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"), + (p: unknown) => typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"), ); if (!hasStringPlugins) { return undefined; @@ -420,7 +428,7 @@ async function _resolvePostcssStringPluginsUncached( // Resolve string plugin names to actual plugin functions const req = createRequire(path.join(projectRoot, "package.json")); const resolved = await Promise.all( - config.plugins.filter(Boolean).map(async (plugin: any) => { + config.plugins.filter(Boolean).map(async (plugin: unknown) => { if (typeof plugin === "string") { const resolved = req.resolve(plugin); const mod = await import(pathToFileURL(resolved).href); @@ -1384,25 +1392,27 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Detect if Cloudflare's vite plugin is present — if so, skip // SSR externals (Workers bundle everything, can't have Node.js externals). - const pluginsFlat: any[] = []; - function flattenPlugins(arr: any[]) { + const pluginsFlat: unknown[] = []; + function flattenPlugins(arr: unknown[]) { for (const p of arr) { if (Array.isArray(p)) flattenPlugins(p); else if (p) pluginsFlat.push(p); } } - flattenPlugins((config.plugins as any[]) ?? []); + flattenPlugins((config.plugins as unknown[]) ?? []); hasCloudflarePlugin = pluginsFlat.some( - (p: any) => + (p: unknown) => p && typeof p === "object" && + "name" in p && typeof p.name === "string" && (p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")), ); hasNitroPlugin = pluginsFlat.some( - (p: any) => + (p: unknown) => p && typeof p === "object" && + "name" in p && typeof p.name === "string" && (p.name === "nitro" || p.name.startsWith("nitro:")), ); @@ -1414,6 +1424,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // this and resolve the strings to actual plugin functions, then // inject via css.postcss so Vite uses the resolved plugins. // Only do this if the user hasn't already set css.postcss inline. + // oxlint-disable-next-line typescript/no-explicit-any let postcssOverride: { plugins: any[] } | undefined; if (!config.css?.postcss || typeof config.css.postcss === "string") { postcssOverride = await resolvePostcssStringPlugins(root); @@ -1422,9 +1433,10 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Auto-inject @mdx-js/rollup when MDX files exist and no MDX plugin is // already configured. Applies remark/rehype plugins from next.config. hasUserMdxPlugin = pluginsFlat.some( - (p: any) => + (p: unknown) => p && typeof p === "object" && + "name" in p && typeof p.name === "string" && (p.name === "@mdx-js/rollup" || p.name === "mdx"), ); @@ -1827,7 +1839,12 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Assumes @vitejs/plugin-react top-level plugin names continue to use // the vite:react* prefix across supported versions. const reactRootPlugins = config.plugins.filter( - (p: any) => p && typeof p.name === "string" && p.name.startsWith("vite:react"), + (p: unknown) => + p && + typeof p === "object" && + "name" in p && + typeof p.name === "string" && + p.name.startsWith("vite:react"), ); const counts = new Map(); for (const plugin of reactRootPlugins) { @@ -1853,7 +1870,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (rscPluginPromise) { // Count top-level RSC plugins (name === "rsc") — each call to // the rsc() factory produces exactly one plugin with this name. - const rscRootPlugins = config.plugins.filter((p: any) => p && p.name === "rsc"); + const rscRootPlugins = config.plugins.filter( + (p: unknown) => p && typeof p === "object" && "name" in p && p.name === "rsc", + ); if (rscRootPlugins.length > 1) { throw new Error( "[vinext] Duplicate @vitejs/plugin-rsc detected.\n" + @@ -2103,7 +2122,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // which may not be tracked in Vite's module graph. Explicitly // sending full-reload ensures changes are always reflected in // the browser. - hotUpdate(options: { file: string; server: ViteDevServer; modules: any[] }) { + hotUpdate(options: { file: string; server: ViteDevServer; modules: unknown[] }) { if (!hasPagesDir || hasAppDir) return; if (options.file.startsWith(pagesDir) && fileMatcher.extensionRegex.test(options.file)) { options.server.environments.client.hot.send({ type: "full-reload" }); @@ -2186,7 +2205,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // BEFORE Vite's built-in middleware. This ensures all requests // (including /@*, /__vite*, /node_modules* paths) are validated // before Vite serves any content. - server.middlewares.use((req: any, res: any, next: any) => { + server.middlewares.use((req, res, next) => { const blockReason = validateDevRequest( { origin: req.headers.origin as string | undefined, @@ -2308,6 +2327,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { }; const _origWriteHead = res.writeHead.bind(res); + // oxlint-disable-next-line typescript/no-explicit-any res.writeHead = function (statusCode, ...args: any[]) { // Normalise the optional headers argument (may be reason, headers object, or both). let headers: Record | undefined; @@ -3063,8 +3083,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const ast = parseAst(code); // Check for file-level "use cache" directive - const cacheDirective = (ast.body as any[]).find( - (node: any) => + const cacheDirective = ast.body.find( + (node) => node.type === "ExpressionStatement" && node.expression?.type === "Literal" && typeof node.expression.value === "string" && @@ -3075,14 +3095,16 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Accepts any function-like node: FunctionDeclaration/Expression, ArrowFunctionExpression, // or MethodDefinition. MethodDefinition stores its FunctionExpression in `.value`, not // `.body`, so we unwrap it here rather than at each call site to keep the callee safe. - function nodeHasInlineCacheDirective(node: any): boolean { + function nodeHasInlineCacheDirective(node: ASTNode): boolean { if (!node || typeof node !== "object") return false; // MethodDefinition wraps its FunctionExpression in .value; unwrap to reach .body. const fn = node.type === "MethodDefinition" ? node.value : node; // fn.body is a BlockStatement node ({type:"BlockStatement", body:Statement[]}), not // a raw array. Unwrap it. Arrow functions with expression bodies have a non-array // .body — the BlockStatement check handles that case (body.body would be undefined). - const stmts = fn?.body?.type === "BlockStatement" ? fn.body.body : null; + const stmts: ASTNode[] | null = + // oxlint-disable-next-line typescript/no-explicit-any + (fn as any)?.body?.type === "BlockStatement" ? (fn as any).body.body : null; if (Array.isArray(stmts)) { for (const stmt of stmts) { if ( @@ -3097,7 +3119,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } return false; } - function astHasInlineCache(nodes: any[]): boolean { + function astHasInlineCache(nodes: ASTNode[]): boolean { for (const node of nodes) { if (!node || typeof node !== "object") continue; if ( @@ -3112,7 +3134,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // Walk into variable declarations, export declarations, etc. for (const key of Object.keys(node)) { if (key === "type" || key === "start" || key === "end" || key === "loc") continue; - const child = node[key]; + const child = node[key as keyof typeof node] as ASTNode; if (Array.isArray(child) && child.some((c) => c && typeof c === "object")) { if (astHasInlineCache(child)) return true; } else if (child && typeof child === "object" && child.type) { @@ -3122,7 +3144,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { } return false; } - const hasInlineCache = !cacheDirective && astHasInlineCache(ast.body as any[]); + const hasInlineCache = !cacheDirective && astHasInlineCache(ast.body); if (!cacheDirective && !hasInlineCache) return null; @@ -3143,7 +3165,8 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { // registerCachedFunction. Page default exports are wrapped directly // (they're leaf components). Layout/template defaults are excluded // because they receive {children} from the framework. - const directiveValue: string = cacheDirective.expression.value; + // oxlint-disable-next-line typescript/no-explicit-any + const directiveValue = (cacheDirective as any).expression.value; const variant = directiveValue === "use cache" ? "" @@ -3159,11 +3182,11 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { const runtimeModuleUrl = pathToFileURL( resolveShimModulePath(shimsDir, "cache-runtime"), ).href; - const result = transformWrapExport(code, ast as any, { - runtime: (value: any, name: any) => + const result = transformWrapExport(code, ast, { + runtime: (value: string, name: string) => `(await import(${JSON.stringify(runtimeModuleUrl)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`, rejectNonAsyncFunction: false, - filter: (name: any, meta: any) => { + filter: (name: string, meta: { isFunction?: boolean }) => { // Skip non-functions (constants, types, etc.) if (meta.isFunction === false) return false; // Skip the default export on layout/template files — these @@ -3211,9 +3234,9 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { ).href; try { - const result = transformHoistInlineDirective(code, ast as any, { + const result = transformHoistInlineDirective(code, ast, { directive: /^use cache(:\s*\w+)?$/, - runtime: (value: any, name: any, meta: any) => { + runtime: (value: string, name: string, meta: { directiveMatch: string[] }) => { const directiveMatch = meta.directiveMatch[0]; const variant = directiveMatch === "use cache" @@ -3422,6 +3445,7 @@ export default function vinext(options: VinextOptions = {}): PluginOption[] { if (fs.existsSync(buildManifestPath)) { try { const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8")); + // oxlint-disable-next-line typescript/no-explicit-any for (const [, value] of Object.entries(buildManifest) as [string, any][]) { if (value && value.isEntry && value.file) { clientEntryFile = manifestFileWithBase(value.file, clientBase); @@ -3704,14 +3728,14 @@ function stripServerExports(code: string): string | null { const s = new MagicString(code); let changed = false; - for (const node of ast.body as any[]) { + for (const node of ast.body) { if (node.type !== "ExportNamedDeclaration") continue; // Case 1: export function name() {} / export async function name() {} // Case 2: export const/let/var name = ... if (node.declaration) { const decl = node.declaration; - if (decl.type === "FunctionDeclaration" && SERVER_EXPORTS.has(decl.id?.name)) { + if (decl.type === "FunctionDeclaration" && decl.id && SERVER_EXPORTS.has(decl.id.name)) { s.overwrite( node.start, node.end, @@ -3731,11 +3755,12 @@ function stripServerExports(code: string): string | null { // Case 3: export { getServerSideProps } or export { getServerSideProps as gSSP } if (node.specifiers && node.specifiers.length > 0 && !node.source) { - const kept: any[] = []; + const kept: Extract[] = []; const stripped: string[] = []; for (const spec of node.specifiers) { // spec.local.name is the binding name, spec.exported.name is the export name - const exportedName = spec.exported?.name ?? spec.exported?.value; + // oxlint-disable-next-line typescript/no-explicit-any + const exportedName = (spec.exported as any)?.name ?? (spec.exported as any)?.value; if (SERVER_EXPORTS.has(exportedName)) { stripped.push(exportedName); } else { @@ -3747,6 +3772,7 @@ function stripServerExports(code: string): string | null { const parts: string[] = []; if (kept.length > 0) { const keptStr = kept + // oxlint-disable-next-line typescript/no-explicit-any .map((sp: any) => { const local = sp.local.name; const exported = sp.exported?.name ?? sp.exported?.value; @@ -3774,6 +3800,7 @@ function stripServerExports(code: string): string | null { */ function applyRedirects( pathname: string, + // oxlint-disable-next-line typescript/no-explicit-any res: any, redirects: NextRedirect[], ctx: RequestContext, @@ -3881,6 +3908,7 @@ function applyRewrites( */ function applyHeaders( pathname: string, + // oxlint-disable-next-line typescript/no-explicit-any res: any, headers: NextHeader[], ctx: RequestContext, 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 93ce02b34..da0270700 100644 --- a/packages/vinext/src/plugins/fix-use-server-closure-collision.ts +++ b/packages/vinext/src/plugins/fix-use-server-closure-collision.ts @@ -124,7 +124,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { // Also collect let/const/var/class/import declared as immediate children // of this node (e.g. top-level Program statements, or the direct body of // a BlockStatement) — those ARE in scope for everything in the same block. - const immediateStmts: any[] = + const immediateStmts = node.type === "Program" ? node.body : node.type === "BlockStatement" ? node.body : []; for (const stmt of immediateStmts) { if (stmt?.type === "VariableDeclaration") { @@ -158,7 +158,7 @@ export const fixUseServerClosureCollisionPlugin: Plugin = { for (const p of node.params ?? []) collectPatternNames(p, namesForBody); // Check whether the body has the 'use server' directive. - const bodyStmts: any[] = node.body?.type === "BlockStatement" ? node.body.body : []; + const bodyStmts = node.body?.type === "BlockStatement" ? node.body.body : []; const isServerFn = hasUseServerDirective(bodyStmts); if (isServerFn) { diff --git a/packages/vinext/src/plugins/fonts.ts b/packages/vinext/src/plugins/fonts.ts index 26cee42ad..b21be8a6a 100644 --- a/packages/vinext/src/plugins/fonts.ts +++ b/packages/vinext/src/plugins/fonts.ts @@ -528,7 +528,7 @@ export function createGoogleFontsPlugin(fontGoogleShimPath: string, shimsDir: st try { const parsed = parseStaticObjectLiteral(optionsStr); if (!parsed) return; // Contains dynamic expressions, skip - options = parsed as Record; + options = parsed as Record; } catch { return; // Can't parse options statically, skip } diff --git a/packages/vinext/src/server/app-page-boundary-render.ts b/packages/vinext/src/server/app-page-boundary-render.ts index 77e711910..f62b4d7e1 100644 --- a/packages/vinext/src/server/app-page-boundary-render.ts +++ b/packages/vinext/src/server/app-page-boundary-render.ts @@ -25,6 +25,7 @@ import { type AppPageSsrHandler, } from "./app-page-stream.js"; +// oxlint-disable-next-line @typescript-eslint/no-explicit-any type AppPageComponent = ComponentType; type AppPageModule = Record & { default?: AppPageComponent | null | undefined; @@ -190,6 +191,7 @@ function wrapRenderedBoundaryElement( }, renderLayoutSegmentProvider(childSegments, children) { return createElement( + // oxlint-disable-next-line @typescript-eslint/no-explicit-any LayoutSegmentProvider as ComponentType, { childSegments }, children, diff --git a/packages/vinext/src/server/dev-server.ts b/packages/vinext/src/server/dev-server.ts index cea7c4407..ad46e2553 100644 --- a/packages/vinext/src/server/dev-server.ts +++ b/packages/vinext/src/server/dev-server.ts @@ -625,11 +625,12 @@ export function createSSRHandler( // Re-render the page with fresh props inside fresh // render sub-scopes so head/cache state cannot leak. + // oxlint-disable-next-line typescript/no-explicit-any let RegenApp: any = null; const appPath = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPath, matcher)) { try { - const appMod = (await runner.import(appPath)) as Record; + const appMod = (await runner.import(appPath)) as Record; RegenApp = appMod.default ?? null; } catch { // _app failed to load @@ -908,10 +909,11 @@ hydrate(); // Try to load custom _document.tsx const docPath = path.join(pagesDir, "_document"); + // oxlint-disable-next-line typescript/no-explicit-any let DocumentComponent: any = null; if (findFileWithExtensions(docPath, matcher)) { try { - const docModule = (await runner.import(docPath)) as Record; + const docModule = (await runner.import(docPath)) as Record; DocumentComponent = docModule.default ?? null; } catch { // _document exists but failed to load @@ -1064,6 +1066,7 @@ async function renderErrorPage( if (!ErrorComponent) continue; // Try to load _app.tsx to wrap the error page + // oxlint-disable-next-line typescript/no-explicit-any let AppComponent: any = null; const appPathErr = path.join(pagesDir, "_app"); if (findFileWithExtensions(appPathErr, matcher)) { @@ -1108,6 +1111,7 @@ async function renderErrorPage( // Try custom _document let html: string; + // oxlint-disable-next-line typescript/no-explicit-any let DocumentComponent: any = null; const docPathErr = path.join(pagesDir, "_document"); if (findFileWithExtensions(docPathErr, matcher)) { diff --git a/packages/vinext/src/server/middleware.ts b/packages/vinext/src/server/middleware.ts index aec4580ff..672e171d4 100644 --- a/packages/vinext/src/server/middleware.ts +++ b/packages/vinext/src/server/middleware.ts @@ -437,12 +437,12 @@ export async function runMiddleware( let response: Response | undefined; try { response = await middlewareFn(nextRequest, fetchEvent); - } catch (e: any) { + } catch (e) { console.error("[vinext] Middleware error:", e); const message = process.env.NODE_ENV === "production" ? "Internal Server Error" - : "Middleware Error: " + (e?.message ?? String(e)); + : "Middleware Error: " + (e instanceof Error ? e.message : String(e)); return { continue: false, response: new Response(message, { diff --git a/packages/vinext/src/shims/cache-runtime.ts b/packages/vinext/src/shims/cache-runtime.ts index 397167d3b..bff5f363e 100644 --- a/packages/vinext/src/shims/cache-runtime.ts +++ b/packages/vinext/src/shims/cache-runtime.ts @@ -301,6 +301,7 @@ export function clearPrivateCache(): void { * @param variant - Cache variant: "" (default/shared), "remote", "private" * @returns A wrapper function that checks cache before calling the original */ +// oxlint-disable-next-line typescript/no-explicit-any export function registerCachedFunction Promise>( fn: T, id: string, @@ -316,6 +317,7 @@ export function registerCachedFunction Promise => { const rsc = await getRscModule(); @@ -457,11 +459,13 @@ export function registerCachedFunction Promise Promise>( fn: T, + // oxlint-disable-next-line @typescript-eslint/no-explicit-any args: any[], variant: string, -): Promise { +): Promise>> { const ctx: CacheContext = { tags: [], lifeConfigs: [], @@ -502,11 +506,13 @@ function unwrapThenableObjects(value: unknown): unknown { // Detect thenable (Promise-like) with own enumerable properties — // this is the Object.assign(Promise.resolve(obj), obj) pattern. + // oxlint-disable-next-line @typescript-eslint/no-explicit-any if (typeof (value as any).then === "function") { const keys = Object.keys(value); if (keys.length > 0) { const plain: Record = {}; for (const key of keys) { + // oxlint-disable-next-line typescript/no-explicit-any plain[key] = unwrapThenableObjects((value as any)[key]); } return plain; @@ -518,6 +524,7 @@ function unwrapThenableObjects(value: unknown): unknown { // Regular object — recurse into values const result: Record = {}; for (const key of Object.keys(value)) { + // oxlint-disable-next-line @typescript-eslint/no-explicit-any result[key] = unwrapThenableObjects((value as any)[key]); } return result; diff --git a/packages/vinext/src/shims/cache.ts b/packages/vinext/src/shims/cache.ts index 3084559cc..da63084dd 100644 --- a/packages/vinext/src/shims/cache.ts +++ b/packages/vinext/src/shims/cache.ts @@ -677,6 +677,7 @@ interface UnstableCacheOptions { * Returns a new function that caches results. The cache key is derived * from keyParts + serialized arguments. */ +// oxlint-disable-next-line @typescript-eslint/no-explicit-any export function unstable_cache Promise>( fn: T, keyParts?: string[], diff --git a/packages/vinext/src/shims/dynamic.ts b/packages/vinext/src/shims/dynamic.ts index b1b8bbfb3..949ffedd5 100644 --- a/packages/vinext/src/shims/dynamic.ts +++ b/packages/vinext/src/shims/dynamic.ts @@ -35,6 +35,7 @@ type Loader

= () => Promise<{ default: ComponentType

} | ComponentType

> * Lazily created because React.Component is not available in the RSC environment * (server components use a slimmed-down React that doesn't include class components). */ +// oxlint-disable-next-line typescript/no-explicit-any let DynamicErrorBoundary: any; function getDynamicErrorBoundary() { if (DynamicErrorBoundary) return DynamicErrorBoundary; @@ -48,6 +49,7 @@ function getDynamicErrorBoundary() { { error: Error | null } > ) { + // oxlint-disable-next-line typescript/no-explicit-any constructor(props: any) { super(props); this.state = { error: null }; diff --git a/packages/vinext/src/shims/error-boundary.tsx b/packages/vinext/src/shims/error-boundary.tsx index 6707af74a..9c311f890 100644 --- a/packages/vinext/src/shims/error-boundary.tsx +++ b/packages/vinext/src/shims/error-boundary.tsx @@ -4,12 +4,12 @@ import React from "react"; // oxlint-disable-next-line @typescript-eslint/no-require-imports -- next/navigation is shimmed import { usePathname } from "next/navigation"; -interface ErrorBoundaryProps { +export interface ErrorBoundaryProps { fallback: React.ComponentType<{ error: Error; reset: () => void }>; children: React.ReactNode; } -interface ErrorBoundaryState { +export interface ErrorBoundaryState { error: Error | null; } @@ -29,7 +29,7 @@ export class ErrorBoundary extends React.Component { if (error && typeof error === "object" && "digest" in error) { - const digest = String((error as any).digest); + const digest = String(error.digest); if (digest === "NEXT_NOT_FOUND" || digest.startsWith("NEXT_HTTP_ERROR_FALLBACK;404")) { return { notFound: true }; } diff --git a/packages/vinext/src/shims/form.tsx b/packages/vinext/src/shims/form.tsx index f78d23e60..c617f4806 100644 --- a/packages/vinext/src/shims/form.tsx +++ b/packages/vinext/src/shims/form.tsx @@ -174,7 +174,7 @@ const Form = forwardRef(function Form(props: FormProps, ref: ForwardedRef; + return

; } // Block dangerous action URLs. Render without action attribute @@ -187,13 +187,13 @@ const Form = forwardRef(function Form(props: FormProps, ref: ForwardedRef blocked unsafe action: ${action}`); } - return ; + return ; } - async function handleSubmit(e: any) { + async function handleSubmit(e: React.SubmitEvent) { // Call user's onSubmit first if (onSubmit) { - (onSubmit as any)(e); + onSubmit(e); if (e.defaultPrevented) return; } diff --git a/packages/vinext/src/shims/layout-segment-context.tsx b/packages/vinext/src/shims/layout-segment-context.tsx index 32085cb8e..af9855a23 100644 --- a/packages/vinext/src/shims/layout-segment-context.tsx +++ b/packages/vinext/src/shims/layout-segment-context.tsx @@ -35,7 +35,7 @@ export function LayoutSegmentProvider({ const ctx = getLayoutSegmentContext(); if (!ctx) { // Fallback: no context available (shouldn't happen in SSR/Browser) - return children as any; + return children; } return createElement(ctx.Provider, { value: childSegments }, children); } diff --git a/packages/vinext/src/shims/link.tsx b/packages/vinext/src/shims/link.tsx index 9db8b381d..4b9ff0f06 100644 --- a/packages/vinext/src/shims/link.tsx +++ b/packages/vinext/src/shims/link.tsx @@ -157,7 +157,7 @@ function prefetchUrl(href: string): void { if (prefetched.has(rscUrl)) return; prefetched.add(rscUrl); - const schedule = (window as any).requestIdleCallback ?? ((fn: () => void) => setTimeout(fn, 100)); + const schedule = window.requestIdleCallback ?? ((fn: () => void) => setTimeout(fn, 100)); schedule(() => { if (typeof window.__VINEXT_RSC_NAVIGATE__ === "function") { @@ -165,7 +165,7 @@ function prefetchUrl(href: string): void { fetch(rscUrl, { headers: { Accept: "text/x-component" }, credentials: "include", - priority: "low" as any, + priority: "low" as const, // @ts-expect-error — purpose is a valid fetch option in some browsers purpose: "prefetch", }) @@ -305,7 +305,7 @@ const Link = forwardRef(function Link( forwardedRef, ) { // Extract locale from rest props - const { locale, ...restWithoutLocale } = rest as any; + const { locale, ...restWithoutLocale } = rest; // If `as` is provided, use it as the actual URL (legacy Next.js pattern // where href is a route pattern like "/user/[id]" and as is "/user/1") diff --git a/packages/vinext/src/shims/next-shims.d.ts b/packages/vinext/src/shims/next-shims.d.ts index d5c7cdf36..d221d885f 100644 --- a/packages/vinext/src/shims/next-shims.d.ts +++ b/packages/vinext/src/shims/next-shims.d.ts @@ -1,3 +1,4 @@ +// oxlint-disable typescript/no-explicit-any /** * Type declarations for next/* bare specifiers used within shims. * diff --git a/packages/vinext/src/shims/unified-request-context.ts b/packages/vinext/src/shims/unified-request-context.ts index 11057d6ef..b77a032ea 100644 --- a/packages/vinext/src/shims/unified-request-context.ts +++ b/packages/vinext/src/shims/unified-request-context.ts @@ -50,6 +50,7 @@ export interface UnifiedRequestContext // ── cache-for-request.ts ────────────────────────────────────────── /** Per-request cache for cacheForRequest(). Keyed by factory function reference. */ + // oxlint-disable-next-line @typescript-eslint/no-explicit-any requestCache: WeakMap<(...args: any[]) => any, unknown>; } diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index ee0b896f5..1eb074fd8 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -1,5 +1,5 @@ import type { Page } from "@playwright/test"; export async function waitForHydration(page: Page) { - await page.waitForFunction(() => Boolean((window as any).__VINEXT_ROOT__)); + await page.waitForFunction(() => Boolean(window.__VINEXT_ROOT__)); } diff --git a/tests/helpers.ts b/tests/helpers.ts index d95c892fc..5b48d944e 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -67,7 +67,7 @@ export async function startFixtureServer( // Pass appDir explicitly since tests run with configFile: false and // cwd may not be the fixture directory. // Note: opts.appRouter is accepted but unused — vinext auto-detects. - const plugins: any[] = [vinext({ appDir: fixtureDir })]; + const plugins = [vinext({ appDir: fixtureDir })]; const server = await createServer({ root: fixtureDir, @@ -122,6 +122,7 @@ export async function fetchJson( baseUrl: string, urlPath: string, init?: RequestInit, + // oxlint-disable-next-line typescript/no-explicit-any ): Promise<{ res: Response; data: any }> { const res = await fetch(`${baseUrl}${urlPath}`, init); const data = await res.json(); diff --git a/vite.config.ts b/vite.config.ts index b8a94666e..e73fa8b93 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ }, plugins: ["typescript", "unicorn", "import", "react"], rules: { - // TODO: "@typescript-eslint/no-explicit-any": "error", + "@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", From d4cf57d42ce68507fd49a3c8208f823eb818eb80 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Sun, 29 Mar 2026 21:43:48 +0100 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: James Anderson --- packages/vinext/src/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/vinext/src/index.ts b/packages/vinext/src/index.ts index ccdedf239..fc4e9c2f7 100644 --- a/packages/vinext/src/index.ts +++ b/packages/vinext/src/index.ts @@ -323,7 +323,6 @@ const POSTCSS_CONFIG_FILES = [ * Stores the Promise itself so concurrent calls (RSC/SSR/Client config() hooks firing in * parallel) all await the same in-flight scan rather than each starting their own. */ -// oxlint-disable-next-line typescript/no-explicit-any const _postcssCache = new Map>(); // Cache materialized tsconfig/jsconfig aliases so Vite's glob and dynamic-import // transforms can see them via resolve.alias without re-reading config files per env. @@ -358,7 +357,6 @@ function resolveTsconfigAliases(projectRoot: string): Record { * Returns the resolved PostCSS config object to inject into Vite's * `css.postcss`, or `undefined` if no resolution is needed. */ -// oxlint-disable-next-line typescript/no-explicit-any function resolvePostcssStringPlugins( projectRoot: string, ): Promise<{ plugins: unknown[] } | undefined> { @@ -371,7 +369,6 @@ function resolvePostcssStringPlugins( async function _resolvePostcssStringPluginsUncached( projectRoot: string, - // oxlint-disable-next-line typescript/no-explicit-any ): Promise<{ plugins: unknown[] } | undefined> { // Find the PostCSS config file let configPath: string | null = null;