Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: config and CSS expiration #346

Merged
merged 9 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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