From 3246c41e4573a7c8d3c6bdd9dbd3d77a6c57e770 Mon Sep 17 00:00:00 2001 From: Daniel Caspi Date: Mon, 30 Mar 2026 01:29:31 -0500 Subject: [PATCH 1/2] fix(next): detect next/image in Next.js 15+ App Router projects Next.js 15+ App Router projects may not populate the export marker or the client-reference-manifest with next/image references, even when next/image is actively used. This adds a source-level fallback that scans .tsx/.ts/.jsx/.js files under the app directory for next/image import statements before falling back to the images-manifest.json check. --- src/frameworks/next/utils.ts | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index 3bc9c245c64..4a3b80fc7f2 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -236,6 +236,14 @@ export async function isUsingImageOptimization( } } + // Next.js 15+ may not populate manifests with next/image references; + // fall back to scanning project source files in the app directory. + if (!isNextImageImported && isUsingAppDirectory(join(projectDir, distDir))) { + if (await isNextImageInProjectSource(projectDir)) { + isNextImageImported = true; + } + } + if (isNextImageImported) { const imagesManifest = await readJSON( join(projectDir, distDir, IMAGES_MANIFEST), @@ -275,6 +283,38 @@ export async function isUsingNextImageInAppDirectory( return false; } +/** + * Whether next/image is imported in the project source files under the app + * directory. This is a fallback for Next.js 15+ where the build manifests + * may not include next/image references even when the component is used. + * + * @param projectDir path to the project root + * @return true if any .tsx/.ts/.jsx/.js file under `app/` imports next/image + */ +export async function isNextImageInProjectSource(projectDir: string): Promise { + const appDir = join(projectDir, "app"); + if (!existsSync(appDir)) { + // Also check src/app for projects using the src directory layout + const srcAppDir = join(projectDir, "src", "app"); + if (!existsSync(srcAppDir)) { + return false; + } + return scanDirForNextImage(srcAppDir); + } + return scanDirForNextImage(appDir); +} + +async function scanDirForNextImage(dir: string): Promise { + const files = globSync(join(dir, "**", "*.{tsx,ts,jsx,js}")); + for (const filepath of files) { + const contents = await readFile(filepath, "utf-8"); + if (/from\s+['"]next\/image['"]/.test(contents)) { + return true; + } + } + return false; +} + /** * Whether Next.js app directory is being used * From 5870a2c854b32b25f9581e826e1476d933a3d8c1 Mon Sep 17 00:00:00 2001 From: Daniel Caspi Date: Mon, 30 Mar 2026 02:13:02 -0500 Subject: [PATCH 2/2] refactor(next): address review feedback on image detection fallback Reduce nesting in isNextImageInProjectSource by iterating over candidate directories. Simplify the conditional assignment in isUsingImageOptimization. Switch from globSync to async glob to avoid blocking the event loop during file scanning. --- src/frameworks/next/utils.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index 4a3b80fc7f2..55b9ea5c7dc 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -239,9 +239,7 @@ export async function isUsingImageOptimization( // Next.js 15+ may not populate manifests with next/image references; // fall back to scanning project source files in the app directory. if (!isNextImageImported && isUsingAppDirectory(join(projectDir, distDir))) { - if (await isNextImageInProjectSource(projectDir)) { - isNextImageImported = true; - } + isNextImageImported = await isNextImageInProjectSource(projectDir); } if (isNextImageImported) { @@ -292,20 +290,23 @@ export async function isUsingNextImageInAppDirectory( * @return true if any .tsx/.ts/.jsx/.js file under `app/` imports next/image */ export async function isNextImageInProjectSource(projectDir: string): Promise { - const appDir = join(projectDir, "app"); - if (!existsSync(appDir)) { + const dirsToScan = [ + join(projectDir, "app"), // Also check src/app for projects using the src directory layout - const srcAppDir = join(projectDir, "src", "app"); - if (!existsSync(srcAppDir)) { - return false; + join(projectDir, "src", "app"), + ]; + + for (const dir of dirsToScan) { + if (existsSync(dir)) { + return scanDirForNextImage(dir); } - return scanDirForNextImage(srcAppDir); } - return scanDirForNextImage(appDir); + + return false; } async function scanDirForNextImage(dir: string): Promise { - const files = globSync(join(dir, "**", "*.{tsx,ts,jsx,js}")); + const files = await glob(join(dir, "**", "*.{tsx,ts,jsx,js}")); for (const filepath of files) { const contents = await readFile(filepath, "utf-8"); if (/from\s+['"]next\/image['"]/.test(contents)) {