Skip to content

Commit baa53a2

Browse files
fix: config and CSS expiration (#346)
* fix: config mtime compared as string * feat: using a map to cache CSS files * refactor: declare regexp earlier
1 parent 78f912e commit baa53a2

File tree

6 files changed

+119
-75
lines changed

6 files changed

+119
-75
lines changed

lib/util/cssFiles.js

+55-21
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
const fg = require('fast-glob');
44
const fs = require('fs');
55
const postcss = require('postcss');
6+
const lastClassFromSelectorRegexp = /\.([^\.\,\s\n\:\(\)\[\]\'~\+\>\*\\]*)/gim;
67
const removeDuplicatesFromArray = require('./removeDuplicatesFromArray');
78

8-
let previousGlobsResults = [];
9+
const cssFilesInfos = new Map();
910
let lastUpdate = null;
1011
let classnamesFromFiles = [];
1112

@@ -16,28 +17,61 @@ let classnamesFromFiles = [];
1617
* @returns {Array} List of classnames
1718
*/
1819
const generateClassnamesListSync = (patterns, refreshRate = 5_000) => {
19-
const now = new Date().getTime();
20-
const files = fg.sync(patterns, { suppressErrors: true });
21-
const newGlobs = previousGlobsResults.flat().join(',') != files.flat().join(',');
22-
const expired = lastUpdate === null || now - lastUpdate > refreshRate;
23-
if (newGlobs || expired) {
24-
previousGlobsResults = files;
25-
lastUpdate = now;
26-
let detectedClassnames = [];
27-
for (const file of files) {
28-
const data = fs.readFileSync(file, 'utf-8');
29-
const root = postcss.parse(data);
30-
root.walkRules((rule) => {
31-
const regexp = /\.([^\.\,\s\n\:\(\)\[\]\'~\+\>\*\\]*)/gim;
32-
const matches = [...rule.selector.matchAll(regexp)];
33-
const classnames = matches.map((arr) => arr[1]);
34-
detectedClassnames.push(...classnames);
35-
});
36-
detectedClassnames = removeDuplicatesFromArray(detectedClassnames);
20+
const now = Date.now();
21+
const isExpired = lastUpdate === null || now - lastUpdate > refreshRate;
22+
23+
if (!isExpired) {
24+
// console.log(`generateClassnamesListSync from cache (${classnamesFromFiles.length} classes)`);
25+
return classnamesFromFiles;
26+
}
27+
28+
// console.log('generateClassnamesListSync EXPIRED');
29+
// Update classnames from CSS files
30+
lastUpdate = now;
31+
const filesToBeRemoved = new Set([...cssFilesInfos.keys()]);
32+
const files = fg.sync(patterns, { suppressErrors: true, stats: true });
33+
for (const file of files) {
34+
let mtime = '';
35+
let canBeSkipped = cssFilesInfos.has(file.path);
36+
if (canBeSkipped) {
37+
// This file is still used
38+
filesToBeRemoved.delete(file.path);
39+
// Check modification date
40+
const stats = fs.statSync(file.path);
41+
mtime = `${stats.mtime || ''}`;
42+
canBeSkipped = cssFilesInfos.get(file.path).mtime === mtime;
3743
}
38-
classnamesFromFiles = detectedClassnames;
44+
if (canBeSkipped) {
45+
// File did not change since last run
46+
continue;
47+
}
48+
// Parse CSS file
49+
const data = fs.readFileSync(file.path, 'utf-8');
50+
const root = postcss.parse(data);
51+
let detectedClassnames = new Set();
52+
root.walkRules((rule) => {
53+
const matches = [...rule.selector.matchAll(lastClassFromSelectorRegexp)];
54+
const classnames = matches.map((arr) => arr[1]);
55+
detectedClassnames = new Set([...detectedClassnames, ...classnames]);
56+
});
57+
// Save the detected classnames
58+
cssFilesInfos.set(file.path, {
59+
mtime: mtime,
60+
classNames: [...detectedClassnames],
61+
});
62+
}
63+
// Remove erased CSS from the Map
64+
const deletedFiles = [...filesToBeRemoved];
65+
for (let i = 0; i < deletedFiles.length; i++) {
66+
cssFilesInfos.delete(deletedFiles[i]);
3967
}
40-
return classnamesFromFiles;
68+
// Build the final list
69+
classnamesFromFiles = [];
70+
cssFilesInfos.forEach((css) => {
71+
classnamesFromFiles = [...classnamesFromFiles, ...css.classNames];
72+
});
73+
// Unique classnames
74+
return removeDuplicatesFromArray(classnamesFromFiles);
4175
};
4276

4377
module.exports = generateClassnamesListSync;

lib/util/customConfig.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,39 @@ let lastModifiedDate = null;
2525
function requireUncached(module) {
2626
delete require.cache[require.resolve(module)];
2727
if (twLoadConfig === null) {
28+
// Using native loading
2829
return require(module);
2930
} else {
31+
// Using Tailwind CSS's loadConfig utility
3032
return twLoadConfig.loadConfig(module);
3133
}
3234
}
3335

36+
/**
37+
* Load the config from a path string or parsed from an object
38+
* @param {string|Object} config
39+
* @returns `null` when unchanged, `{}` when not found
40+
*/
3441
function loadConfig(config) {
3542
let loadedConfig = null;
3643
if (typeof config === 'string') {
3744
const resolvedPath = path.isAbsolute(config) ? config : path.join(path.resolve(), config);
3845
try {
3946
const stats = fs.statSync(resolvedPath);
47+
const mtime = `${stats.mtime || ''}`;
4048
if (stats === null) {
49+
// Default to no config
4150
loadedConfig = {};
42-
} else if (lastModifiedDate !== stats.mtime) {
43-
lastModifiedDate = stats.mtime;
51+
} else if (lastModifiedDate !== mtime) {
52+
// Load the config based on path
53+
lastModifiedDate = mtime;
4454
loadedConfig = requireUncached(resolvedPath);
4555
} else {
56+
// Unchanged config
4657
loadedConfig = null;
4758
}
4859
} catch (err) {
60+
// Default to no config
4961
loadedConfig = {};
5062
} finally {
5163
return loadedConfig;
@@ -70,8 +82,8 @@ function convertConfigToString(config) {
7082
}
7183

7284
function resolve(twConfig) {
73-
const now = new Date().getTime();
7485
const newConfig = convertConfigToString(twConfig) !== convertConfigToString(previousConfig);
86+
const now = Date.now();
7587
const expired = now - lastCheck > CHECK_REFRESH_RATE;
7688
if (newConfig || expired) {
7789
previousConfig = twConfig;

package-lock.json

+38-38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-tailwindcss",
3-
"version": "3.17.2",
3+
"version": "3.17.3-beta.3",
44
"description": "Rules enforcing best practices while using Tailwind CSS",
55
"keywords": [
66
"eslint",

tests/integrations/flat-config.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const cp = require("child_process");
55
const path = require("path");
66
const semver = require("semver");
77

8-
const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`;
8+
const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep);
99

1010
describe("Integration with flat config", () => {
1111
let originalCwd;
@@ -29,11 +29,10 @@ describe("Integration with flat config", () => {
2929
return;
3030
}
3131

32-
const result = JSON.parse(
33-
cp.execSync(`${ESLINT} a.vue --format=json`, {
34-
encoding: "utf8",
35-
})
36-
);
32+
const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, {
33+
encoding: "utf8",
34+
});
35+
const result = JSON.parse(lintResult);
3736
assert.strictEqual(result.length, 1);
3837
assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder");
3938
});

tests/integrations/legacy-config.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const cp = require("child_process");
55
const path = require("path");
66
const semver = require("semver");
77

8-
const ESLINT = `.${path.sep}node_modules${path.sep}.bin${path.sep}eslint`;
8+
const ESLINT_BIN_PATH = [".", "node_modules", ".bin", "eslint"].join(path.sep);
99

1010
describe("Integration with legacy config", () => {
1111
let originalCwd;
@@ -29,11 +29,10 @@ describe("Integration with legacy config", () => {
2929
return;
3030
}
3131

32-
const result = JSON.parse(
33-
cp.execSync(`${ESLINT} a.vue --format=json`, {
34-
encoding: "utf8",
35-
})
36-
);
32+
const lintResult = cp.execSync(`${ESLINT_BIN_PATH} a.vue --format=json`, {
33+
encoding: "utf8",
34+
});
35+
const result = JSON.parse(lintResult);
3736
assert.strictEqual(result.length, 1);
3837
assert.deepStrictEqual(result[0].messages[0].messageId, "invalidOrder");
3938
});

0 commit comments

Comments
 (0)