diff --git a/changelog_unreleased/cli/13019.md b/changelog_unreleased/cli/13019.md new file mode 100644 index 000000000..bb4f81b2b --- /dev/null +++ b/changelog_unreleased/cli/13019.md @@ -0,0 +1,9 @@ +#### Add `--cache-location` option (#13019 by @sosukesuzuki) + +Path to the cache file location used by `--cache` flag. If you don't explicit `--cache-location`, Prettier saves cache file at `./node_modules/.cache/prettier/.prettier-cache`. + +If a file path is passed, that file is used as the cache file. + +```bash +prettier --write --cache --cache-location=my_cache_file src +``` diff --git a/docs/cli.md b/docs/cli.md index af3afef5f..b4d3981e9 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -225,6 +225,16 @@ Also, since the cache file is stored in `./node_modules/.cache/prettier/.prettie > Plugins version and implementation are not used as cache keys. We recommend that you delete the cache when updating plugins. +## `--cache-location` + +Path to the cache file location used by `--cache` flag. If you don't explicit `--cache-location`, Prettier saves cache file at `./node_modules/.cache/prettier/.prettier-cache`. + +If a file path is passed, that file is used as the cache file. + +```bash +prettier --write --cache --cache-location=my_cache_file src +``` + ## `--cache-strategy` Strategy for the cache to use for detecting changed files. Can be either `metadata` or `content`. diff --git a/src/cli/constant.js b/src/cli/constant.js index c7cc8958d..33a64f3d1 100644 --- a/src/cli/constant.js +++ b/src/cli/constant.js @@ -76,6 +76,10 @@ const options = { description: "Only format changed files. Cannot use with --stdin-filepath.", type: "boolean", }, + "cache-location": { + description: "Path to the cache file.", + type: "path", + }, "cache-strategy": { choices: [ { diff --git a/src/cli/find-cache-file.js b/src/cli/find-cache-file.js index c8960ead4..cc6ebee79 100644 --- a/src/cli/find-cache-file.js +++ b/src/cli/find-cache-file.js @@ -1,19 +1,51 @@ "use strict"; +const fs = require("fs").promises; const os = require("os"); const path = require("path"); const findCacheDir = require("find-cache-dir"); +const { statSafe, isJson } = require("./utils.js"); /** * Find default cache file (`./node_modules/.cache/prettier/.prettier-cache`) using https://github.com/avajs/find-cache-dir - * - * @returns {string} */ -function findCacheFile() { +function findDefaultCacheFile() { const cacheDir = findCacheDir({ name: "prettier", create: true }) || os.tmpdir(); const cacheFilePath = path.join(cacheDir, ".prettier-cache"); return cacheFilePath; } +async function findCacheFileFromOption(cacheLocation) { + const cacheFile = path.resolve(cacheLocation); + + const stat = await statSafe(cacheFile); + if (stat) { + if (stat.isDirectory()) { + throw new Error( + `Resolved --cache-location '${cacheFile}' is a directory` + ); + } + + const data = await fs.readFile(cacheFile, "utf8"); + if (!isJson(data)) { + throw new Error(`'${cacheFile}' isn't a valid JSON file`); + } + } + + return cacheFile; +} + +/** + * @param {string | undefined} cacheLocation + * @returns {Promise} + */ +async function findCacheFile(cacheLocation) { + if (!cacheLocation) { + return findDefaultCacheFile(); + } + const cacheFile = await findCacheFileFromOption(cacheLocation); + return cacheFile; +} + module.exports = findCacheFile; diff --git a/src/cli/format.js b/src/cli/format.js index 8f5e4867f..6d91998ac 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -299,7 +299,7 @@ async function formatFiles(context) { } let formatResultsCache; - const cacheFilePath = findCacheFile(); + const cacheFilePath = await findCacheFile(context.argv.cacheLocation); if (context.argv.cache) { formatResultsCache = new FormatResultsCache( cacheFilePath, @@ -312,9 +312,11 @@ async function formatFiles(context) { ); process.exit(2); } - const stat = await statSafe(cacheFilePath); - if (stat) { - await fs.unlink(cacheFilePath); + if (!context.argv.cacheLocation) { + const stat = await statSafe(cacheFilePath); + if (stat) { + await fs.unlink(cacheFilePath); + } } } diff --git a/src/cli/utils.js b/src/cli/utils.js index a846c185c..d13b5efcf 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -67,4 +67,17 @@ async function statSafe(filePath) { } } -module.exports = { printToScreen, groupBy, pick, createHash, statSafe }; +/** + * @param {string} value + * @returns {boolean} + */ +function isJson(value) { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +module.exports = { printToScreen, groupBy, pick, createHash, statSafe, isJson }; diff --git a/tests/integration/__tests__/__snapshots__/early-exit.js.snap b/tests/integration/__tests__/__snapshots__/early-exit.js.snap index 63f00b025..d7886195e 100644 --- a/tests/integration/__tests__/__snapshots__/early-exit.js.snap +++ b/tests/integration/__tests__/__snapshots__/early-exit.js.snap @@ -141,6 +141,7 @@ Other options: --cache Only format changed files. Cannot use with --stdin-filepath. Defaults to false. + --cache-location Path to the cache file. --cache-strategy Strategy for the cache to use for detecting changed files. --no-color Do not colorize error messages. @@ -315,6 +316,7 @@ Other options: --cache Only format changed files. Cannot use with --stdin-filepath. Defaults to false. + --cache-location Path to the cache file. --cache-strategy Strategy for the cache to use for detecting changed files. --no-color Do not colorize error messages. diff --git a/tests/integration/__tests__/__snapshots__/help-options.js.snap b/tests/integration/__tests__/__snapshots__/help-options.js.snap index 009f516d3..5574c326f 100644 --- a/tests/integration/__tests__/__snapshots__/help-options.js.snap +++ b/tests/integration/__tests__/__snapshots__/help-options.js.snap @@ -57,6 +57,17 @@ Default: false exports[`show detailed usage with --help cache (write) 1`] = `[]`; +exports[`show detailed usage with --help cache-location (stderr) 1`] = `""`; + +exports[`show detailed usage with --help cache-location (stdout) 1`] = ` +"--cache-location + + Path to the cache file. +" +`; + +exports[`show detailed usage with --help cache-location (write) 1`] = `[]`; + exports[`show detailed usage with --help cache-strategy (stderr) 1`] = `""`; exports[`show detailed usage with --help cache-strategy (stdout) 1`] = ` diff --git a/tests/integration/__tests__/cache.js b/tests/integration/__tests__/cache.js index 863cd2901..a15618395 100644 --- a/tests/integration/__tests__/cache.js +++ b/tests/integration/__tests__/cache.js @@ -18,6 +18,9 @@ describe("--cache option", () => { "node_modules/.cache/prettier/.prettier-cache" ); + const nonDefaultCacheFileName = ".non-default-cache-file"; + const nonDefaultCacheFilePath = path.join(dir, nonDefaultCacheFileName); + let contentA; let contentB; @@ -28,6 +31,7 @@ describe("--cache option", () => { afterEach(async () => { rimraf.sync(path.join(dir, "node_modules")); + rimraf.sync(nonDefaultCacheFilePath); await fs.writeFile(path.join(dir, "a.js"), contentA); await fs.writeFile(path.join(dir, "b.js"), contentB); }); @@ -68,6 +72,20 @@ describe("--cache option", () => { ); }); + it("throws error when `--cache-location` is a directory.", async () => { + const { stderr } = await runPrettier(dir, [ + "foo.js", + "--cache", + "--cache-location", + "dir", + ]); + expect(stripAnsi(stderr.trim())).toEqual( + 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( @@ -370,4 +388,83 @@ describe("--cache option", () => { await expect(fs.stat(defaultCacheFile)).rejects.toThrowError(); }); }); + + 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 runPrettier(dir, [ + "--cache", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + await expect(fs.stat(defaultCacheFile)).rejects.toHaveProperty( + "code", + "ENOENT" + ); + }); + + it("throws error for invalid JSON file", async () => { + const { stderr } = await runPrettier(dir, [ + "--cache", + "--cache-location", + "a.js", + ".", + ]); + expect(stripAnsi(stderr).trim()).toEqual( + 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 runPrettier(dir, [ + "--cache", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + await expect( + fs.stat(nonDefaultCacheFilePath) + ).resolves.not.toThrowError(); + }); + + it("does'nt format when cache is available", async () => { + const { stdout: firstStdout } = await runPrettier(dir, [ + "--cache", + "--write", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + expect(stripAnsi(firstStdout).split("\n").filter(Boolean)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^a\.js .+ms$/), + expect.stringMatching(/^b\.js .+ms$/), + ]) + ); + + const { stdout: secondStdout } = await runPrettier(dir, [ + "--cache", + "--write", + "--cache-location", + nonDefaultCacheFileName, + ".", + ]); + expect(stripAnsi(secondStdout).split("\n").filter(Boolean)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^a\.js .+ms \(cached\)$/), + expect.stringMatching(/^b\.js .+ms \(cached\)$/), + ]) + ); + }); + }); + }); }); diff --git a/tests/integration/cli/cache/dir/.gitkeep b/tests/integration/cli/cache/dir/.gitkeep new file mode 100644 index 000000000..e69de29bb