/** * The image optimizer stays off while nothing uses it. * * `next.config.ts` sets `images.unoptimized`, which makes Next answer * `/_next/image` with 404 before it reads a parameter, so the route that * fetches, decodes and re-encodes images is not reachable at all. Two ways * to undo that quietly, and this pins both: * * 1. A component imports `next/image` (or `next/legacy/image`) while the * optimizer is off. It would render, unoptimized, and the next person * to notice the missing optimization would switch the optimizer back on * without the review this flag deserves. Using `next/image` means * revisiting this decision, so the import fails here first. * 2. The optimizer comes back on with `dangerouslyAllowSVG`. An SVG is a * document, not a picture, and the framework's own advisories have been * about exactly that input. Re-enabling the optimizer is allowed; doing * it with SVG input allowed is not. */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { requireFloor, stripComments, walkSourceFiles, } from "./helpers/source-files"; const ROOT = process.cwd(); const SRC = join(ROOT, "src"); type ImagesConfig = { unoptimized?: boolean; dangerouslyAllowSVG?: boolean; }; async function imagesConfig(): Promise { const mod = (await import("../../next.config")) as { default: { images?: ImagesConfig }; }; return mod.default.images ?? {}; } // Any import of the module: static, dynamic, `require`, side effect or // re-export. Two literals with one shape rather than one built from a string, // so nothing has to be escaped into a pattern. const NEXT_IMAGE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)["']next\/(?:legacy\/)?image["']/m; const NEXT_LINK_IMPORT = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)["']next\/link["']/m; /** Every non-generated, non-test source file under `src/` matching `re`. */ function importers(re: RegExp): string[] { return walkSourceFiles(SRC, { floor: 3000 }) .filter((rel) => !rel.startsWith("generated/")) .filter((rel) => !rel.includes("__tests__/")) .filter((rel) => re.test(stripComments(readFileSync(join(SRC, rel), "utf8"))), ); } describe("next/image optimizer", () => { it("the import matcher recognises every form of the import", () => { // The sweep below passes when it matches nothing, so the matcher has to // be shown to match before an empty result means anything. for (const line of [ 'import Image from "next/image";', "import Image from 'next/legacy/image';", 'const Image = await import("next/image");', 'const Image = require("next/image");', 'import "next/image";', 'export { default } from "next/image";', ]) { expect(NEXT_IMAGE_IMPORT.test(line), line).toBe(true); } for (const line of [ "// `next/image` is wrong here", 'const path = "/_next/image?url=x";', 'import { foo } from "next/image-extra";', ]) { expect(NEXT_IMAGE_IMPORT.test(stripComments(line)), line).toBe(false); } }); it("the same sweep finds the imports of a sibling module", () => { // An empty result for `next/image` is only worth something if the walk, // the comment stripping and the matcher together do find real imports. // `next/link` is imported the same way all over the tree. requireFloor("next/link importers", importers(NEXT_LINK_IMPORT), 100); }); it("is off, and then nothing imports next/image", async () => { const images = await imagesConfig(); if (images.unoptimized !== true) return; expect(importers(NEXT_IMAGE_IMPORT)).toEqual([]); }); it("is never on with SVG input allowed", async () => { const images = await imagesConfig(); if (images.unoptimized === true) return; expect(images.dangerouslyAllowSVG ?? false).toBe(false); }); it("is off today", async () => { // The two cases above each skip on the other branch. This pins which // branch the tree is on, so flipping it is a visible change to this // file and not a silent switch between the two. expect((await imagesConfig()).unoptimized).toBe(true); }); });