Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/frameworks/next/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImagesManifest>(
join(projectDir, distDir, IMAGES_MANIFEST),
Expand Down Expand Up @@ -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<boolean> {
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<boolean> {
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
*
Expand Down