diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index 3bc9c245c64..55b9ea5c7dc 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -236,6 +236,12 @@ 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))) { + isNextImageImported = await isNextImageInProjectSource(projectDir); + } + if (isNextImageImported) { const imagesManifest = await readJSON( join(projectDir, distDir, IMAGES_MANIFEST), @@ -275,6 +281,41 @@ 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 dirsToScan = [ + join(projectDir, "app"), + // Also check src/app for projects using the src directory layout + join(projectDir, "src", "app"), + ]; + + for (const dir of dirsToScan) { + if (existsSync(dir)) { + return scanDirForNextImage(dir); + } + } + + return false; +} + +async function scanDirForNextImage(dir: string): Promise { + 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)) { + return true; + } + } + return false; +} + /** * Whether Next.js app directory is being used *