From dfb7587724228bbbed3e00c71cd2c894573c1226 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Sat, 26 Apr 2025 15:34:07 +0300 Subject: [PATCH 1/8] Relocate config file to temp directory. The config file was previously stored in the project eslint directory, which could lead to write error in sandbox environments (like Bazel). This commit relocates the config file to the OS's temporary directory, namespaced by a hash of the project eslint directory. --- lib/config.js | 9 ++++++++- lib/config.test.js | 23 +++++++++++++++++++---- lib/daemon.js | 2 +- lib/daemon.test.js | 23 +++++++++++++---------- lib/forwarder.test.js | 13 +++++++++---- lib/launcher.js | 16 +++++++++------- lib/launcher.test.js | 31 ++++++++++++++++++------------- 7 files changed, 77 insertions(+), 40 deletions(-) diff --git a/lib/config.js b/lib/config.js index c0b8328..e79103d 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,5 +1,8 @@ import debug from 'debug'; +import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; const log = debug('eslint_d:config'); @@ -67,5 +70,9 @@ export async function removeConfig(resolver) { * @returns {string} */ export function configFile(resolver) { - return `${resolver.base}/.eslint_d`; + const hash = createHash('sha256'); + + hash.update(`${resolver.base}/.eslint_d`); + + return path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`); } diff --git a/lib/config.test.js b/lib/config.test.js index 0854014..52dd98d 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -1,7 +1,9 @@ +import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; +import os from 'node:os'; import { assert, sinon } from '@sinonjs/referee-sinon'; -import { loadConfig, writeConfig, removeConfig } from './config.js'; +import { configFile, loadConfig, writeConfig, removeConfig } from './config.js'; describe('lib/config', () => { const resolver = { @@ -16,7 +18,7 @@ describe('lib/config', () => { loadConfig(resolver); - assert.calledOnceWith(fs.readFile, 'some/base/.eslint_d', 'utf8'); + assert.calledOnceWith(fs.readFile, configFile(resolver), 'utf8'); }); it('returns parsed config from file content', async () => { @@ -75,7 +77,7 @@ describe('lib/config', () => { await assert.resolves(promise); assert.calledOnceWith( fs.writeFile, - 'some/base/.eslint_d', + configFile(resolver), 'token 123 456 hash' ); }); @@ -88,7 +90,20 @@ describe('lib/config', () => { const promise = removeConfig(resolver); await assert.resolves(promise); - assert.calledOnceWith(fs.unlink, 'some/base/.eslint_d'); + assert.calledOnceWith(fs.unlink, configFile(resolver)); + }); + }); + + context('configFile', () => { + it('returns unique config file path inside temp dir', () => { + const hash = createHash('sha256'); + + hash.update(`${resolver.base}/.eslint_d`); + + assert.equals( + configFile(resolver), + `${os.tmpdir()}/.${hash.digest('hex')}.eslint_d` + ); }); }); }); diff --git a/lib/daemon.js b/lib/daemon.js index f04234d..06b1fd6 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -117,7 +117,7 @@ function shutdown() { function watchConfig() { watcher = fs - .watch(configFile(resolver), { persistent: false }) + .watchFile(configFile(resolver), { persistent: false }) .on('change', (type) => { if (type === 'rename') { log('Shutting down due to config removal'); diff --git a/lib/daemon.test.js b/lib/daemon.test.js index c8c4754..473b894 100644 --- a/lib/daemon.test.js +++ b/lib/daemon.test.js @@ -4,6 +4,7 @@ import { normalize, resolve } from 'node:path'; import crypto from 'node:crypto'; import net from 'node:net'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; describe('lib/daemon', () => { const randome_bytes = Buffer.from('token'); @@ -38,7 +39,7 @@ describe('lib/daemon', () => { ); sinon.replace(fs_promises, 'unlink', sinon.fake.resolves()); watcher = { on: sinon.fake(returnThis), close: sinon.fake() }; - sinon.replace(fs, 'watch', sinon.fake.returns(watcher)); + sinon.replace(fs, 'watchFile', sinon.fake.returns(watcher)); sinon.replace(fs, 'readFile', sinon.fake()); argv = ['node', 'eslint_d', '0', '0', base, 'hash']; sinon.replace(process, 'argv', argv); @@ -59,10 +60,10 @@ describe('lib/daemon', () => { assert.calledOnceWith( fs_promises.writeFile, - `${base}/.eslint_d`, + configFile({ base }), `${randome_bytes.toString('hex')} 1234 ${process.pid} hash` ); - refute.called(fs.watch); + refute.called(fs.watchFile); }); it('watches config file when fs.writeFile succeeded', async () => { @@ -72,7 +73,9 @@ describe('lib/daemon', () => { fs_write_promise.resolve(); await new Promise(setImmediate); - assert.calledOnceWith(fs.watch, `${base}/.eslint_d`, { persistent: false }); + assert.calledOnceWith(fs.watchFile, configFile({ base }), { + persistent: false + }); }); it('logs error runs shutdown if config file write fails', async () => { @@ -82,10 +85,10 @@ describe('lib/daemon', () => { fs_write_promise.reject(new Error('Oh noes!')); await new Promise(setImmediate); - refute.called(fs.watch); + refute.called(fs.watchFile); assert.calledOnceWith(console.error, 'eslint_d: Error: Oh noes!'); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile({ base })); assert.calledOnceWith(process.exit, 0); }); @@ -102,7 +105,7 @@ describe('lib/daemon', () => { assert.calledOnce(watcher.close); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile({ base })); assert.calledOnceWith(process.exit, 0); }); @@ -147,7 +150,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile({ base })); assert.calledOnceWith(process.exit, 0); server.close.resetHistory(); fs_promises.unlink['resetHistory'](); @@ -188,7 +191,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile({ base })); assert.calledOnceWith(process.exit, 0); }); @@ -214,7 +217,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile({ base })); assert.calledOnceWith(process.exit, 0); }); }); diff --git a/lib/forwarder.test.js b/lib/forwarder.test.js index f957329..1bee356 100644 --- a/lib/forwarder.test.js +++ b/lib/forwarder.test.js @@ -2,10 +2,12 @@ import net from 'node:net'; import child_process from 'node:child_process'; import EventEmitter from 'node:events'; import { PassThrough } from 'node:stream'; +import path from 'node:path'; import fs from 'node:fs'; import fs_promises from 'node:fs/promises'; import supportsColor from 'supports-color'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; import { forwardToDaemon, isAlive } from './forwarder.js'; import { createResolver } from './resolver.js'; import { LINT_COMMAND } from './commands.js'; @@ -191,6 +193,7 @@ describe('lib/forwarder', () => { }); it('launch daemon on first connection refused', async () => { + const resolverConfigFile = configFile(resolver); const chunks = ['response ', 'from daemon']; sinon.replace( socket, @@ -207,12 +210,12 @@ describe('lib/forwarder', () => { await new Promise(setImmediate); // removeConfig - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, resolverConfigFile); await new Promise(setImmediate); // spawn assert.calledOnce(child_process.spawn); // watch config - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); await new Promise(setImmediate); // read config read_file_promise.resolve(JSON.stringify(config)); @@ -346,6 +349,8 @@ describe('lib/forwarder', () => { }); it('logs error on second ECONNREFUSED', async () => { + const resolverConfigFile = configFile(resolver); + sinon.replace(fs_promises, 'unlink', sinon.fake.resolves()); forwardToDaemon(resolver, config); @@ -355,12 +360,12 @@ describe('lib/forwarder', () => { await new Promise(setImmediate); // removeConfig - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, resolverConfigFile); await new Promise(setImmediate); // spawn assert.calledOnce(child_process.spawn); // watch config - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); await new Promise(setImmediate); // read config read_file_promise.resolve(JSON.stringify(config)); diff --git a/lib/launcher.js b/lib/launcher.js index d53afee..45bfe04 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -1,8 +1,9 @@ +import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; import child_process from 'node:child_process'; import debug from 'debug'; -import { loadConfig, removeConfig } from './config.js'; +import { configFile, loadConfig, removeConfig } from './config.js'; import { forwardCommandToDaemon } from './forwarder.js'; import { SHUTDOWN_COMMAND } from './commands.js'; @@ -51,7 +52,7 @@ export async function launchDaemon(resolver, hash, is_debug_mode) { daemon_process.unref(); } - await waitForConfig(resolver.base); + await waitForConfig(configFile(resolver)); config = await loadConfig(resolver); } catch (err) { error = err; @@ -72,7 +73,7 @@ export async function stopDaemon(resolver, config) { log('Stopping daemon %o', config); try { await Promise.all([ - waitForConfig(resolver.base), + waitForConfig(configFile(resolver)), platformAwareStopDaemon(config) ]); } catch (err) { @@ -127,16 +128,17 @@ function getIDLE(ppid) { } /** - * @param {string} base + * @param {string} resolverConfigFile * @returns {Promise} */ -function waitForConfig(base) { +function waitForConfig(resolverConfigFile) { return new Promise((resolve, reject) => { + const configBasename = path.basename(resolverConfigFile); let timeout = null; const watcher = fs - .watch(base) + .watch(path.dirname(resolverConfigFile)) .on('change', (type, filename) => { - if (type === 'rename' && filename === '.eslint_d') { + if (type === 'rename' && filename === configBasename) { clearTimeout(timeout); watcher.close(); resolve(); diff --git a/lib/launcher.test.js b/lib/launcher.test.js index 933dab2..aaff5fb 100644 --- a/lib/launcher.test.js +++ b/lib/launcher.test.js @@ -1,3 +1,4 @@ +import path from 'node:path'; import fs from 'node:fs'; import fs_promises from 'node:fs/promises'; import os from 'node:os'; @@ -5,6 +6,7 @@ import net from 'node:net'; import child_process from 'node:child_process'; import EventEmitter from 'node:events'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; import { createResolver } from './resolver.js'; import { launchDaemon, stopDaemon } from './launcher.js'; import { strictEqual } from 'node:assert'; @@ -169,7 +171,7 @@ describe('lib/launcher', () => { assert.calledWith(process.on, 'SIGINT', match.func); process.on['callback'](); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('fails if idle is not a number', () => { @@ -186,14 +188,15 @@ describe('lib/launcher', () => { it('waits for config file to appear and resolves', async () => { const promise = launchDaemon(resolver, 'hash', false); + const resolverConfigFile = configFile(resolver); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await Promise.resolve(); - assert.calledOnceWith(fs_promises.readFile, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.readFile, configFile(resolver)); read_file_promise.resolve('token 123 456 hash'); @@ -223,7 +226,7 @@ describe('lib/launcher', () => { const promise = launchDaemon(resolver, 'hash', false); const error = new Error('read error'); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(configFile(resolver))); read_file_promise.reject(error); await assert.resolves(promise, null); @@ -275,12 +278,13 @@ describe('lib/launcher', () => { it('waits for the config to be removed and resolves', async () => { const promise = stopDaemon(resolver, config); + const resolverConfigFile = configFile(resolver); await new Promise(setImmediate); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await assert.resolves(promise, undefined); @@ -301,13 +305,13 @@ describe('lib/launcher', () => { console.error, 'eslint_d: Error: kill error - removing config' ); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('does not wait for the config file removal', async () => { const promise = stopDaemon(resolver, config); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(configFile(resolver))); await assert.resolves(promise, undefined); }); }); @@ -366,6 +370,7 @@ describe('lib/launcher', () => { it('waits for the config to be removed and resolves', async () => { const promise = stopDaemon(resolver, config); + const resolverConfigFile = configFile(resolver); await new Promise((resolve) => { server.on('connection', (socket) => { @@ -373,9 +378,9 @@ describe('lib/launcher', () => { }); }); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await assert.resolves(promise, undefined); @@ -396,13 +401,13 @@ describe('lib/launcher', () => { console.error, 'eslint_d: Error: kill error - removing config' ); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('does not wait for the config file removal', async () => { const promise = stopDaemon(resolver, config); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(configFile(resolver))); await assert.resolves(promise, undefined); }); }); From 1f408d480dc0b24afccbb631b36fc69ecfbf7af4 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Sat, 26 Apr 2025 15:52:31 +0300 Subject: [PATCH 2/8] Revert back to fs.watch function. --- lib/daemon.js | 2 +- lib/daemon.test.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/daemon.js b/lib/daemon.js index 06b1fd6..f04234d 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -117,7 +117,7 @@ function shutdown() { function watchConfig() { watcher = fs - .watchFile(configFile(resolver), { persistent: false }) + .watch(configFile(resolver), { persistent: false }) .on('change', (type) => { if (type === 'rename') { log('Shutting down due to config removal'); diff --git a/lib/daemon.test.js b/lib/daemon.test.js index 473b894..4827503 100644 --- a/lib/daemon.test.js +++ b/lib/daemon.test.js @@ -39,7 +39,7 @@ describe('lib/daemon', () => { ); sinon.replace(fs_promises, 'unlink', sinon.fake.resolves()); watcher = { on: sinon.fake(returnThis), close: sinon.fake() }; - sinon.replace(fs, 'watchFile', sinon.fake.returns(watcher)); + sinon.replace(fs, 'watch', sinon.fake.returns(watcher)); sinon.replace(fs, 'readFile', sinon.fake()); argv = ['node', 'eslint_d', '0', '0', base, 'hash']; sinon.replace(process, 'argv', argv); @@ -63,7 +63,7 @@ describe('lib/daemon', () => { configFile({ base }), `${randome_bytes.toString('hex')} 1234 ${process.pid} hash` ); - refute.called(fs.watchFile); + refute.called(fs.watch); }); it('watches config file when fs.writeFile succeeded', async () => { @@ -73,7 +73,7 @@ describe('lib/daemon', () => { fs_write_promise.resolve(); await new Promise(setImmediate); - assert.calledOnceWith(fs.watchFile, configFile({ base }), { + assert.calledOnceWith(fs.watch, configFile({ base }), { persistent: false }); }); @@ -85,7 +85,7 @@ describe('lib/daemon', () => { fs_write_promise.reject(new Error('Oh noes!')); await new Promise(setImmediate); - refute.called(fs.watchFile); + refute.called(fs.watch); assert.calledOnceWith(console.error, 'eslint_d: Error: Oh noes!'); assert.calledOnceWith(server.close, match.func); assert.calledOnceWith(fs_promises.unlink, configFile({ base })); From d9b2b0c36a2edbc9eb49b5e1c912234df5b947ed Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Tue, 29 Apr 2025 20:11:16 +0300 Subject: [PATCH 3/8] Fix integration test. --- test/test.integration.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/test.integration.js b/test/test.integration.js index cfe858f..a9872c1 100644 --- a/test/test.integration.js +++ b/test/test.integration.js @@ -1,6 +1,8 @@ import child_process from 'node:child_process'; +import { createHash } from 'node:crypto'; import path from 'node:path'; import fs from 'node:fs/promises'; +import os from 'node:os'; import { createRequire } from 'node:module'; import { assert, refute } from '@sinonjs/referee-sinon'; @@ -87,7 +89,12 @@ describe('integration tests', () => { const { version: eslint_version } = require( require.resolve('eslint/package.json', { paths: [cwd] }) ); - const config = `${cwd}/node_modules/eslint/.eslint_d`; + const eslintPath = `${cwd}/node_modules/eslint`; + const hash = createHash('sha256'); + + hash.update(`${eslintPath}/.eslint_d`); + + const config = path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`); let pid; after(unlinkHook(config)); @@ -191,7 +198,15 @@ describe('integration tests', () => { ).forEach((fixture) => { context(fixture, () => { const cwd = path.resolve(`test/fixture/${fixture}`); - const config = `${cwd}/node_modules/eslint/.eslint_d`; + const eslintPath = `${cwd}/node_modules/eslint`; + const hash = createHash('sha256'); + + hash.update(`${eslintPath}/.eslint_d`); + + const config = path.join( + os.tmpdir(), + `.${hash.digest('hex')}.eslint_d` + ); after(unlinkHook(config)); From ca38e125c813e8913b010bf5a57f2c7829fcf424 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Fri, 2 May 2025 14:30:06 +0300 Subject: [PATCH 4/8] Fix: Use resolver mock in daemon tests. The configFile function in the daemon tests was being called with a base object instead of the resolver mock object. This commit fixes this issue by using the resolverMock object in all calls to the configFile function in the daemon tests. --- lib/daemon.test.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/daemon.test.js b/lib/daemon.test.js index 4827503..b20138d 100644 --- a/lib/daemon.test.js +++ b/lib/daemon.test.js @@ -6,6 +6,10 @@ import net from 'node:net'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; import { configFile } from './config.js'; +/** + * @import {Resolver} from './resolver.js' + */ + describe('lib/daemon', () => { const randome_bytes = Buffer.from('token'); const project = 'test/fixture/v9.0.x'; @@ -16,6 +20,15 @@ describe('lib/daemon', () => { let watcher; let argv; + /** + * @type {Resolver} + */ + const resolverMock = { + base, + bundled: false, + require: sinon.stub() + }; + function returnThis() { // @ts-ignore return this; @@ -60,7 +73,7 @@ describe('lib/daemon', () => { assert.calledOnceWith( fs_promises.writeFile, - configFile({ base }), + configFile(resolverMock), `${randome_bytes.toString('hex')} 1234 ${process.pid} hash` ); refute.called(fs.watch); @@ -73,7 +86,7 @@ describe('lib/daemon', () => { fs_write_promise.resolve(); await new Promise(setImmediate); - assert.calledOnceWith(fs.watch, configFile({ base }), { + assert.calledOnceWith(fs.watch, configFile(resolverMock), { persistent: false }); }); @@ -88,7 +101,7 @@ describe('lib/daemon', () => { refute.called(fs.watch); assert.calledOnceWith(console.error, 'eslint_d: Error: Oh noes!'); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, configFile({ base })); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -105,7 +118,7 @@ describe('lib/daemon', () => { assert.calledOnce(watcher.close); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, configFile({ base })); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -150,7 +163,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, configFile({ base })); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); server.close.resetHistory(); fs_promises.unlink['resetHistory'](); @@ -191,7 +204,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, configFile({ base })); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -217,7 +230,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, configFile({ base })); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); }); From 2150c102ea67eaa7131dd9b3a6d8aff572a88a16 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Fri, 2 May 2025 15:34:54 +0300 Subject: [PATCH 5/8] Fix configFile path construction in unit test. Use path.join to construct the config file path, ensuring compatibility across different operating systems by handling path separators correctly. --- lib/config.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/config.test.js b/lib/config.test.js index 52dd98d..f793f8b 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -2,6 +2,7 @@ import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import os from 'node:os'; +import path from 'node:path'; import { assert, sinon } from '@sinonjs/referee-sinon'; import { configFile, loadConfig, writeConfig, removeConfig } from './config.js'; @@ -102,7 +103,7 @@ describe('lib/config', () => { assert.equals( configFile(resolver), - `${os.tmpdir()}/.${hash.digest('hex')}.eslint_d` + path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`) ); }); }); From d32705d513453e16f1f68fdb209762998e391977 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Tue, 6 May 2025 14:12:19 +0300 Subject: [PATCH 6/8] Fix: Generate valid config file name for all OS. The file name generated for the config was invalid for some OS's. It contained a leading ".", which is valid on *nix, but not on Windows. Also, replaced backslashes with forward slashes in the config SHA256 hash to maintain consistency. --- lib/config.js | 4 ++-- lib/config.test.js | 2 +- test/test.integration.js | 11 ++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/lib/config.js b/lib/config.js index e79103d..9515a6a 100644 --- a/lib/config.js +++ b/lib/config.js @@ -72,7 +72,7 @@ export async function removeConfig(resolver) { export function configFile(resolver) { const hash = createHash('sha256'); - hash.update(`${resolver.base}/.eslint_d`); + hash.update(`${resolver.base.replace('\\', '/')}/.eslint_d`); - return path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`); + return path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); } diff --git a/lib/config.test.js b/lib/config.test.js index f793f8b..d939c6b 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -103,7 +103,7 @@ describe('lib/config', () => { assert.equals( configFile(resolver), - path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`) + path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`) ); }); }); diff --git a/test/test.integration.js b/test/test.integration.js index a9872c1..bd28150 100644 --- a/test/test.integration.js +++ b/test/test.integration.js @@ -92,9 +92,9 @@ describe('integration tests', () => { const eslintPath = `${cwd}/node_modules/eslint`; const hash = createHash('sha256'); - hash.update(`${eslintPath}/.eslint_d`); + hash.update(`${eslintPath.replace('\\', '/')}/.eslint_d`); - const config = path.join(os.tmpdir(), `.${hash.digest('hex')}.eslint_d`); + const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); let pid; after(unlinkHook(config)); @@ -201,12 +201,9 @@ describe('integration tests', () => { const eslintPath = `${cwd}/node_modules/eslint`; const hash = createHash('sha256'); - hash.update(`${eslintPath}/.eslint_d`); + hash.update(`${eslintPath.replace('\\', '/')}/.eslint_d`); - const config = path.join( - os.tmpdir(), - `.${hash.digest('hex')}.eslint_d` - ); + const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); after(unlinkHook(config)); From 1adb7c326a30f40509e7f5e2bd11a71ab11ca1ce Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Tue, 6 May 2025 14:44:06 +0300 Subject: [PATCH 7/8] Fix: Use absolute path for config file hash. The config file path generation was not platform independent causing cache misses on Windows. By using absolute paths, the cache invalidation works correctly. --- lib/config.js | 2 +- lib/config.test.js | 2 +- test/test.integration.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/config.js b/lib/config.js index 9515a6a..5629196 100644 --- a/lib/config.js +++ b/lib/config.js @@ -72,7 +72,7 @@ export async function removeConfig(resolver) { export function configFile(resolver) { const hash = createHash('sha256'); - hash.update(`${resolver.base.replace('\\', '/')}/.eslint_d`); + hash.update(path.resolve(resolver.base).replace('\\', '/')); return path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); } diff --git a/lib/config.test.js b/lib/config.test.js index d939c6b..ea7a493 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -99,7 +99,7 @@ describe('lib/config', () => { it('returns unique config file path inside temp dir', () => { const hash = createHash('sha256'); - hash.update(`${resolver.base}/.eslint_d`); + hash.update(path.resolve(resolver.base)); assert.equals( configFile(resolver), diff --git a/test/test.integration.js b/test/test.integration.js index bd28150..9adfcbb 100644 --- a/test/test.integration.js +++ b/test/test.integration.js @@ -92,7 +92,7 @@ describe('integration tests', () => { const eslintPath = `${cwd}/node_modules/eslint`; const hash = createHash('sha256'); - hash.update(`${eslintPath.replace('\\', '/')}/.eslint_d`); + hash.update(path.resolve(eslintPath).replace('\\', '/')); const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); let pid; @@ -201,7 +201,7 @@ describe('integration tests', () => { const eslintPath = `${cwd}/node_modules/eslint`; const hash = createHash('sha256'); - hash.update(`${eslintPath.replace('\\', '/')}/.eslint_d`); + hash.update(path.resolve(eslintPath).replace('\\', '/')); const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); From d61de9344f3328d68082435b229eb83662ba3602 Mon Sep 17 00:00:00 2001 From: Shlomi Scemes Date: Tue, 6 May 2025 15:03:54 +0300 Subject: [PATCH 8/8] Fix: Use POSIX path for config file name. On Windows, `path.resolve` returns backslash as path separator. This commit replaces backslash to slash for consistent file name. --- lib/config.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/config.test.js b/lib/config.test.js index ea7a493..554f8f8 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -99,7 +99,7 @@ describe('lib/config', () => { it('returns unique config file path inside temp dir', () => { const hash = createHash('sha256'); - hash.update(path.resolve(resolver.base)); + hash.update(path.resolve(resolver.base).replace('\\', '/')); assert.equals( configFile(resolver),