Skip to content

Commit

Permalink
fix: new instance of FlatESLint should load latest config file version (
Browse files Browse the repository at this point in the history
#16608)

* fix: new instance of FlatESLint should load latest config file version

* clear  require.cache

* add delays
  • Loading branch information
mdjermanovic committed Dec 29, 2022
1 parent 8d93081 commit 87b2470
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
38 changes: 37 additions & 1 deletion lib/eslint/flat-eslint.js
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

/**
Expand Down
80 changes: 80 additions & 0 deletions tests/lib/eslint/flat-eslint.js
Expand Up @@ -11,6 +11,7 @@
//------------------------------------------------------------------------------

const assert = require("assert");
const util = require("util");
const fs = require("fs");
const fsp = fs.promises;
const os = require("os");
Expand Down Expand Up @@ -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
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -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);
});
});

});

0 comments on commit 87b2470

Please sign in to comment.