diff --git a/.changeset/mighty-apricots-cheat.md b/.changeset/mighty-apricots-cheat.md new file mode 100644 index 0000000000..c96ce14872 --- /dev/null +++ b/.changeset/mighty-apricots-cheat.md @@ -0,0 +1,5 @@ +--- +"stylelint": minor +--- + +Added: `cacheStrategy` option diff --git a/docs/user-guide/usage/cli.md b/docs/user-guide/usage/cli.md index 54f4a8c4e0..e37b6c722e 100644 --- a/docs/user-guide/usage/cli.md +++ b/docs/user-guide/usage/cli.md @@ -20,6 +20,10 @@ The process exits without throwing an error when glob pattern matches no files. Path to a file or directory for the cache location. [More info](options.md#cachelocation). +### `--cache-strategy` + +Strategy for the cache to use for detecting changed files. Can be either "metadata" or "content". [More info](options.md#cachestrategy). + ### `--cache` Store the results of processed files so that Stylelint only operates on the changed ones. By default, the cache is stored in `./.stylelintcache` in `process.cwd()`. [More info](options.md#cache). diff --git a/docs/user-guide/usage/options.md b/docs/user-guide/usage/options.md index 2a34f7bbfb..005b9124df 100644 --- a/docs/user-guide/usage/options.md +++ b/docs/user-guide/usage/options.md @@ -110,6 +110,14 @@ If a directory is specified, Stylelint creates a cache file inside the specified _If the directory of `cacheLocation` does not exist, make sure you add a trailing `/` on \*nix systems or `\` on Windows. Otherwise, Stylelint assumes the path to be a file._ +## `cacheStrategy` + +CLI flag: `--cache-strategy` + +Strategy for the cache to use for detecting changed files. Can be either "metadata" or "content". + +The "content" strategy can be useful in cases where the modification time of your files changes even if their contents have not. For example, this can happen during git operations like "git clone" because git does not track file modification time. + ## `maxWarnings` CLI flags: `--max-warnings, --mw` diff --git a/lib/__tests__/__snapshots__/cli.test.js.snap b/lib/__tests__/__snapshots__/cli.test.js.snap index c554de0d9e..42d39a49c4 100644 --- a/lib/__tests__/__snapshots__/cli.test.js.snap +++ b/lib/__tests__/__snapshots__/cli.test.js.snap @@ -88,6 +88,16 @@ exports[`CLI --help 1`] = ` If the directory for the cache does not exist, make sure you add a trailing \\"/\\" on *nix systems or \\"\\\\\\" on Windows. Otherwise the path will be assumed to be a file. + --cache-strategy [default: \\"metadata\\"] + + Strategy for the cache to use for detecting changed files. Can be either + \\"metadata\\" or \\"content\\". + + The \\"content\\" strategy can be useful in cases where the modification time of + your files changes even if their contents have not. For example, this can happen + during git operations like \\"git clone\\" because git does not track file modification + time. + --formatter, -f [default: \\"string\\"] The output formatter: \\"compact\\", \\"github\\", \\"json\\", \\"string\\", \\"tap\\", \\"unix\\" or \\"verbose\\". diff --git a/lib/__tests__/cli.test.js b/lib/__tests__/cli.test.js index 373489384c..f726813c54 100644 --- a/lib/__tests__/cli.test.js +++ b/lib/__tests__/cli.test.js @@ -50,6 +50,11 @@ describe('buildCLI', () => { expect(buildCLI(['--cache-location=foo']).flags.cacheLocation).toBe('foo'); }); + it('flags.cacheStrategy', () => { + expect(buildCLI(['--cache-strategy=content']).flags.cacheStrategy).toBe('content'); + expect(buildCLI(['--cache-strategy=metadata']).flags.cacheStrategy).toBe('metadata'); + }); + it('flags.color', () => { expect(buildCLI(['--color']).flags.color).toBe(true); expect(buildCLI(['--no-color']).flags.color).toBe(false); diff --git a/lib/__tests__/standalone-cache.test.js b/lib/__tests__/standalone-cache.test.js index ecc80f1f45..45c629041c 100644 --- a/lib/__tests__/standalone-cache.test.js +++ b/lib/__tests__/standalone-cache.test.js @@ -242,3 +242,53 @@ describe('standalone cache uses a config file', () => { expect(resultsNew[0].warnings).toHaveLength(1); }); }); + +describe('standalone cache uses cacheStrategy', () => { + const cwd = path.join(__dirname, 'tmp', 'standalone-cache-uses-cacheStrategy'); + + safeChdir(cwd); + + const expectedCacheFilePath = path.join(cwd, '.stylelintcache'); + + afterEach(async () => { + // clean up after each test + await removeFile(expectedCacheFilePath); + await removeFile(newFileDest); + }); + + it('cacheStrategy is invalid', async () => { + await expect(standalone(getConfig({ cacheStrategy: 'foo' }))).rejects.toThrow( + '"foo" cache strategy is unsupported. Specify either "metadata" or "content"', + ); + }); + + it('cacheStrategy is "metadata"', async () => { + const cacheStrategy = 'metadata'; + + await fs.copyFile(validFile, newFileDest); + const { results } = await standalone(getConfig({ cacheStrategy })); + + expect(results.some((file) => isChanged(file, newFileDest))).toBe(true); + + // No content change, but file metadata id changed + await fs.utimes(newFileDest, new Date(), new Date()); + const { results: resultsCached } = await standalone(getConfig({ cacheStrategy })); + + expect(resultsCached.some((file) => isChanged(file, newFileDest))).toBe(true); + }); + + it('cacheStrategy is "content"', async () => { + const cacheStrategy = 'content'; + + await fs.copyFile(validFile, newFileDest); + const { results } = await standalone(getConfig({ cacheStrategy })); + + expect(results.some((file) => isChanged(file, newFileDest))).toBe(true); + + // No content change, but file metadata id changed + await fs.utimes(newFileDest, new Date(), new Date()); + const { results: resultsCached } = await standalone(getConfig({ cacheStrategy })); + + expect(resultsCached.some((file) => isChanged(file, newFileDest))).toBe(false); + }); +}); diff --git a/lib/cli.js b/lib/cli.js index 906ac6573e..d2d7cda8a8 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -21,6 +21,7 @@ const EXIT_CODE_ERROR = 2; * @typedef {object} CLIFlags * @property {boolean} [cache] * @property {string} [cacheLocation] + * @property {string} [cacheStrategy] * @property {string | false} config * @property {string} [configBasedir] * @property {string} [customSyntax] @@ -64,6 +65,7 @@ const EXIT_CODE_ERROR = 2; * @property {boolean} [cache] * @property {string} [configFile] * @property {string} [cacheLocation] + * @property {string} [cacheStrategy] * @property {string} [customSyntax] * @property {string} [codeFilename] * @property {string} [configBasedir] @@ -173,6 +175,16 @@ const meowOptions = { If the directory for the cache does not exist, make sure you add a trailing "/" on *nix systems or "\\" on Windows. Otherwise the path will be assumed to be a file. + --cache-strategy [default: "metadata"] + + Strategy for the cache to use for detecting changed files. Can be either + "metadata" or "content". + + The "content" strategy can be useful in cases where the modification time of + your files changes even if their contents have not. For example, this can happen + during git operations like "git clone" because git does not track file modification + time. + --formatter, -f [default: "string"] The output formatter: ${getFormatterOptionsText({ useOr: true })}. @@ -236,6 +248,9 @@ const meowOptions = { cacheLocation: { type: 'string', }, + cacheStrategy: { + type: 'string', + }, color: { type: 'boolean', }, @@ -410,6 +425,10 @@ module.exports = async (argv) => { optionsBase.cacheLocation = cli.flags.cacheLocation; } + if (cli.flags.cacheStrategy) { + optionsBase.cacheStrategy = cli.flags.cacheStrategy; + } + if (cli.flags.fix) { optionsBase.fix = cli.flags.fix; } diff --git a/lib/createStylelint.js b/lib/createStylelint.js index e63a5aa6f8..7f034c664b 100644 --- a/lib/createStylelint.js +++ b/lib/createStylelint.js @@ -36,7 +36,11 @@ function createStylelint(options = {}) { stylelint._specifiedConfigCache = new Map(); stylelint._postcssResultCache = new Map(); - stylelint._fileCache = new FileCache(stylelint._options.cacheLocation, stylelint._options.cwd); + stylelint._fileCache = new FileCache( + stylelint._options.cacheLocation, + stylelint._options.cacheStrategy, + stylelint._options.cwd, + ); stylelint._createStylelintResult = createStylelintResult.bind(null, stylelint); stylelint._getPostcssResult = getPostcssResult.bind(null, stylelint); stylelint._lintSource = lintSource.bind(null, stylelint); diff --git a/lib/standalone.js b/lib/standalone.js index b544d0fc2e..b74659ab7d 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -36,6 +36,7 @@ async function standalone({ allowEmptyInput = false, cache: useCache = false, cacheLocation, + cacheStrategy, code, codeFilename, config, @@ -91,6 +92,7 @@ async function standalone({ const stylelint = createStylelint({ cacheLocation, + cacheStrategy, config, configFile, configBasedir, diff --git a/lib/utils/FileCache.js b/lib/utils/FileCache.js index 67b9f227ec..c599e6b82a 100644 --- a/lib/utils/FileCache.js +++ b/lib/utils/FileCache.js @@ -7,20 +7,31 @@ const hash = require('./hash'); const pkg = require('../../package.json'); const path = require('path'); +const CACHE_STRATEGY_METADATA = 'metadata'; +const CACHE_STRATEGY_CONTENT = 'content'; + const DEFAULT_CACHE_LOCATION = './.stylelintcache'; +const DEFAULT_CACHE_STRATEGY = CACHE_STRATEGY_METADATA; /** @typedef {import('file-entry-cache').FileDescriptor["meta"] & { hashOfConfig?: string }} CacheMetadata */ class FileCache { - /** - * @param {string | undefined} cacheLocation - * @param {string} cwd - */ - constructor(cacheLocation = DEFAULT_CACHE_LOCATION, cwd) { + constructor( + cacheLocation = DEFAULT_CACHE_LOCATION, + cacheStrategy = DEFAULT_CACHE_STRATEGY, + cwd = process.cwd(), + ) { + if (![CACHE_STRATEGY_METADATA, CACHE_STRATEGY_CONTENT].includes(cacheStrategy)) { + throw new Error( + `"${cacheStrategy}" cache strategy is unsupported. Specify either "${CACHE_STRATEGY_METADATA}" or "${CACHE_STRATEGY_CONTENT}"`, + ); + } + const cacheFile = path.resolve(getCacheFile(cacheLocation, cwd)); + const useCheckSum = cacheStrategy === CACHE_STRATEGY_CONTENT; debug(`Cache file is created at ${cacheFile}`); - this._fileCache = fileEntryCache.create(cacheFile); + this._fileCache = fileEntryCache.create(cacheFile, undefined, useCheckSum); this._hashOfConfig = ''; } diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index 1b363f4bef..940ab88106 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -222,6 +222,7 @@ declare module 'stylelint' { globbyOptions?: GlobbyOptions; cache?: boolean; cacheLocation?: string; + cacheStrategy?: string; code?: string; codeFilename?: string; config?: Config;