diff --git a/lib/util/cssFiles.js b/lib/util/cssFiles.js index 4a49a33..324a42b 100644 --- a/lib/util/cssFiles.js +++ b/lib/util/cssFiles.js @@ -3,9 +3,10 @@ const fg = require('fast-glob'); const fs = require('fs'); const postcss = require('postcss'); +const lastClassFromSelectorRegexp = /\.([^\.\,\s\n\:\(\)\[\]\'~\+\>\*\\]*)/gim; const removeDuplicatesFromArray = require('./removeDuplicatesFromArray'); -let previousGlobsResults = []; +const cssFilesInfos = new Map(); let lastUpdate = null; let classnamesFromFiles = []; @@ -16,28 +17,61 @@ let classnamesFromFiles = []; * @returns {Array} List of classnames */ const generateClassnamesListSync = (patterns, refreshRate = 5_000) => { - const now = new Date().getTime(); - const files = fg.sync(patterns, { suppressErrors: true }); - const newGlobs = previousGlobsResults.flat().join(',') != files.flat().join(','); - const expired = lastUpdate === null || now - lastUpdate > refreshRate; - if (newGlobs || expired) { - previousGlobsResults = files; - lastUpdate = now; - let detectedClassnames = []; - for (const file of files) { - const data = fs.readFileSync(file, 'utf-8'); - const root = postcss.parse(data); - root.walkRules((rule) => { - const regexp = /\.([^\.\,\s\n\:\(\)\[\]\'~\+\>\*\\]*)/gim; - const matches = [...rule.selector.matchAll(regexp)]; - const classnames = matches.map((arr) => arr[1]); - detectedClassnames.push(...classnames); - }); - detectedClassnames = removeDuplicatesFromArray(detectedClassnames); + const now = Date.now(); + const isExpired = lastUpdate === null || now - lastUpdate > refreshRate; + + if (!isExpired) { + // console.log(`generateClassnamesListSync from cache (${classnamesFromFiles.length} classes)`); + return classnamesFromFiles; + } + + // console.log('generateClassnamesListSync EXPIRED'); + // Update classnames from CSS files + lastUpdate = now; + const filesToBeRemoved = new Set([...cssFilesInfos.keys()]); + const files = fg.sync(patterns, { suppressErrors: true, stats: true }); + for (const file of files) { + let mtime = ''; + let canBeSkipped = cssFilesInfos.has(file.path); + if (canBeSkipped) { + // This file is still used + filesToBeRemoved.delete(file.path); + // Check modification date + const stats = fs.statSync(file.path); + mtime = `${stats.mtime || ''}`; + canBeSkipped = cssFilesInfos.get(file.path).mtime === mtime; } - classnamesFromFiles = detectedClassnames; + if (canBeSkipped) { + // File did not change since last run + continue; + } + // Parse CSS file + const data = fs.readFileSync(file.path, 'utf-8'); + const root = postcss.parse(data); + let detectedClassnames = new Set(); + root.walkRules((rule) => { + const matches = [...rule.selector.matchAll(lastClassFromSelectorRegexp)]; + const classnames = matches.map((arr) => arr[1]); + detectedClassnames = new Set([...detectedClassnames, ...classnames]); + }); + // Save the detected classnames + cssFilesInfos.set(file.path, { + mtime: mtime, + classNames: [...detectedClassnames], + }); + } + // Remove erased CSS from the Map + const deletedFiles = [...filesToBeRemoved]; + for (let i = 0; i < deletedFiles.length; i++) { + cssFilesInfos.delete(deletedFiles[i]); } - return classnamesFromFiles; + // Build the final list + classnamesFromFiles = []; + cssFilesInfos.forEach((css) => { + classnamesFromFiles = [...classnamesFromFiles, ...css.classNames]; + }); + // Unique classnames + return removeDuplicatesFromArray(classnamesFromFiles); }; module.exports = generateClassnamesListSync; diff --git a/lib/util/customConfig.js b/lib/util/customConfig.js index 9bc6a40..1ab1599 100644 --- a/lib/util/customConfig.js +++ b/lib/util/customConfig.js @@ -25,27 +25,39 @@ let lastModifiedDate = null; function requireUncached(module) { delete require.cache[require.resolve(module)]; if (twLoadConfig === null) { + // Using native loading return require(module); } else { + // Using Tailwind CSS's loadConfig utility return twLoadConfig.loadConfig(module); } } +/** + * Load the config from a path string or parsed from an object + * @param {string|Object} config + * @returns `null` when unchanged, `{}` when not found + */ function loadConfig(config) { let loadedConfig = null; if (typeof config === 'string') { const resolvedPath = path.isAbsolute(config) ? config : path.join(path.resolve(), config); try { const stats = fs.statSync(resolvedPath); + const mtime = `${stats.mtime || ''}`; if (stats === null) { + // Default to no config loadedConfig = {}; - } else if (lastModifiedDate !== stats.mtime) { - lastModifiedDate = stats.mtime; + } else if (lastModifiedDate !== mtime) { + // Load the config based on path + lastModifiedDate = mtime; loadedConfig = requireUncached(resolvedPath); } else { + // Unchanged config loadedConfig = null; } } catch (err) { + // Default to no config loadedConfig = {}; } finally { return loadedConfig; @@ -70,8 +82,8 @@ function convertConfigToString(config) { } function resolve(twConfig) { - const now = new Date().getTime(); const newConfig = convertConfigToString(twConfig) !== convertConfigToString(previousConfig); + const now = Date.now(); const expired = now - lastCheck > CHECK_REFRESH_RATE; if (newConfig || expired) { previousConfig = twConfig; diff --git a/package-lock.json b/package-lock.json index 523c883..c59e7dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-tailwindcss", - "version": "3.17.2", + "version": "3.17.3-beta.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-tailwindcss", - "version": "3.17.2", + "version": "3.17.3-beta.3", "license": "MIT", "dependencies": { "fast-glob": "^3.2.5", @@ -551,11 +551,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1179,9 +1179,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1810,9 +1810,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -2019,9 +2019,9 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -2037,9 +2037,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -2399,9 +2399,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -3219,11 +3219,11 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browser-stdout": { @@ -3690,9 +3690,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } @@ -4154,9 +4154,9 @@ } }, "nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, "natural-compare": { "version": "1.4.0", @@ -4297,13 +4297,13 @@ "dev": true }, "postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "requires": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" } }, "postcss-import": { @@ -4525,9 +4525,9 @@ "dev": true }, "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, "string-width": { "version": "4.2.2", diff --git a/package.json b/package.json index 77b7308..a4bf73b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-tailwindcss", - "version": "3.17.2", + "version": "3.17.3-beta.3", "description": "Rules enforcing best practices while using Tailwind CSS", "keywords": [ "eslint", diff --git a/tests/integrations/flat-config.js b/tests/integrations/flat-config.js index 391c69f..a81683f 100644 --- a/tests/integrations/flat-config.js +++ b/tests/integrations/flat-config.js @@ -5,7 +5,7 @@ const cp = require("child_process"); const path = require("path"); const semver = require("semver"); -const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`; +const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep); describe("Integration with flat config", () => { let originalCwd; @@ -29,11 +29,10 @@ describe("Integration with flat config", () => { return; } - const result = JSON.parse( - cp.execSync(`${ESLINT} a.vue --format=json`, { - encoding: "utf8", - }) - ); + const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, { + encoding: "utf8", + }); + const result = JSON.parse(lintResult); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder"); }); diff --git a/tests/integrations/legacy-config.js b/tests/integrations/legacy-config.js index 243b10d..071ac9f 100644 --- a/tests/integrations/legacy-config.js +++ b/tests/integrations/legacy-config.js @@ -5,7 +5,7 @@ const cp = require("child_process"); const path = require("path"); const semver = require("semver"); -const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`; +const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep); describe("Integration with legacy config", () => { let originalCwd; @@ -29,11 +29,10 @@ describe("Integration with legacy config", () => { return; } - const result = JSON.parse( - cp.execSync(`${ESLINT} a.vue --format=json`, { - encoding: "utf8", - }) - ); + const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, { + encoding: "utf8", + }); + const result = JSON.parse(lintResult); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder"); });