Skip to content

Commit fdccc0f

Browse files
sosukesuzukilydell
andauthored
Add --cache CLI option (#12800)
* Install `file-entry-cache` and types * Implement `FormatResultsCache` * Add `--cache` and `--cache-location` option * Implement `--cache` and `--cache-location` * Add tests for `--cache` * Add tests for `--cache-location` * Avoid spellcheck error * Update snapshots for new options * Add `groupBy` for website * Use `fs.promises` instead of `fs/promises` * Use `rimraf` instead of `fs.rm` * Fix properties order of `options` * Install `find-cache-dir` and types * Change default cache file location to `node_modules/.cache/prettier` * Fix tests following to change default cache file location * Update snapshots for changing default cache file location * Set `os.tmpdir()` as a fallback for default cache dir * Add docs for `--cache` and `--cache-location` * Use `sdbm` instead of `node:crypto` * Add changelog * Fix `getHashOfOptions` * Remove `cache-location` * `prettiercache` -> `prettier-cache` * Remove cache file when run Prettier without `--cache` option * Implement `--cache-strategy` * Add tests for invalid cache strategy * Fix lint problems * Tweaks tests * Add tests for timestamp * Add tests for `--cache-strategy` * Add docs for cache-strategy * Address review * Fix `findCacheFile` * Use Set.prototype.has * Throw error with --stdin-filepath * Update snapshots * Fix error for cache and stdin * Use string constructor instead of toString * Update changelog * Use flag validation * Fix `cache-strategy` definition * Update docs * Mark highlihgt * Throw error for `--cache-strategy` without `--cache` * Update docs * Fix typo * Updates snapshots * Update docs/cli.md Co-authored-by: Simon Lydell <simon.lydell@gmail.com> * Fix error message * Remove `:::` syntax * Defaults `content` * Update docs * Fix by Prettier Co-authored-by: Simon Lydell <simon.lydell@gmail.com>
1 parent 97bab8f commit fdccc0f

24 files changed

Lines changed: 811 additions & 24 deletions

File tree

changelog_unreleased/cli/12800.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#### [HIGHLIGHT]Add `--cache` and `--cache-strategy` CLI option (#12800 by @sosukesuzuki)
2+
3+
Two new CLI options have been added for a caching system similar to [ESLint's one](https://eslint.org/docs/user-guide/command-line-interface#caching).
4+
5+
##### `--cache`
6+
7+
If this option is enabled, the following values are used as cache keys and the file is formatted only if one of them is changed.
8+
9+
- Prettier version
10+
- Options
11+
- Node.js version
12+
- (if `--cache-strategy` is `content`) content of the file
13+
- (if `--cache-strategy` is `metadata`) file metadata, such as timestamps
14+
15+
```bash
16+
prettier --write --cache src
17+
```
18+
19+
##### `--cache-strategy`
20+
21+
Strategy for the cache to use for detecting changed files. Can be either `metadata` or `content`. If no strategy is specified, `content` will be used.
22+
23+
```bash
24+
prettier --write --cache --cache-strategy metadata src
25+
```

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@
289289
"sandhose",
290290
"Sapegin",
291291
"sbdchd",
292+
"sdbm",
292293
"scandir",
293294
"Serializers",
294295
"setlocal",

docs/cli.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,35 @@ Prevent errors when pattern is unmatched.
204204
## `--no-plugin-search`
205205

206206
Disable plugin autoloading.
207+
208+
## `--cache`
209+
210+
If this option is enabled, the following values are used as cache keys and the file is formatted only if one of them is changed.
211+
212+
- Prettier version
213+
- Options
214+
- Node.js version
215+
- (if `--cache-strategy` is `metadata`) file metadata, such as timestamps
216+
- (if `--cache-strategy` is `content`) content of the file
217+
218+
```bash
219+
prettier --write --cache src
220+
```
221+
222+
Running Prettier without `--cache` will delete the cache.
223+
224+
Also, since the cache file is stored in `./node_modules/.cache/prettier/.prettier-cache`, so you can use `rm ./node_modules/.cache/prettier/.prettier-cache` to remove it manually.
225+
226+
> Plugins version and implementation are not used as cache keys. We recommend that you delete the cache when updating plugins.
227+
228+
## `--cache-strategy`
229+
230+
Strategy for the cache to use for detecting changed files. Can be either `metadata` or `content`.
231+
232+
In general, `metadata` is faster. However, `content` is useful for updating the timestamp without changing the file content. This can happen, for example, during git operations such as `git clone`, because it does not track file modification times.
233+
234+
If no strategy is specified, `content` will be used.
235+
236+
```bash
237+
prettier --write --cache --cache-strategy metadata src
238+
```

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
"esutils": "2.0.3",
4848
"fast-glob": "3.2.11",
4949
"fast-json-stable-stringify": "2.1.0",
50+
"file-entry-cache": "6.0.1",
51+
"find-cache-dir": "3.3.2",
5052
"find-parent-dir": "0.3.1",
5153
"flow-parser": "0.180.0",
5254
"get-stdin": "8.0.0",
@@ -79,6 +81,7 @@
7981
"remark-math": "3.0.1",
8082
"remark-parse": "8.0.3",
8183
"resolve": "1.22.0",
84+
"sdbm": "2.0.0",
8285
"semver": "7.3.7",
8386
"string-width": "5.0.1",
8487
"strip-ansi": "7.0.1",
@@ -96,6 +99,8 @@
9699
"@esbuild-plugins/node-modules-polyfill": "0.1.4",
97100
"@glimmer/reference": "0.84.2",
98101
"@types/estree": "0.0.51",
102+
"@types/file-entry-cache": "5.0.2",
103+
"@types/find-cache-dir": "3.2.1",
99104
"@types/jest": "27.4.1",
100105
"@typescript-eslint/eslint-plugin": "5.20.0",
101106
"babel-jest": "27.5.1",

scripts/vendors/vendor-meta.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"html-void-elements": "html-void-elements.json",
1010
"leven": "leven.js",
1111
"mem": "mem.js",
12+
"sdbm": "sdbm.js",
1213
"string-width": "string-width.js",
1314
"strip-ansi": "strip-ansi.js"
1415
},
@@ -503,6 +504,23 @@
503504
},
504505
"contributors": []
505506
},
507+
{
508+
"name": "sdbm",
509+
"maintainers": [],
510+
"version": "2.0.0",
511+
"description": "SDBM non-cryptographic hash function",
512+
"repository": "sindresorhus/sdbm",
513+
"homepage": null,
514+
"private": false,
515+
"license": "MIT",
516+
"licenseText": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
517+
"author": {
518+
"name": "Sindre Sorhus",
519+
"email": "sindresorhus@gmail.com",
520+
"url": "https://sindresorhus.com"
521+
},
522+
"contributors": []
523+
},
506524
{
507525
"name": "ansi-regex",
508526
"maintainers": [],

scripts/vendors/vendors.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const vendors = [
88
"html-void-elements",
99
"leven",
1010
"mem",
11+
"sdbm",
1112
"string-width",
1213
"strip-ansi",
1314
"tempy",

src/cli/constant.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@ const categoryOrder = [
7171
*/
7272
/* eslint sort-keys: "error" */
7373
const options = {
74+
cache: {
75+
default: false,
76+
description: "Only format changed files. Cannot use with --stdin-filepath.",
77+
type: "boolean",
78+
},
79+
"cache-strategy": {
80+
choices: [
81+
{
82+
description: "Use the file metadata such as timestamps as cache keys",
83+
value: "metadata",
84+
},
85+
{
86+
description: "Use the file content as cache keys",
87+
value: "content",
88+
},
89+
],
90+
description: "Strategy for the cache to use for detecting changed files.",
91+
type: "choice",
92+
},
7493
check: {
7594
alias: "c",
7695
category: coreOptions.CATEGORY_OUTPUT,

src/cli/expand-patterns.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use strict";
22

33
const path = require("path");
4-
const { promises: fs } = require("fs");
54
const fastGlob = require("fast-glob");
65

6+
const { statSafe } = require("./utils.js");
7+
78
/** @typedef {import('./context').Context} Context */
89

910
/**
@@ -173,22 +174,6 @@ function sortPaths(paths) {
173174
return paths.sort((a, b) => a.localeCompare(b));
174175
}
175176

176-
/**
177-
* Get stats of a given path.
178-
* @param {string} filePath The path to target file.
179-
* @returns {Promise<import('fs').Stats | undefined>} The stats.
180-
*/
181-
async function statSafe(filePath) {
182-
try {
183-
return await fs.stat(filePath);
184-
} catch (error) {
185-
/* istanbul ignore next */
186-
if (error.code !== "ENOENT") {
187-
throw error;
188-
}
189-
}
190-
}
191-
192177
/**
193178
* This function should be replaced with `fastGlob.escapePath` when these issues are fixed:
194179
* - https://github.com/mrmlnc/fast-glob/issues/261

src/cli/find-cache-file.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use strict";
2+
3+
const os = require("os");
4+
const path = require("path");
5+
const findCacheDir = require("find-cache-dir");
6+
7+
/**
8+
* Find default cache file (`./node_modules/.cache/prettier/.prettier-cache`) using https://github.com/avajs/find-cache-dir
9+
*
10+
* @returns {string}
11+
*/
12+
function findCacheFile() {
13+
const cacheDir =
14+
findCacheDir({ name: "prettier", create: true }) || os.tmpdir();
15+
const cacheFilePath = path.join(cacheDir, ".prettier-cache");
16+
return cacheFilePath;
17+
}
18+
19+
module.exports = findCacheFile;

src/cli/format-results-cache.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"use strict";
2+
3+
// Inspired by LintResultsCache from ESLint
4+
// https://github.com/eslint/eslint/blob/c2d0a830754b6099a3325e6d3348c3ba983a677a/lib/cli-engine/lint-result-cache.js
5+
6+
const fileEntryCache = require("file-entry-cache");
7+
const stringify = require("fast-json-stable-stringify");
8+
// eslint-disable-next-line no-restricted-modules
9+
const { version: prettierVersion } = require("../index.js");
10+
const { createHash } = require("./utils.js");
11+
12+
const optionsHashCache = new WeakMap();
13+
const nodeVersion = process && process.version;
14+
15+
/**
16+
* @param {*} options
17+
* @returns {string}
18+
*/
19+
function getHashOfOptions(options) {
20+
if (optionsHashCache.has(options)) {
21+
return optionsHashCache.get(options);
22+
}
23+
const hash = createHash(
24+
`${prettierVersion}_${nodeVersion}_${stringify(options)}`
25+
);
26+
optionsHashCache.set(options, hash);
27+
return hash;
28+
}
29+
30+
/**
31+
* @typedef {{ hashOfOptions?: string }} OurMeta
32+
* @typedef {import("file-entry-cache").FileDescriptor} FileDescriptor
33+
*
34+
* @param {import("file-entry-cache").FileDescriptor} fileDescriptor
35+
* @returns {FileDescriptor["meta"] & OurMeta}
36+
*/
37+
function getMetadataFromFileDescriptor(fileDescriptor) {
38+
return fileDescriptor.meta;
39+
}
40+
41+
class FormatResultsCache {
42+
/**
43+
* @param {string} cacheFileLocation The path of cache file location. (default: `node_modules/.cache/prettier/prettier-cache`)
44+
* @param {string} cacheStrategy
45+
*/
46+
constructor(cacheFileLocation, cacheStrategy) {
47+
const useChecksum = cacheStrategy === "content";
48+
49+
this.cacheFileLocation = cacheFileLocation;
50+
this.fileEntryCache = fileEntryCache.create(
51+
/* cacheId */ cacheFileLocation,
52+
/* directory */ undefined,
53+
useChecksum
54+
);
55+
}
56+
57+
/**
58+
* @param {string} filePath
59+
* @param {any} options
60+
*/
61+
existsAvailableFormatResultsCache(filePath, options) {
62+
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
63+
const hashOfOptions = getHashOfOptions(options);
64+
const meta = getMetadataFromFileDescriptor(fileDescriptor);
65+
const changed =
66+
fileDescriptor.changed || meta.hashOfOptions !== hashOfOptions;
67+
68+
if (fileDescriptor.notFound) {
69+
return false;
70+
}
71+
72+
if (changed) {
73+
return false;
74+
}
75+
76+
return true;
77+
}
78+
79+
/**
80+
* @param {string} filePath
81+
* @param {any} options
82+
*/
83+
setFormatResultsCache(filePath, options) {
84+
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
85+
const meta = getMetadataFromFileDescriptor(fileDescriptor);
86+
if (fileDescriptor && !fileDescriptor.notFound) {
87+
meta.hashOfOptions = getHashOfOptions(options);
88+
}
89+
}
90+
91+
reconcile() {
92+
this.fileEntryCache.reconcile();
93+
}
94+
}
95+
96+
module.exports = FormatResultsCache;

0 commit comments

Comments
 (0)