import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; function resolveDir(dir) { return fileURLToPath(new URL(`../${dir}/`, import.meta.url)); } const runCliWithoutGitignore = (dir, args, options) => runCli(dir, [...args, "--ignore-path", ".prettierignore"], options); describe("--cache option", () => { const dir = resolveDir("cli/cache"); const defaultCacheFile = path.join( dir, "node_modules/.cache/prettier/.prettier-cache", ); const nonDefaultCacheFileName = ".non-default-cache-file"; const directoryNameAsCacheFile = "directory-as-cache-file"; const nonDefaultCacheFilePath = path.join(dir, nonDefaultCacheFileName); const prettierIgnorePath = path.join(dir, ".prettierignore"); const contentA = `function a() { console.log("this is a.js") } `; const contentB = `function b() { console.log("this is b.js"); } `; const clean = async () => { await fs.rm(path.join(dir, directoryNameAsCacheFile), { force: true, recursive: true, }); await fs.rm(nonDefaultCacheFilePath, { force: true }); await fs.rm(path.join(dir, "a.js"), { force: true }); await fs.rm(path.join(dir, "b.js"), { force: true }); }; beforeAll(async () => { await clean(); await fs.mkdir(path.join(dir, directoryNameAsCacheFile)); await fs.writeFile(path.join(dir, "a.js"), contentA); await fs.writeFile(path.join(dir, "b.js"), contentB); }); afterEach(async () => { await fs.rm(path.join(dir, "node_modules"), { force: true, recursive: true, }); await fs.rm(nonDefaultCacheFilePath, { force: true }); await fs.writeFile(path.join(dir, "a.js"), contentA); await fs.writeFile(path.join(dir, "b.js"), contentB); }); afterAll(clean); it("creates cache file with --cache", async () => { await runCliWithoutGitignore(dir, ["--cache", "a.js"]); await expect(fs.stat(defaultCacheFile)).resolves.not.toThrow(); }); it("doesn't create cache directory without --cache", async () => { await runCliWithoutGitignore(dir, ["a.js"]); await expect( fs.stat(path.join(dir, "node_modules/.cache")), ).rejects.toHaveProperty("code", "ENOENT"); }); it("throw error when cache-strategy is invalid", async () => { const { stderr } = await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "invalid", "*.js", ]); expect(stderr.trim()).toBe( '[error] Invalid --cache-strategy value. Expected "content" or "metadata", but received "invalid".', ); }); it("throws error when use with --stdin-filepath", async () => { const { stderr } = await runCliWithoutGitignore( dir, ["--cache", "--stdin-filepath", "foo.js"], { input: "const a = a;" }, ); expect(stderr.trim()).toBe( "[error] `--cache` cannot be used when formatting stdin.", ); }); it("throws error when use `--cache-strategy` without `--cache`.", async () => { const { stderr } = await runCliWithoutGitignore( dir, ["foo.js", "--cache-strategy", "content"], { input: "const a = a;", }, ); expect(stderr.trim()).toBe( "[error] `--cache-strategy` cannot be used without `--cache`.", ); }); it("throws error when `--cache-location` is a directory.", async () => { const { stderr } = await runCliWithoutGitignore(dir, [ "foo.js", "--cache", "--cache-location", directoryNameAsCacheFile, ]); expect(stderr.trim()).toStrictEqual( expect.stringMatching( /\[error\] Resolved --cache-location '.+' is a directory/, ), ); }); describe("--cache-strategy metadata", () => { it("creates default cache file named `node_modules/.cache/prettier/.prettier-cache`", async () => { await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( "code", "ENOENT", ); await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "metadata", "*.js", ]); await expect(fs.stat(defaultCacheFile)).resolves.not.toThrow(); }); it("doesn't format when cache is available", async () => { const { stdout: firstStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when a file has been updated.", async () => { const cliArguments = [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); // Update `a.js` await fs.writeFile(path.join(dir, "a.js"), "const a = `a`;"); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( // the cache of `b.js` is only available. expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when timestamp has been updated", async () => { const cliArguments = [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); // update timestamp const time = new Date(); await fs.utimes(path.join(dir, "a.js"), time, time); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( // the cache of `b.js` is only available. expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when options has been updated.", async () => { const { stdout: firstStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "metadata", "--write", "--trailing-comma", "all", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); }); it("re-formats after execution without write.", async () => { await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "metadata", "*.js", ]); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--write", "--cache", "--cache-strategy", "metadata", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when multiple cached files are updated.", async () => { const cliArguments = [ "--write", "--cache", "--cache-strategy", "metadata", "*.js", ]; await runCliWithoutGitignore(dir, cliArguments); // Update `a.js` to unformatted await fs.writeFile(path.join(dir, "a.js"), "const a = `a`; "); // Update `b.js` but still formatted const time = new Date(); await fs.utimes(path.join(dir, "b.js"), time, time); await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "metadata", "*.js", ]); const { stdout: thirdStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(thirdStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("doesn't cache files when write error.", async () => { const { stdout: firstStdout, stderr: firstStderr, status: firstStatus, } = await runCliWithoutGitignore( dir, ["--write", "--cache", "--cache-strategy", "metadata", "*.js"], { mockWriteFileErrors: { "a.js": "EACCES: permission denied.", }, }, ); expect(firstStatus).toBe(2); expect(firstStderr).toBe( '[error] Unable to write file "a.js":\n' + "[error] EACCES: permission denied. (mocked error)", ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--list-different", "--cache", "--cache-strategy", "metadata", "*.js", ]); expect(secondStdout).toBe("a.js"); }); it("removes cache file when run Prettier without `--cache` option", async () => { await runCliWithoutGitignore(dir, [ "--cache", "--write", "--cache-strategy", "metadata", "*.js", ]); await expect(fs.stat(defaultCacheFile)).resolves.not.toThrow(); await runCliWithoutGitignore(dir, ["--write", "*.js"]); await expect(fs.stat(defaultCacheFile)).rejects.toThrow(); }); it("invalidates cache when previously ignored files are restored", async () => { await runCliWithoutGitignore(dir, [ "--write", "--cache", "--cache-strategy", "metadata", "*.js", ]); await fs.writeFile(prettierIgnorePath, ["b.js"].join("\n")); await fs.writeFile( path.join(dir, "b.js"), "const b = 'this is new content';", ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--write", "--cache", "--cache-strategy", "metadata", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/u), ]), ); await fs.rm(prettierIgnorePath, { force: true }); const { stdout: thirdStdout } = await runCliWithoutGitignore(dir, [ "--write", "--cache", "--cache-strategy", "metadata", "*.js", ]); expect(thirdStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/u), expect.stringMatching(/^b\.js .+ms$/u), ]), ); }); }); describe("--cache-strategy content", () => { it("creates default cache file named `node_modules/.cache/prettier/.prettier-cache`", async () => { await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( "code", "ENOENT", ); await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "content", "*.js", ]); await expect(fs.stat(defaultCacheFile)).resolves.not.toThrow(); }); it("doesn't format when cache is available", async () => { const cliArguments = [ "--cache", "--cache-strategy", "content", "--write", "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when a file has been updated.", async () => { const cliArguments = [ "--cache", "--cache-strategy", "content", "--write", "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); // Update `a.js` await fs.writeFile(path.join(dir, "a.js"), "const a = `a`;"); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( // the cache of `b.js` is only available. expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("doesn't when timestamp has been updated", async () => { const cliArguments = [ "--cache", "--cache-strategy", "content", "--write", "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); // update timestamp const time = new Date(); await fs.utimes(path.join(dir, "a.js"), time, time); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when content changes but file size stays the same", async () => { const cliArguments = [ "--cache", "--cache-strategy", "content", "--write", "*.js", ]; // First run — populates cache await runCliWithoutGitignore(dir, cliArguments); // Replace with different content of the same byte length const sameSizeContent = contentA.replace( 'console.log("this is a.js")', 'console.log("that is a.js")', ); await fs.writeFile(path.join(dir, "a.js"), sameSizeContent); // Second run — must detect the content change const { stdout } = await runCliWithoutGitignore(dir, cliArguments); expect(stdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when options has been updated.", async () => { const { stdout: firstStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "content", "--write", "*.js", ]); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--cache", "--write", "--cache-strategy", "content", "--trailing-comma", "all", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); }); it("re-formats after execution without write.", async () => { await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "content", "*.js", ]); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--write", "--cache", "--cache-strategy", "content", "*.js", ]); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("re-formats when multiple cached files are updated.", async () => { const cliArguments = [ "--write", "--cache", "--cache-strategy", "content", "*.js", ]; await runCliWithoutGitignore(dir, cliArguments); // Update `a.js` to unformatted await fs.writeFile(path.join(dir, "a.js"), "const a = `a`; "); // Update `b.js` but still formatted const time = new Date(); await fs.utimes(path.join(dir, "b.js"), time, time); await runCliWithoutGitignore(dir, [ "--cache", "--cache-strategy", "content", "*.js", ]); const { stdout: thirdStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(thirdStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); it("doesn't cache files when write error.", async () => { const { stdout: firstStdout, stderr: firstStderr, status: firstStatus, } = await runCliWithoutGitignore( dir, ["--write", "--cache", "--cache-strategy", "content", "*.js"], { mockWriteFileErrors: { "a.js": "EACCES: permission denied.", }, }, ); expect(firstStatus).toBe(2); expect(firstStderr).toBe( '[error] Unable to write file "a.js":\n' + "[error] EACCES: permission denied. (mocked error)", ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore(dir, [ "--list-different", "--cache", "--cache-strategy", "content", "*.js", ]); expect(secondStdout).toBe("a.js"); }); it("removes cache file when run Prettier without `--cache` option", async () => { await runCliWithoutGitignore(dir, ["--cache", "--write", "*.js"]); await expect(fs.stat(defaultCacheFile)).resolves.not.toThrow(); await runCliWithoutGitignore(dir, ["--write", "*.js"]); await expect(fs.stat(defaultCacheFile)).rejects.toThrow(); }); }); describe("--cache-location", () => { it("doesn't create default cache file when `--cache-location` exists", async () => { await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( "code", "ENOENT", ); await runCliWithoutGitignore(dir, [ "--cache", "--cache-location", nonDefaultCacheFileName, "*.js", ]); await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( "code", "ENOENT", ); }); it("throws error for invalid JSON file", async () => { const { stderr } = await runCliWithoutGitignore(dir, [ "--cache", "--cache-location", "a.js", "*.js", ]); expect(stderr.trim()).toStrictEqual( expect.stringMatching(/\[error\] '.+' isn't a valid JSON file/), ); }); describe("file", () => { it("creates the cache file at location specified by `--cache-location`", async () => { await expect(fs.stat(nonDefaultCacheFilePath)).rejects.toHaveProperty( "code", "ENOENT", ); await runCliWithoutGitignore(dir, [ "--cache", "--cache-location", nonDefaultCacheFileName, "*.js", ]); await expect(fs.stat(nonDefaultCacheFilePath)).resolves.not.toThrow(); }); it("doesn't format when cache is available", async () => { const cliArguments = [ "--cache", "--write", "--cache-location", nonDefaultCacheFileName, "*.js", ]; const { stdout: firstStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(firstStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms$/), expect.stringMatching(/^b\.js .+ms \(unchanged\)$/), ]), ); const { stdout: secondStdout } = await runCliWithoutGitignore( dir, cliArguments, ); expect(secondStdout.split("\n")).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/^a\.js .+ms \(unchanged\) \(cached\)$/), expect.stringMatching(/^b\.js .+ms \(unchanged\) \(cached\)$/), ]), ); }); }); }); });