From 87b247058ed520061fe1a146b7f0e7072a94990d Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Thu, 29 Dec 2022 23:38:16 +0100 Subject: [PATCH] fix: new instance of FlatESLint should load latest config file version (#16608) * fix: new instance of FlatESLint should load latest config file version * clear require.cache * add delays --- lib/eslint/flat-eslint.js | 38 +++++++++++++++- tests/lib/eslint/flat-eslint.js | 80 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js index 484c57a03de..d88cf178233 100644 --- a/lib/eslint/flat-eslint.js +++ b/lib/eslint/flat-eslint.js @@ -93,6 +93,7 @@ const FLAT_CONFIG_FILENAME = "eslint.config.js"; const debug = require("debug")("eslint:flat-eslint"); const removedFormatters = new Set(["table", "codeframe"]); const privateMembers = new WeakMap(); +const importedConfigFileModificationTime = new Map(); /** * It will calculate the error and warning count for collection of messages per file @@ -281,7 +282,42 @@ async function loadFlatConfigFile(filePath) { debug(`Config file URL is ${fileURL}`); - return (await import(fileURL)).default; + const mtime = (await fs.stat(filePath)).mtime.getTime(); + + /* + * Append a query with the config file's modification time (`mtime`) in order + * to import the current version of the config file. Without the query, `import()` would + * cache the config file module by the pathname only, and then always return + * the same version (the one that was actual when the module was imported for the first time). + * + * This ensures that the config file module is loaded and executed again + * if it has been changed since the last time it was imported. + * If it hasn't been changed, `import()` will just return the cached version. + * + * Note that we should not overuse queries (e.g., by appending the current time + * to always reload the config file module) as that could cause memory leaks + * because entries are never removed from the import cache. + */ + fileURL.searchParams.append("mtime", mtime); + + /* + * With queries, we can bypass the import cache. However, when import-ing a CJS module, + * Node.js uses the require infrastructure under the hood. That includes the require cache, + * which caches the config file module by its file path (queries have no effect). + * Therefore, we also need to clear the require cache before importing the config file module. + * In order to get the same behavior with ESM and CJS config files, in particular - to reload + * the config file only if it has been changed, we track file modification times and clear + * the require cache only if the file has been changed. + */ + if (importedConfigFileModificationTime.get(filePath) !== mtime) { + delete require.cache[filePath]; + } + + const config = (await import(fileURL)).default; + + importedConfigFileModificationTime.set(filePath, mtime); + + return config; } /** diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js index 0f0b8c1ffd5..ee17c3213cc 100644 --- a/tests/lib/eslint/flat-eslint.js +++ b/tests/lib/eslint/flat-eslint.js @@ -11,6 +11,7 @@ //------------------------------------------------------------------------------ const assert = require("assert"); +const util = require("util"); const fs = require("fs"); const fsp = fs.promises; const os = require("os"); @@ -41,6 +42,15 @@ function ensureDirectoryExists(dirPath) { } } +/** + * Does nothing for a given time. + * @param {number} time Time in ms. + * @returns {void} + */ +async function sleep(time) { + await util.promisify(setTimeout)(time); +} + //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ @@ -5305,4 +5315,74 @@ describe("FlatESLint", () => { }); }); + describe("config file", () => { + + it("new instance of FlatESLint should use the latest version of the config file (ESM)", async () => { + const cwd = path.join(getFixturePath(), `config_file_${Date.now()}`); + const configFileContent = "export default [{ rules: { semi: ['error', 'always'] } }];"; + const teardown = createCustomTeardown({ + cwd, + files: { + "package.json": '{ "type": "module" }', + "eslint.config.js": configFileContent, + "a.js": "foo\nbar;" + } + }); + + await teardown.prepare(); + + let eslint = new FlatESLint({ cwd }); + let [{ messages }] = await eslint.lintFiles(["a.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "semi"); + assert.strictEqual(messages[0].messageId, "missingSemi"); + assert.strictEqual(messages[0].line, 1); + + await sleep(100); + await fsp.writeFile(path.join(cwd, "eslint.config.js"), configFileContent.replace("always", "never")); + + eslint = new FlatESLint({ cwd }); + [{ messages }] = await eslint.lintFiles(["a.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "semi"); + assert.strictEqual(messages[0].messageId, "extraSemi"); + assert.strictEqual(messages[0].line, 2); + }); + + it("new instance of FlatESLint should use the latest version of the config file (CJS)", async () => { + const cwd = path.join(getFixturePath(), `config_file_${Date.now()}`); + const configFileContent = "module.exports = [{ rules: { semi: ['error', 'always'] } }];"; + const teardown = createCustomTeardown({ + cwd, + files: { + "eslint.config.js": configFileContent, + "a.js": "foo\nbar;" + } + }); + + await teardown.prepare(); + + let eslint = new FlatESLint({ cwd }); + let [{ messages }] = await eslint.lintFiles(["a.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "semi"); + assert.strictEqual(messages[0].messageId, "missingSemi"); + assert.strictEqual(messages[0].line, 1); + + await sleep(100); + await fsp.writeFile(path.join(cwd, "eslint.config.js"), configFileContent.replace("always", "never")); + + eslint = new FlatESLint({ cwd }); + [{ messages }] = await eslint.lintFiles(["a.js"]); + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "semi"); + assert.strictEqual(messages[0].messageId, "extraSemi"); + assert.strictEqual(messages[0].line, 2); + }); + }); + });