From 270f41944b98523e52168e1c0827c6690b894c07 Mon Sep 17 00:00:00 2001 From: Claudio F Date: Mon, 12 Dec 2022 17:28:29 -0500 Subject: [PATCH] Add sharp library to Next.js build (#5238) Checking for /app dir and nextImage to add the sharp library to the Next.js build --- CHANGELOG.md | 1 + src/frameworks/next/index.ts | 13 +++++ src/frameworks/next/interfaces.ts | 14 +++++ src/frameworks/next/utils.ts | 40 ++++++++++++- src/test/frameworks/next/utils.spec.ts | 80 ++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..b0c8713f3bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add sharp NPM module to Cloud Functions when using Next.js Image Optimization diff --git a/src/frameworks/next/index.ts b/src/frameworks/next/index.ts index 6a71d5dbfe0..53888503ed9 100644 --- a/src/frameworks/next/index.ts +++ b/src/frameworks/next/index.ts @@ -31,6 +31,7 @@ import { import type { Manifest } from "./interfaces"; import { readJSON } from "../utils"; import { warnIfCustomBuildScript } from "../utils"; +import { usesAppDirRouter, usesNextImage, hasUnoptimizedImage } from "./utils"; const CLI_COMMAND = join( "node_modules", @@ -370,6 +371,18 @@ export async function ɵcodegenFunctionsDirectory(sourceDir: string, destDir: st await mkdir(join(destDir, "public")); await copy(join(sourceDir, "public"), join(destDir, "public")); } + + // Add the `sharp` library if `/app` folder exists (i.e. Next.js 13+) + // or usesNextImage in `export-marker.json` is set to true. + // As of (10/2021) the new Next.js 13 route is in beta, and usesNextImage is always being set to false + // if the image component is used in pages coming from the new `/app` routes. + if ( + !(await hasUnoptimizedImage(sourceDir, distDir)) && + (usesAppDirRouter(sourceDir) || (await usesNextImage(sourceDir, distDir))) + ) { + packageJson.dependencies["sharp"] = "latest"; + } + await mkdirp(join(destDir, distDir)); await copy(join(sourceDir, distDir), join(destDir, distDir)); return { packageJson, frameworksEntry: "next.js" }; diff --git a/src/frameworks/next/interfaces.ts b/src/frameworks/next/interfaces.ts index e012402b9fb..4a88ac9f2d5 100644 --- a/src/frameworks/next/interfaces.ts +++ b/src/frameworks/next/interfaces.ts @@ -29,3 +29,17 @@ export interface Manifest { >; rewrites?: RoutesManifestRewrite[] | RoutesManifestRewriteObject; } + +export interface ExportMarker { + version: number; + hasExportPathMap: boolean; + exportTrailingSlash: boolean; + isNextImageImported: boolean; +} + +export interface ImageManifest { + version: number; + images: { + unoptimized: boolean; + }; +} diff --git a/src/frameworks/next/utils.ts b/src/frameworks/next/utils.ts index 040f06adedb..0d3cb4afbb1 100644 --- a/src/frameworks/next/utils.ts +++ b/src/frameworks/next/utils.ts @@ -1,6 +1,9 @@ +import { existsSync } from "fs"; +import { join } from "path"; import type { Header, Redirect, Rewrite } from "next/dist/lib/load-custom-routes"; import type { Manifest, RoutesManifestRewrite } from "./interfaces"; -import { isUrl } from "../utils"; +import { isUrl, readJSON } from "../utils"; +import type { ExportMarker, ImageManifest } from "./interfaces"; /** * Whether the given path has a regex or not. @@ -112,3 +115,38 @@ export function getNextjsRewritesToUse( return []; } + +/** + * Check if `/app` directory is used in the Next.js project. + * @param sourceDir location of the source directory + * @return true if app directory is used in the Next.js project + */ +export function usesAppDirRouter(sourceDir: string): boolean { + const appPathRoutesManifestPath = join(sourceDir, "app-path-routes-manifest.json"); + return existsSync(appPathRoutesManifestPath); +} +/** + * Check if the project is using the next/image component based on the export-marker.json file. + * @param sourceDir location of the source directory + * @return true if the Next.js project uses the next/image component + */ +export async function usesNextImage(sourceDir: string, distDir: string): Promise { + const exportMarker = await readJSON(join(sourceDir, distDir, "export-marker.json")); + return exportMarker.isNextImageImported; +} + +/** + * Check if Next.js is forced to serve the source image as-is instead of being oprimized + * by setting `unoptimized: true` in next.config.js. + * https://nextjs.org/docs/api-reference/next/image#unoptimized + * + * @param sourceDir location of the source directory + * @param distDir location of the dist directory + * @return true if image optimization is disabled + */ +export async function hasUnoptimizedImage(sourceDir: string, distDir: string): Promise { + const imageManifest = await readJSON( + join(sourceDir, distDir, "images-manifest.json") + ); + return imageManifest.images.unoptimized; +} diff --git a/src/test/frameworks/next/utils.spec.ts b/src/test/frameworks/next/utils.spec.ts index 06c0e995b25..f8cecfc6870 100644 --- a/src/test/frameworks/next/utils.spec.ts +++ b/src/test/frameworks/next/utils.spec.ts @@ -1,4 +1,7 @@ import { expect } from "chai"; +import * as fs from "fs"; +import * as fsExtra from "fs-extra"; +import * as sinon from "sinon"; import { pathHasRegex, @@ -7,6 +10,9 @@ import { isRedirectSupportedByFirebase, isHeaderSupportedByFirebase, getNextjsRewritesToUse, + usesAppDirRouter, + usesNextImage, + hasUnoptimizedImage, } from "../../../frameworks/next/utils"; import { pathsAsGlobs, @@ -142,4 +148,78 @@ describe("Next.js utils", () => { expect(rewritesToUse).to.have.length(supportedRewritesArray.length); }); }); + + describe("usesAppDirRouter", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return false when app dir doesn't exist", () => { + sandbox.stub(fs, "existsSync").returns(false); + expect(usesAppDirRouter("")).to.be.false; + }); + + it("should return true when app dir does exist", () => { + sandbox.stub(fs, "existsSync").returns(true); + expect(usesAppDirRouter("")).to.be.true; + }); + }); + + describe("usesNextImage", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return true when export marker has isNextImageImported", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + isNextImageImported: true, + }); + expect(await usesNextImage("", "")).to.be.true; + }); + + it("should return false when export marker has !isNextImageImported", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + isNextImageImported: false, + }); + expect(await usesNextImage("", "")).to.be.false; + }); + }); + + describe("hasUnoptimizedImage", () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should return true when images manfiest indicates unoptimized", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + images: { unoptimized: true }, + }); + expect(await hasUnoptimizedImage("", "")).to.be.true; + }); + + it("should return true when images manfiest indicates !unoptimized", async () => { + sandbox.stub(fsExtra, "readJSON").resolves({ + images: { unoptimized: false }, + }); + expect(await hasUnoptimizedImage("", "")).to.be.false; + }); + }); });