Skip to content

Commit

Permalink
Backport cache feature to 2.6.2
Browse files Browse the repository at this point in the history
  • Loading branch information
bpaquet committed Jun 3, 2022
1 parent d6f82a0 commit 647a45e
Show file tree
Hide file tree
Showing 14 changed files with 1,267 additions and 1,140 deletions.
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.175.0",
"get-stdin": "8.0.0",
Expand Down Expand Up @@ -80,6 +82,7 @@
"remark-math": "3.0.1",
"remark-parse": "8.0.3",
"resolve": "1.22.0",
"sdbm": "2.0.0",
"semver": "7.3.5",
"string-width": "5.0.1",
"strip-ansi": "7.0.1",
Expand All @@ -97,6 +100,8 @@
"@esbuild-plugins/node-modules-polyfill": "0.1.4",
"@glimmer/reference": "0.84.1",
"@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.17.0",
"babel-jest": "27.5.1",
Expand Down
1 change: 1 addition & 0 deletions scripts/vendors/vendors.mjs
Expand Up @@ -7,6 +7,7 @@ const vendors = [
"html-void-elements",
"leven",
"mem",
"sdbm",
"string-width",
"strip-ansi",
"tempy",
Expand Down
20 changes: 20 additions & 0 deletions src/cli/constant.js
Expand Up @@ -70,6 +70,26 @@ const categoryOrder = [
* Note: The options below are sorted alphabetically.
*/
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",
},
],
default: "metadata",
description: "Strategy for the cache to use for detecting changed files.",
type: "choice",
},
check: {
type: "boolean",
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;
38 changes: 37 additions & 1 deletion src/cli/format.js
Expand Up @@ -15,6 +15,9 @@ const { createIgnorer, errors } = require("./prettier-internal.js");
const { expandPatterns, fixWindowsSlashes } = require("./expand-patterns.js");
const getOptionsForFile = require("./options/get-options-for-file.js");
const isTTY = require("./is-tty.js");
const findCacheFile = require("./find-cache-file.js");
const FormatResultsCache = require("./format-results-cache.js");
const { statSafe } = require("./utils.js");

function diff(a, b) {
return require("diff").createTwoFilesPatch("", "", a, b, "", "", {
Expand Down Expand Up @@ -284,6 +287,20 @@ async function formatFiles(context) {
context.logger.log("Checking formatting...");
}

let formatResultsCache;
const cacheFilePath = findCacheFile();
if (context.argv.cache) {
formatResultsCache = new FormatResultsCache(
cacheFilePath,
context.argv.cacheStrategy
);
} else {
const stat = await statSafe(cacheFilePath);
if (stat) {
await fs.unlink(cacheFilePath);
}
}

for await (const pathOrError of expandPatterns(context)) {
if (typeof pathOrError === "object") {
context.logger.error(pathOrError.error);
Expand Down Expand Up @@ -351,16 +368,28 @@ async function formatFiles(context) {

const start = Date.now();

const isCacheExists = formatResultsCache?.existsAvailableFormatResultsCache(
filename,
options
);


let result;
let output;

try {
if (isCacheExists) {
result = { formatted: input };
} else {
result = format(context, input, options);
}
result = format(context, input, options);
output = result.formatted;
} catch (error) {
handleError(context, filename, error, printedFilename);
continue;
}
formatResultsCache?.setFormatResultsCache(filename, options);

const isDifferent = output !== input;

Expand Down Expand Up @@ -390,7 +419,12 @@ async function formatFiles(context) {
process.exitCode = 2;
}
} else if (!context.argv.check && !context.argv.listDifferent) {
context.logger.log(`${chalk.grey(filename)} ${Date.now() - start}ms`);
const message = `${chalk.grey(filename)} ${Date.now() - start}ms`;
if (isCacheExists) {
context.logger.log(`${message} (cached)`);
} else {
context.logger.log(message);
}
}
} else if (context.argv.debugCheck) {
/* istanbul ignore else */
Expand All @@ -413,6 +447,8 @@ async function formatFiles(context) {
}
}

formatResultsCache?.reconcile();

// Print check summary based on expected exit code
if (context.argv.check) {
if (numberOfUnformattedFilesFound === 0) {
Expand Down
4 changes: 4 additions & 0 deletions src/cli/index.js
Expand Up @@ -101,6 +101,10 @@ async function main(rawArguments, logger) {
} else if (context.argv.fileInfo) {
await logFileInfoOrDie(context);
} else if (useStdin) {
if (context.argv.cache) {
context.logger.error("`--cache` cannot be used with stdin.");
process.exit(2);
}
await formatStdin(context);
} else if (hasFilePatterns) {
await formatFiles(context);
Expand Down
31 changes: 30 additions & 1 deletion src/cli/utils.js
@@ -1,6 +1,35 @@
"use strict";

const { promises: fs } = require("fs");

// eslint-disable-next-line no-restricted-modules
const { default: sdbm } = require("../../vendors/sdbm.js");

// eslint-disable-next-line no-console
const printToScreen = console.log.bind(console);

module.exports = { printToScreen };
/**
* @param {string} source
* @returns {string}
*/
function createHash(source) {
return String(sdbm(source));
}

/**
* 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;
}
}
}

module.exports = { printToScreen, createHash, statSafe };
10 changes: 10 additions & 0 deletions tests/integration/__tests__/__snapshots__/early-exit.js.snap
Expand Up @@ -139,6 +139,11 @@ Editor options:
Other options:
--cache Only format changed files. Cannot use with --stdin-filepath.
Defaults to false.
--cache-strategy <metadata|content>
Strategy for the cache to use for detecting changed files.
Defaults to metadata.
--no-color Do not colorize error messages.
--no-error-on-unmatched-pattern
Prevent errors when pattern is unmatched.
Expand Down Expand Up @@ -309,6 +314,11 @@ Editor options:
Other options:
--cache Only format changed files. Cannot use with --stdin-filepath.
Defaults to false.
--cache-strategy <metadata|content>
Strategy for the cache to use for detecting changed files.
Defaults to metadata.
--no-color Do not colorize error messages.
--no-error-on-unmatched-pattern
Prevent errors when pattern is unmatched.
Expand Down

0 comments on commit 647a45e

Please sign in to comment.