/** * @fileoverview Define classes what use the in-memory file system. * * This provides utilities to test `ConfigArrayFactory`, * `CascadingConfigArrayFactory`, `FileEnumerator`, `CLIEngine`, and `ESLint`. * * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })` * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })` * - `defineESLintWithInMemoryFileSystem({ cwd, files })` * * Those functions define correspond classes with the in-memory file system. * Those search config files, parsers, and plugins in the `files` option via the * in-memory file system. * * For each test case, it makes more readable if we define minimal files the * test case requires. * * For example: * * ```js * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ * files: { * "node_modules/eslint-config-foo/index.js": ` * module.exports = { * parser: "./parser", * rules: { * "no-undef": "error" * } * } * `, * "node_modules/eslint-config-foo/parser.js": ` * module.exports = { * parse() {} * } * `, * ".eslintrc.json": JSON.stringify({ root: true, extends: "foo" }) * } * }); * const factory = new ConfigArrayFactory(); * const config = factory.loadFile(".eslintrc.json"); * * assert(config[0].name === ".eslintrc.json ยป eslint-config-foo"); * assert(config[0].filePath === path.resolve("node_modules/eslint-config-foo/index.js")); * assert(config[0].parser.filePath === path.resolve("node_modules/eslint-config-foo/parser.js")); * * assert(config[1].name === ".eslintrc.json"); * assert(config[1].filePath === path.resolve(".eslintrc.json")); * assert(config[1].root === true); * ``` * * @author Toru Nagashima */ "use strict"; const path = require("path"); const vm = require("vm"); const { Volume, createFsFromVolume } = require("memfs"); const Proxyquire = require("proxyquire/lib/proxyquire"); const CascadingConfigArrayFactoryPath = require.resolve("../../lib/cli-engine/cascading-config-array-factory"); const CLIEnginePath = require.resolve("../../lib/cli-engine/cli-engine"); const ConfigArrayFactoryPath = require.resolve("../../lib/cli-engine/config-array-factory"); const FileEnumeratorPath = require.resolve("../../lib/cli-engine/file-enumerator"); const LoadRulesPath = require.resolve("../../lib/cli-engine/load-rules"); const ESLintPath = require.resolve("../../lib/eslint/eslint"); const ESLintAllPath = require.resolve("../../conf/eslint-all"); const ESLintRecommendedPath = require.resolve("../../conf/eslint-recommended"); // Ensure the needed files has been loaded and cached. require(CascadingConfigArrayFactoryPath); require(CLIEnginePath); require(ConfigArrayFactoryPath); require(FileEnumeratorPath); require(LoadRulesPath); require(ESLintPath); require("js-yaml"); require("espree"); // Override `_require` in order to throw runtime errors in stubs. const ERRORED = Symbol("errored"); const proxyquire = new class extends Proxyquire { _require(...args) { const retv = super._require(...args); // eslint-disable-line no-underscore-dangle if (retv[ERRORED]) { throw retv[ERRORED]; } return retv; } }(module).noCallThru().noPreserveCache(); // Separated (sandbox) context to compile fixture files. const context = vm.createContext(); /** * Check if a given path is an existing file. * @param {import("fs")} fs The file system. * @param {string} filePath Tha path to a file to check. * @returns {boolean} `true` if the file existed. */ function isExistingFile(fs, filePath) { try { return fs.statSync(filePath).isFile(); } catch { return false; } } /** * Get some paths to test. * @param {string} prefix The prefix to try. * @returns {string[]} The paths to test. */ function getTestPaths(prefix) { return [ path.join(prefix), path.join(`${prefix}.js`), path.join(prefix, "index.js") ]; } /** * Iterate the candidate paths of a given request to resolve. * @param {string} request Tha package name or file path to resolve. * @param {string} relativeTo Tha path to the file what called this resolving. * @returns {IterableIterator} The candidate paths. */ function *iterateCandidatePaths(request, relativeTo) { if (path.isAbsolute(request)) { yield* getTestPaths(request); return; } if (/^\.{1,2}[/\\]/u.test(request)) { yield* getTestPaths(path.resolve(path.dirname(relativeTo), request)); return; } let prevPath = path.resolve(relativeTo); let dirPath = path.dirname(prevPath); while (dirPath && dirPath !== prevPath) { yield* getTestPaths(path.join(dirPath, "node_modules", request)); prevPath = dirPath; dirPath = path.dirname(dirPath); } } /** * Resolve a given module name or file path relatively in the given file system. * @param {import("fs")} fs The file system. * @param {string} request Tha package name or file path to resolve. * @param {string} relativeTo Tha path to the file what called this resolving. * @returns {void} */ function fsResolve(fs, request, relativeTo) { for (const filePath of iterateCandidatePaths(request, relativeTo)) { if (isExistingFile(fs, filePath)) { return filePath; } } throw Object.assign( new Error(`Cannot find module '${request}'`), { code: "MODULE_NOT_FOUND" } ); } /** * Compile a JavaScript file. * This is used to compile only fixture files, so this is minimam. * @param {import("fs")} fs The file system. * @param {Object} stubs The stubs. * @param {string} filePath The path to a JavaScript file to compile. * @param {string} content The source code to compile. * @returns {any} The exported value. */ function compile(fs, stubs, filePath, content) { const code = `(function(exports, require, module, __filename, __dirname) { ${content} })`; const f = vm.runInContext(code, context); const exports = {}; const module = { exports }; f.call( exports, exports, request => { const modulePath = fsResolve(fs, request, filePath); const stub = stubs[modulePath]; if (stub[ERRORED]) { throw stub[ERRORED]; } return stub; }, module, filePath, path.dirname(filePath) ); return module.exports; } /** * Import a given file path in the given file system. * @param {import("fs")} fs The file system. * @param {Object} stubs The stubs. * @param {string} absolutePath Tha file path to import. * @returns {void} */ function fsImportFresh(fs, stubs, absolutePath) { if (absolutePath === ESLintAllPath) { return require(ESLintAllPath); } if (absolutePath === ESLintRecommendedPath) { return require(ESLintRecommendedPath); } if (fs.existsSync(absolutePath)) { return compile( fs, stubs, absolutePath, fs.readFileSync(absolutePath, "utf8") ); } throw Object.assign( new Error(`Cannot find module '${absolutePath}'`), { code: "MODULE_NOT_FOUND" } ); } /** * Define in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {import("fs")} The stubbed `ConfigArrayFactory` class. */ function defineInMemoryFs({ cwd = process.cwd, files = {} } = {}) { /** * The in-memory file system for this mock. * @type {import("fs")} */ const fs = createFsFromVolume(new Volume()); fs.mkdirSync(cwd(), { recursive: true }); /* * Write all files to the in-memory file system and compile all JavaScript * files then set to `stubs`. */ (function initFiles(directoryPath, definition) { for (const [filename, content] of Object.entries(definition)) { const filePath = path.resolve(directoryPath, filename); const parentPath = path.dirname(filePath); if (typeof content === "object") { initFiles(filePath, content); } else if (typeof content === "string") { if (!fs.existsSync(parentPath)) { fs.mkdirSync(parentPath, { recursive: true }); } fs.writeFileSync(filePath, content); } else { throw new Error(`Invalid content: ${typeof content}`); } } }(cwd(), files)); return fs; } /** * Define stubbed `ConfigArrayFactory` class what uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. */ function defineConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const fs = defineInMemoryFs({ cwd, files }); const RelativeModuleResolver = { resolve: fsResolve.bind(null, fs) }; /* * Stubs for proxyquire. * This contains the JavaScript files in `options.files`. */ const stubs = {}; stubs.fs = fs; stubs["import-fresh"] = fsImportFresh.bind(null, fs, stubs); stubs["../shared/relative-module-resolver"] = RelativeModuleResolver; /* * Write all files to the in-memory file system and compile all JavaScript * files then set to `stubs`. */ (function initFiles(directoryPath, definition) { for (const [filename, content] of Object.entries(definition)) { const filePath = path.resolve(directoryPath, filename); if (typeof content === "object") { initFiles(filePath, content); continue; } /* * Compile then stub if this file is a JavaScript file. * For parsers and plugins that `require()` will import. */ if (path.extname(filePath) === ".js") { Object.defineProperty(stubs, filePath, { configurable: true, enumerable: true, get() { let stub; try { stub = compile(fs, stubs, filePath, content); } catch (error) { stub = { [ERRORED]: error }; } Object.defineProperty(stubs, filePath, { configurable: true, enumerable: true, value: stub }); return stub; } }); } } }(cwd(), files)); // Load the stubbed one. const { ConfigArrayFactory } = proxyquire(ConfigArrayFactoryPath, stubs); // Override the default cwd. return { fs, stubs, RelativeModuleResolver, ConfigArrayFactory: cwd === process.cwd ? ConfigArrayFactory : class extends ConfigArrayFactory { constructor(options) { super({ cwd: cwd(), ...options }); } } }; } /** * Define stubbed `CascadingConfigArrayFactory` class what uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"] }} The stubbed `CascadingConfigArrayFactory` class. */ function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const { fs, stubs, RelativeModuleResolver, ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files }); const loadRules = proxyquire(LoadRulesPath, stubs); const { CascadingConfigArrayFactory } = proxyquire(CascadingConfigArrayFactoryPath, { "./config-array-factory": { ConfigArrayFactory }, "./load-rules": loadRules }); // Override the default cwd. return { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory: cwd === process.cwd ? CascadingConfigArrayFactory : class extends CascadingConfigArrayFactory { constructor(options) { super({ cwd: cwd(), ...options }); } } }; } /** * Define stubbed `FileEnumerator` class what uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"] }} The stubbed `FileEnumerator` class. */ function defineFileEnumeratorWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory } = defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files }); const { FileEnumerator } = proxyquire(FileEnumeratorPath, { fs, "./cascading-config-array-factory": { CascadingConfigArrayFactory } }); // Override the default cwd. return { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator: cwd === process.cwd ? FileEnumerator : class extends FileEnumerator { constructor(options) { super({ cwd: cwd(), ...options }); } } }; } /** * Define stubbed `CLIEngine` class what uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../../lib/cli-engine/file-enumerator")["FileEnumerator"], CLIEngine: import("../../../lib/cli-engine/cli-engine")["CLIEngine"], getCLIEngineInternalSlots: import("../../../lib/cli-engine/cli-engine")["getCLIEngineInternalSlots"] }} The stubbed `CLIEngine` class. */ function defineCLIEngineWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator } = defineFileEnumeratorWithInMemoryFileSystem({ cwd, files }); const { CLIEngine, getCLIEngineInternalSlots } = proxyquire(CLIEnginePath, { fs, "./cascading-config-array-factory": { CascadingConfigArrayFactory }, "./file-enumerator": { FileEnumerator }, "../shared/relative-module-resolver": RelativeModuleResolver }); // Override the default cwd. return { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator, CLIEngine: cwd === process.cwd ? CLIEngine : class extends CLIEngine { constructor(options) { super({ cwd: cwd(), ...options }); } }, getCLIEngineInternalSlots }; } /** * Define stubbed `ESLint` class that uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../lib/cli-engine/file-enumerator")["FileEnumerator"], ESLint: import("../../lib/eslint/eslint")["ESLint"], getCLIEngineInternalSlots: import("../../lib//eslint/eslint")["getESLintInternalSlots"] }} The stubbed `ESLint` class. */ function defineESLintWithInMemoryFileSystem({ cwd = process.cwd, files = {} } = {}) { const { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator, CLIEngine, getCLIEngineInternalSlots } = defineCLIEngineWithInMemoryFileSystem({ cwd, files }); const { ESLint, getESLintPrivateMembers } = proxyquire(ESLintPath, { "../cli-engine/cli-engine": { CLIEngine, getCLIEngineInternalSlots } }); // Override the default cwd. return { fs, RelativeModuleResolver, ConfigArrayFactory, CascadingConfigArrayFactory, FileEnumerator, CLIEngine, getCLIEngineInternalSlots, ESLint: cwd === process.cwd ? ESLint : class extends ESLint { constructor(options) { super({ cwd: cwd(), ...options }); } }, getESLintPrivateMembers }; } module.exports = { defineInMemoryFs, defineConfigArrayFactoryWithInMemoryFileSystem, defineCascadingConfigArrayFactoryWithInMemoryFileSystem, defineFileEnumeratorWithInMemoryFileSystem, defineCLIEngineWithInMemoryFileSystem, defineESLintWithInMemoryFileSystem };