Skip to content

Commit

Permalink
Add --cache CLI option (prettier#12800)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
2 people authored and medikoo committed Jan 4, 2024
1 parent 665841b commit b74a3df
Show file tree
Hide file tree
Showing 24 changed files with 811 additions and 24 deletions.
25 changes: 25 additions & 0 deletions changelog_unreleased/cli/12800.md
@@ -0,0 +1,25 @@
#### [HIGHLIGHT]Add `--cache` and `--cache-strategy` CLI option (#12800 by @sosukesuzuki)

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).

##### `--cache`

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.

- Prettier version
- Options
- Node.js version
- (if `--cache-strategy` is `content`) content of the file
- (if `--cache-strategy` is `metadata`) file metadata, such as timestamps

```bash
prettier --write --cache src
```

##### `--cache-strategy`

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.

```bash
prettier --write --cache --cache-strategy metadata src
```
1 change: 1 addition & 0 deletions cspell.json
Expand Up @@ -289,6 +289,7 @@
"sandhose",
"Sapegin",
"sbdchd",
"sdbm",
"scandir",
"Serializers",
"setlocal",
Expand Down
32 changes: 32 additions & 0 deletions docs/cli.md
Expand Up @@ -204,3 +204,35 @@ Prevent errors when pattern is unmatched.
## `--no-plugin-search`

Disable plugin autoloading.

## `--cache`

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.

- Prettier version
- Options
- Node.js version
- (if `--cache-strategy` is `metadata`) file metadata, such as timestamps
- (if `--cache-strategy` is `content`) content of the file

```bash
prettier --write --cache src
```

Running Prettier without `--cache` will delete the cache.

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.

> Plugins version and implementation are not used as cache keys. We recommend that you delete the cache when updating plugins.
## `--cache-strategy`

Strategy for the cache to use for detecting changed files. Can be either `metadata` or `content`.

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.

If no strategy is specified, `content` will be used.

