Skip to content

Commit

Permalink
Add sharp library to Next.js build (#5238)
Browse files Browse the repository at this point in the history
Checking for /app dir and nextImage to add the sharp library to the Next.js build
  • Loading branch information
cfofiu committed Dec 12, 2022
1 parent 482f53a commit 270f419
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -0,0 +1 @@
- Add sharp NPM module to Cloud Functions when using Next.js Image Optimization
13 changes: 13 additions & 0 deletions src/frameworks/next/index.ts
Expand Up @@ -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",
Expand Down Expand Up @@ -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" };
Expand Down
14 changes: 14 additions & 0 deletions src/frameworks/next/interfaces.ts
Expand Up @@ -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;
};
}
40 changes: 39 additions & 1 deletion 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.
Expand Down Expand Up @@ -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<boolean> {
const exportMarker = await readJSON<ExportMarker>(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<boolean> {
const imageManifest = await readJSON<ImageManifest>(
join(sourceDir, distDir, "images-manifest.json")
);
return imageManifest.images.unoptimized;
}
80 changes: 80 additions & 0 deletions 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,
Expand All @@ -7,6 +10,9 @@ import {
isRedirectSupportedByFirebase,
isHeaderSupportedByFirebase,
getNextjsRewritesToUse,
usesAppDirRouter,
usesNextImage,
hasUnoptimizedImage,
} from "../../../frameworks/next/utils";
import {
pathsAsGlobs,
Expand Down Expand Up @@ -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;
});
});
});

0 comments on commit 270f419

Please sign in to comment.