diff --git a/.changeset/happy-oranges-invite.md b/.changeset/happy-oranges-invite.md new file mode 100644 index 0000000000..15b6fc6b66 --- /dev/null +++ b/.changeset/happy-oranges-invite.md @@ -0,0 +1,5 @@ +--- +"stylelint": patch +--- + +Fixed: cache refresh when config is changed diff --git a/lib/__tests__/standalone-cache.test.js b/lib/__tests__/standalone-cache.test.js index e199fe0697..ecc80f1f45 100644 --- a/lib/__tests__/standalone-cache.test.js +++ b/lib/__tests__/standalone-cache.test.js @@ -10,6 +10,10 @@ const removeFile = require('../testUtils/removeFile'); const safeChdir = require('../testUtils/safeChdir'); const standalone = require('../standalone'); +const isChanged = (file, targetFilePath) => { + return file.source === targetFilePath && !file.ignored; +}; + const fixturesPath = path.join(__dirname, 'fixtures'); const invalidFile = path.join(fixturesPath, 'empty-block.css'); const syntaxErrorFile = path.join(fixturesPath, 'syntax_error.css'); @@ -68,8 +72,8 @@ describe('standalone cache', () => { const { results } = await standalone(getConfig()); // Ensure only changed files are linted - expect(results.some((file) => file.source === validFile)).toBe(false); - expect(results.some((file) => file.source === newFileDest)).toBe(true); + expect(results.some((file) => isChanged(file, validFile))).toBe(false); + expect(results.some((file) => isChanged(file, newFileDest))).toBe(true); const { cache } = fCache.createFromFile(expectedCacheFilePath); @@ -92,8 +96,8 @@ describe('standalone cache', () => { ); // Ensure all files are re-linted - expect(results.some((file) => file.source === validFile)).toBe(true); - expect(results.some((file) => file.source === newFileDest)).toBe(true); + expect(results.some((file) => isChanged(file, validFile))).toBe(true); + expect(results.some((file) => isChanged(file, newFileDest))).toBe(true); }); it('invalid files are not cached', async () => { @@ -105,8 +109,8 @@ describe('standalone cache', () => { expect(errored).toBe(true); // Ensure only changed files are linted - expect(results.some((file) => file.source === validFile)).toBe(false); - expect(results.some((file) => file.source === newFileDest)).toBe(true); + expect(results.some((file) => isChanged(file, validFile))).toBe(false); + expect(results.some((file) => isChanged(file, newFileDest))).toBe(true); const { cache } = fCache.createFromFile(expectedCacheFilePath); @@ -194,3 +198,47 @@ describe('standalone cache uses cacheLocation', () => { expect(cache.getKey(validFile)).toBeTruthy(); }); }); + +describe('standalone cache uses a config file', () => { + const cwd = path.join(__dirname, 'tmp', 'standalone-cache-use-config-file'); + + safeChdir(cwd); + + const configFile = path.join(cwd, '.stylelintrc.json'); + const lintedFile = path.join(cwd, 'a.css'); + + beforeEach(async () => { + await fs.writeFile(lintedFile, 'a {}'); + }); + + afterEach(async () => { + await removeFile(configFile); + await removeFile(lintedFile); + }); + + it('cache is discarded when a config file is changed', async () => { + const config = { files: [lintedFile], config: undefined, cache: true }; + + // No warnings when a rule is disabled. + await fs.writeFile( + configFile, + JSON.stringify({ + rules: { 'block-no-empty': null }, + }), + ); + const { results } = await standalone(config); + + expect(results[0].warnings).toHaveLength(0); + + // Some warnings when a rule becomes enabled by changing the config. + await fs.writeFile( + configFile, + JSON.stringify({ + rules: { 'block-no-empty': true }, + }), + ); + const { results: resultsNew } = await standalone(config); + + expect(resultsNew[0].warnings).toHaveLength(1); + }); +}); diff --git a/lib/createStylelint.js b/lib/createStylelint.js index ffe0a04582..e63a5aa6f8 100644 --- a/lib/createStylelint.js +++ b/lib/createStylelint.js @@ -6,6 +6,7 @@ const getConfigForFile = require('./getConfigForFile'); const getPostcssResult = require('./getPostcssResult'); const isPathIgnored = require('./isPathIgnored'); const lintSource = require('./lintSource'); +const FileCache = require('./utils/FileCache'); const { cosmiconfig } = require('cosmiconfig'); const IS_TEST = process.env.NODE_ENV === 'test'; @@ -35,6 +36,7 @@ function createStylelint(options = {}) { stylelint._specifiedConfigCache = new Map(); stylelint._postcssResultCache = new Map(); + stylelint._fileCache = new FileCache(stylelint._options.cacheLocation, 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/lintSource.js b/lib/lintSource.js index d45d71e20a..bd101d4a61 100644 --- a/lib/lintSource.js +++ b/lib/lintSource.js @@ -66,6 +66,18 @@ module.exports = async function lintSource(stylelint, options = {}) { const config = configForFile.config; const existingPostcssResult = options.existingPostcssResult; + if (options.cache) { + stylelint._fileCache.calcHashOfConfig(config); + + if (options.filePath && !stylelint._fileCache.hasFileChanged(options.filePath)) { + return existingPostcssResult + ? Object.assign(existingPostcssResult, { + stylelint: createEmptyStylelintPostcssResult(), + }) + : createEmptyPostcssResult(inputFilePath); + } + } + /** @type {StylelintPostcssResult} */ const stylelintResult = { ruleSeverities: {}, diff --git a/lib/standalone.js b/lib/standalone.js index 6d4e45067f..b544d0fc2e 100644 --- a/lib/standalone.js +++ b/lib/standalone.js @@ -9,16 +9,13 @@ const path = require('path'); const createStylelint = require('./createStylelint'); const createStylelintResult = require('./createStylelintResult'); -const FileCache = require('./utils/FileCache'); const filterFilePaths = require('./utils/filterFilePaths'); const formatters = require('./formatters'); const getFileIgnorer = require('./utils/getFileIgnorer'); const getFormatterOptionsText = require('./utils/getFormatterOptionsText'); -const hash = require('./utils/hash'); const NoFilesFoundError = require('./utils/noFilesFoundError'); const AllFilesIgnoredError = require('./utils/allFilesIgnoredError'); const { assert } = require('./utils/validateTypes'); -const pkg = require('../package.json'); const prepareReturnValue = require('./prepareReturnValue'); const ALWAYS_IGNORED_GLOBS = ['**/node_modules/**']; @@ -61,8 +58,6 @@ async function standalone({ reportNeedlessDisables, syntax, }) { - /** @type {FileCache} */ - let fileCache; const startTime = Date.now(); const isValidCode = typeof code === 'string'; @@ -95,6 +90,7 @@ async function standalone({ } const stylelint = createStylelint({ + cacheLocation, config, configFile, configBasedir, @@ -175,16 +171,8 @@ async function standalone({ fileList = fileList.concat(ALWAYS_IGNORED_GLOBS.map((glob) => `!${glob}`)); } - if (useCache) { - const stylelintVersion = pkg.version; - const hashOfConfig = hash(`${stylelintVersion}_${JSON.stringify(config || {})}`); - - fileCache = new FileCache(cacheLocation, cwd, hashOfConfig); - } else { - // No need to calculate hash here, we just want to delete cache file. - fileCache = new FileCache(cacheLocation, cwd); - // Remove cache file if cache option is disabled - fileCache.destroy(); + if (!useCache) { + stylelint._fileCache.destroy(); } const effectiveGlobbyOptions = { @@ -217,21 +205,18 @@ async function standalone({ return absoluteFilepath; }); - if (useCache) { - absoluteFilePaths = absoluteFilePaths.filter(fileCache.hasFileChanged.bind(fileCache)); - } - const getStylelintResults = absoluteFilePaths.map(async (absoluteFilepath) => { debug(`Processing ${absoluteFilepath}`); try { const postcssResult = await stylelint._lintSource({ filePath: absoluteFilepath, + cache: useCache, }); if (postcssResult.stylelint.stylelintError && useCache) { debug(`${absoluteFilepath} contains linting errors and will not be cached.`); - fileCache.removeEntry(absoluteFilepath); + stylelint._fileCache.removeEntry(absoluteFilepath); } /** @@ -258,7 +243,7 @@ async function standalone({ return stylelint._createStylelintResult(postcssResult, absoluteFilepath); } catch (error) { // On any error, we should not cache the lint result - fileCache.removeEntry(absoluteFilepath); + stylelint._fileCache.removeEntry(absoluteFilepath); return handleError(stylelint, error, absoluteFilepath); } @@ -275,7 +260,7 @@ async function standalone({ } if (useCache) { - fileCache.reconcile(); + stylelint._fileCache.reconcile(); } const result = prepareReturnValue(stylelintResults, maxWarnings, formatterFunction, cwd); diff --git a/lib/utils/FileCache.js b/lib/utils/FileCache.js index 698b3105f1..67b9f227ec 100644 --- a/lib/utils/FileCache.js +++ b/lib/utils/FileCache.js @@ -3,29 +3,37 @@ const debug = require('debug')('stylelint:file-cache'); const fileEntryCache = require('file-entry-cache'); const getCacheFile = require('./getCacheFile'); +const hash = require('./hash'); +const pkg = require('../../package.json'); const path = require('path'); const DEFAULT_CACHE_LOCATION = './.stylelintcache'; -const DEFAULT_HASH = ''; /** @typedef {import('file-entry-cache').FileDescriptor["meta"] & { hashOfConfig?: string }} CacheMetadata */ -/** - * @param {string} [cacheLocation] - * @param {string} [hashOfConfig] - * @constructor - */ class FileCache { - constructor( - cacheLocation = DEFAULT_CACHE_LOCATION, - cwd = process.cwd(), - hashOfConfig = DEFAULT_HASH, - ) { + /** + * @param {string | undefined} cacheLocation + * @param {string} cwd + */ + constructor(cacheLocation = DEFAULT_CACHE_LOCATION, cwd) { const cacheFile = path.resolve(getCacheFile(cacheLocation, cwd)); debug(`Cache file is created at ${cacheFile}`); this._fileCache = fileEntryCache.create(cacheFile); - this._hashOfConfig = hashOfConfig; + this._hashOfConfig = ''; + } + + /** + * @param {import('stylelint').Config} config + */ + calcHashOfConfig(config) { + if (this._hashOfConfig) return; + + const stylelintVersion = pkg.version; + const configString = JSON.stringify(config || {}); + + this._hashOfConfig = hash(`${stylelintVersion}_${configString}`); } /** diff --git a/types/stylelint/index.d.ts b/types/stylelint/index.d.ts index 2afb65d3ff..1b363f4bef 100644 --- a/types/stylelint/index.d.ts +++ b/types/stylelint/index.d.ts @@ -2,6 +2,7 @@ declare module 'stylelint' { import type * as PostCSS from 'postcss'; import type { GlobbyOptions } from 'globby'; import type { cosmiconfig } from 'cosmiconfig'; + import type * as fileEntryCache from 'file-entry-cache'; namespace stylelint { export type Severity = 'warning' | 'error'; @@ -87,6 +88,14 @@ declare module 'stylelint' { export type DisabledWarning = { line: number; rule: string }; + type FileCache = { + calcHashOfConfig: (config: Config) => void; + hasFileChanged: (absoluteFilepath: string) => boolean; + reconcile: () => void; + destroy: () => void; + removeEntry: (absoluteFilepath: string) => void; + }; + export type StylelintPostcssResult = { ruleSeverities: { [ruleName: string]: Severity }; customMessages: { [ruleName: string]: RuleMessage }; @@ -205,6 +214,7 @@ declare module 'stylelint' { export type GetLintSourceOptions = GetPostcssOptions & { existingPostcssResult?: PostCSS.Result; + cache?: boolean; }; export type LinterOptions = { @@ -517,6 +527,7 @@ declare module 'stylelint' { _extendExplorer: ReturnType; _specifiedConfigCache: Map>; _postcssResultCache: Map; + _fileCache: FileCache; _getPostcssResult: (options?: GetPostcssOptions) => Promise; _lintSource: (options: GetLintSourceOptions) => Promise;