```bash
prettier --write --cache --cache-strategy metadata src
```
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -47,6 +47,8 @@
"esutils": "2.0.3",
"fast-glob": "3.2.11",
"fast-json-stable-stringify": "2.1.0",
"file-entry-cache": "6.0.1",
"find-cache-dir": "3.3.2",
"find-parent-dir": "0.3.1",
"flow-parser": "0.180.0",
"get-stdin": "8.0.0",
Expand Down Expand Up @@ -79,6 +81,7 @@
"remark-math": "3.0.1",
"remark-parse": "8.0.3",
"resolve": "1.22.0",
"sdbm": "2.0.0",
"semver": "7.3.7",
"string-width": "5.0.1",
"strip-ansi": "7.0.1",
Expand All @@ -96,6 +99,8 @@
"@esbuild-plugins/node-modules-polyfill": "0.1.4",
"@glimmer/reference": "0.84.2",
"@types/estree": "0.0.51",
"@types/file-entry-cache": "5.0.2",
"@types/find-cache-dir": "3.2.1",
"@types/jest": "27.4.1",
"@typescript-eslint/eslint-plugin": "5.20.0",
"babel-jest": "27.5.1",
Expand Down
18 changes: 18 additions & 0 deletions scripts/vendors/vendor-meta.json
Expand Up @@ -9,6 +9,7 @@
"html-void-elements": "html-void-elements.json",
"leven": "leven.js",
"mem": "mem.js",
"sdbm": "sdbm.js",
"string-width": "string-width.js",
"strip-ansi": "strip-ansi.js"
},
Expand Down Expand Up @@ -503,6 +504,23 @@
},
"contributors": []
},
{
"name": "sdbm",
"maintainers": [],
"version": "2.0.0",
"description": "SDBM non-cryptographic hash function",
"repository": "sindresorhus/sdbm",
"homepage": null,
"private": false,
"license": "MIT",
"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",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"contributors": []
},
{
"name": "ansi-regex",
"maintainers": [],
Expand Down
1 change: 1 addition & 0 deletions scripts/vendors/vendors.mjs
Expand Up @@ -8,6 +8,7 @@ const vendors = [
"html-void-elements",
"leven",
"mem",
"sdbm",
"string-width",
"strip-ansi",
"tempy",
Expand Down
19 changes: 19 additions & 0 deletions src/cli/constant.js
Expand Up @@ -71,6 +71,25 @@ const categoryOrder = [
*/
/* eslint sort-keys: "error" */
const options = {
cache: {
default: false,
description: "Only format changed files. Cannot use with --stdin-filepath.",
type: "boolean",
},
"cache-strategy": {
choices: [
{
description: "Use the file metadata such as timestamps as cache keys",
value: "metadata",
},
{
description: "Use the file content as cache keys",
value: "content",
},
],
description: "Strategy for the cache to use for detecting changed files.",
type: "choice",
},
check: {
alias: "c",
category: coreOptions.CATEGORY_OUTPUT,
Expand Down
19 changes: 2 additions & 17 deletions src/cli/expand-patterns.js
@@ -1,9 +1,10 @@
"use strict";

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

const { statSafe } = require("./utils.js");

/** @typedef {import('./context').Context} Context */

/**
Expand Down Expand Up @@ -173,22 +174,6 @@ function sortPaths(paths) {
return paths.sort((a, b) => a.localeCompare(b));
}

/**
* Get stats of a given path.
* @param {string} filePath The path to target file.
* @returns {Promise<import('fs').Stats | undefined>} The stats.
*/
async function statSafe(filePath) {
try {
return await fs.stat(filePath);
} catch (error) {
/* istanbul ignore next */
if (error.code !== "ENOENT") {
throw error;
}
}
}

/**
* This function should be replaced with `fastGlob.escapePath` when these issues are fixed:
* - https://github.com/mrmlnc/fast-glob/issues/261
Expand Down
19 changes: 19 additions & 0 deletions src/cli/find-cache-file.js
@@ -0,0 +1,19 @@
"use strict";

const os = require("os");
const path = require("path");
const findCacheDir = require("find-cache-dir");

/**
* Find default cache file (`./node_modules/.cache/prettier/.prettier-cache`) using https://github.com/avajs/find-cache-dir
*
* @returns {string}
*/
function findCacheFile() {
const cacheDir =
findCacheDir({ name: "prettier", create: true }) || os.tmpdir();
const cacheFilePath = path.join(cacheDir, ".prettier-cache");
return cacheFilePath;
}

module.exports = findCacheFile;
96 changes: 96 additions & 0 deletions src/cli/format-results-cache.js
@@ -0,0 +1,96 @@
"use strict";

// Inspired by LintResultsCache from ESLint
// https://github.com/eslint/eslint/blob/c2d0a830754b6099a3325e6d3348c3ba983a677a/lib/cli-engine/lint-result-cache.js

const fileEntryCache = require("file-entry-cache");
const stringify = require("fast-json-stable-stringify");
// eslint-disable-next-line no-restricted-modules
const { version: prettierVersion } = require("../index.js");
const { createHash } = require("./utils.js");

const optionsHashCache = new WeakMap();
const nodeVersion = process && process.version;

/**
* @param {*} options
* @returns {string}
*/
function getHashOfOptions(options) {
if (optionsHashCache.has(options)) {
return optionsHashCache.get(options);
}
const hash = createHash(
`${prettierVersion}_${nodeVersion}_${stringify(options)}`
);
optionsHashCache.set(options, hash);
return hash;
}

/**
* @typedef {{ hashOfOptions?: string }} OurMeta
* @typedef {import("file-entry-cache").FileDescriptor} FileDescriptor
*
* @param {import("file-entry-cache").FileDescriptor} fileDescriptor
* @returns {FileDescriptor["meta"] & OurMeta}
*/
function getMetadataFromFileDescriptor(fileDescriptor) {
return fileDescriptor.meta;
}

class FormatResultsCache {
/**
* @param {string} cacheFileLocation The path of cache file location. (default: `node_modules/.cache/prettier/prettier-cache`)
* @param {string} cacheStrategy
*/
constructor(cacheFileLocation, cacheStrategy) {
const useChecksum = cacheStrategy === "content";

this.cacheFileLocation = cacheFileLocation;
this.fileEntryCache = fileEntryCache.create(
/* cacheId */ cacheFileLocation,
/* directory */ undefined,
useChecksum
);
}

/**
* @param {string} filePath
* @param {any} options
*/
existsAvailableFormatResultsCache(filePath, options) {
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const hashOfOptions = getHashOfOptions(options);
const meta = getMetadataFromFileDescriptor(fileDescriptor);
const changed =
fileDescriptor.changed || meta.hashOfOptions !== hashOfOptions;

if (fileDescriptor.notFound) {
return false;
}

if (changed) {
return false;
}

return true;
}

/**
* @param {string} filePath
* @param {any} options
*/
setFormatResultsCache(filePath, options) {
const fileDescriptor = this.fileEntryCache.getFileDescriptor(filePath);
const meta = getMetadataFromFileDescriptor(fileDescriptor);
if (fileDescriptor && !fileDescriptor.notFound) {
meta.hashOfOptions = getHashOfOptions(options);
}
}

reconcile() {
this.fileEntryCache.reconcile();
}
}

module.exports = FormatResultsCache;

0 comments on commit b74a3df

Please sign in to comment.