Skip to content

Commit

Permalink
fix: config and CSS expiration (#346)
Browse files Browse the repository at this point in the history
* fix: config mtime compared as string
* feat: using a map to cache CSS files
* refactor: declare regexp earlier
  • Loading branch information
francoismassart committed Jun 21, 2024
1 parent 78f912e commit baa53a2
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 75 deletions.
76 changes: 55 additions & 21 deletions lib/util/cssFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand All @@ -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;
18 changes: 15 additions & 3 deletions lib/util/customConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
76 changes: 38 additions & 38 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
11 changes: 5 additions & 6 deletions tests/integrations/flat-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
});
Expand Down
11 changes: 5 additions & 6 deletions tests/integrations/legacy-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
});
Expand Down

0 comments on commit baa53a2

Please sign in to comment.