2 * @fileoverview Define classes what use the in-memory file system.
4 * This provides utilities to test `ConfigArrayFactory`,
5 * `CascadingConfigArrayFactory`, `FileEnumerator`, `CLIEngine`, and `ESLint`.
7 * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
8 * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
9 * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })`
10 * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })`
11 * - `defineESLintWithInMemoryFileSystem({ cwd, files })`
13 * Those functions define correspond classes with the in-memory file system.
14 * Those search config files, parsers, and plugins in the `files` option via the
15 * in-memory file system.
17 * For each test case, it makes more readable if we define minimal files the
23 * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({
25 * "node_modules/eslint-config-foo/index.js": `
33 * "node_modules/eslint-config-foo/parser.js": `
38 * ".eslintrc.json": JSON.stringify({ root: true, extends: "foo" })
41 * const factory = new ConfigArrayFactory();
42 * const config = factory.loadFile(".eslintrc.json");
44 * assert(config[0].name === ".eslintrc.json ยป eslint-config-foo");
45 * assert(config[0].filePath === path.resolve("node_modules/eslint-config-foo/index.js"));
46 * assert(config[0].parser.filePath === path.resolve("node_modules/eslint-config-foo/parser.js"));
48 * assert(config[1].name === ".eslintrc.json");
49 * assert(config[1].filePath === path.resolve(".eslintrc.json"));
50 * assert(config[1].root === true);
53 * @author Toru Nagashima <https://github.com/mysticatea>
57 const path
= require("path");
58 const vm
= require("vm");
59 const { Volume
, createFsFromVolume
} = require("memfs");
60 const Proxyquire
= require("proxyquire/lib/proxyquire");
62 const CascadingConfigArrayFactoryPath
=
63 require
.resolve("../../lib/cli-engine/cascading-config-array-factory");
65 require
.resolve("../../lib/cli-engine/cli-engine");
66 const ConfigArrayFactoryPath
=
67 require
.resolve("../../lib/cli-engine/config-array-factory");
68 const FileEnumeratorPath
=
69 require
.resolve("../../lib/cli-engine/file-enumerator");
71 require
.resolve("../../lib/cli-engine/load-rules");
73 require
.resolve("../../lib/eslint/eslint");
75 require
.resolve("../../conf/eslint-all");
76 const ESLintRecommendedPath
=
77 require
.resolve("../../conf/eslint-recommended");
79 // Ensure the needed files has been loaded and cached.
80 require(CascadingConfigArrayFactoryPath
);
81 require(CLIEnginePath
);
82 require(ConfigArrayFactoryPath
);
83 require(FileEnumeratorPath
);
84 require(LoadRulesPath
);
89 // Override `_require` in order to throw runtime errors in stubs.
90 const ERRORED
= Symbol("errored");
91 const proxyquire
= new class extends Proxyquire
{
93 const retv
= super._require(...args
); // eslint-disable-line no-underscore-dangle
100 }(module
).noCallThru().noPreserveCache();
102 // Separated (sandbox) context to compile fixture files.
103 const context
= vm
.createContext();
106 * Check if a given path is an existing file.
107 * @param {import("fs")} fs The file system.
108 * @param {string} filePath Tha path to a file to check.
109 * @returns {boolean} `true` if the file existed.
111 function isExistingFile(fs
, filePath
) {
113 return fs
.statSync(filePath
).isFile();
120 * Get some paths to test.
121 * @param {string} prefix The prefix to try.
122 * @returns {string[]} The paths to test.
124 function getTestPaths(prefix
) {
127 path
.join(`${prefix}.js`),
128 path
.join(prefix
, "index.js")
133 * Iterate the candidate paths of a given request to resolve.
134 * @param {string} request Tha package name or file path to resolve.
135 * @param {string} relativeTo Tha path to the file what called this resolving.
136 * @returns {IterableIterator<string>} The candidate paths.
138 function *iterateCandidatePaths(request
, relativeTo
) {
139 if (path
.isAbsolute(request
)) {
140 yield* getTestPaths(request
);
143 if (/^\.{1,2}[/\\]/u
.test(request
)) {
144 yield* getTestPaths(path
.resolve(path
.dirname(relativeTo
), request
));
148 let prevPath
= path
.resolve(relativeTo
);
149 let dirPath
= path
.dirname(prevPath
);
151 while (dirPath
&& dirPath
!== prevPath
) {
152 yield* getTestPaths(path
.join(dirPath
, "node_modules", request
));
154 dirPath
= path
.dirname(dirPath
);
159 * Resolve a given module name or file path relatively in the given file system.
160 * @param {import("fs")} fs The file system.
161 * @param {string} request Tha package name or file path to resolve.
162 * @param {string} relativeTo Tha path to the file what called this resolving.
165 function fsResolve(fs
, request
, relativeTo
) {
166 for (const filePath
of iterateCandidatePaths(request
, relativeTo
)) {
167 if (isExistingFile(fs
, filePath
)) {
173 new Error(`Cannot find module '${request}'`),
174 { code
: "MODULE_NOT_FOUND" }
179 * Compile a JavaScript file.
180 * This is used to compile only fixture files, so this is minimam.
181 * @param {import("fs")} fs The file system.
182 * @param {Object} stubs The stubs.
183 * @param {string} filePath The path to a JavaScript file to compile.
184 * @param {string} content The source code to compile.
185 * @returns {any} The exported value.
187 function compile(fs
, stubs
, filePath
, content
) {
188 const code
= `(function(exports, require, module, __filename, __dirname) { ${content} })`;
189 const f
= vm
.runInContext(code
, context
);
191 const module
= { exports
};
197 const modulePath
= fsResolve(fs
, request
, filePath
);
198 const stub
= stubs
[modulePath
];
207 path
.dirname(filePath
)
210 return module
.exports
;
214 * Import a given file path in the given file system.
215 * @param {import("fs")} fs The file system.
216 * @param {Object} stubs The stubs.
217 * @param {string} absolutePath Tha file path to import.
220 function fsImportFresh(fs
, stubs
, absolutePath
) {
221 if (absolutePath
=== ESLintAllPath
) {
222 return require(ESLintAllPath
);
224 if (absolutePath
=== ESLintRecommendedPath
) {
225 return require(ESLintRecommendedPath
);
228 if (fs
.existsSync(absolutePath
)) {
233 fs
.readFileSync(absolutePath
, "utf8")
238 new Error(`Cannot find module '${absolutePath}'`),
239 { code
: "MODULE_NOT_FOUND" }
244 * Define in-memory file system.
245 * @param {Object} options The options.
246 * @param {() => string} [options.cwd] The current working directory.
247 * @param {Object} [options.files] The initial files definition in the in-memory file system.
248 * @returns {import("fs")} The stubbed `ConfigArrayFactory` class.
250 function defineInMemoryFs({
256 * The in-memory file system for this mock.
257 * @type {import("fs")}
259 const fs
= createFsFromVolume(new Volume());
261 fs
.mkdirSync(cwd(), { recursive
: true });
264 * Write all files to the in-memory file system and compile all JavaScript
265 * files then set to `stubs`.
267 (function initFiles(directoryPath
, definition
) {
268 for (const [filename
, content
] of Object
.entries(definition
)) {
269 const filePath
= path
.resolve(directoryPath
, filename
);
270 const parentPath
= path
.dirname(filePath
);
272 if (typeof content
=== "object") {
273 initFiles(filePath
, content
);
274 } else if (typeof content
=== "string") {
275 if (!fs
.existsSync(parentPath
)) {
276 fs
.mkdirSync(parentPath
, { recursive
: true });
278 fs
.writeFileSync(filePath
, content
);
280 throw new Error(`Invalid content: ${typeof content}`);
289 * Define stubbed `ConfigArrayFactory` class what uses the in-memory file system.
290 * @param {Object} options The options.
291 * @param {() => string} [options.cwd] The current working directory.
292 * @param {Object} [options.files] The initial files definition in the in-memory file system.
293 * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class.
295 function defineConfigArrayFactoryWithInMemoryFileSystem({
299 const fs
= defineInMemoryFs({ cwd
, files
});
300 const RelativeModuleResolver
= { resolve
: fsResolve
.bind(null, fs
) };
303 * Stubs for proxyquire.
304 * This contains the JavaScript files in `options.files`.
309 stubs
["import-fresh"] = fsImportFresh
.bind(null, fs
, stubs
);
310 stubs
["../shared/relative-module-resolver"] = RelativeModuleResolver
;
313 * Write all files to the in-memory file system and compile all JavaScript
314 * files then set to `stubs`.
316 (function initFiles(directoryPath
, definition
) {
317 for (const [filename
, content
] of Object
.entries(definition
)) {
318 const filePath
= path
.resolve(directoryPath
, filename
);
320 if (typeof content
=== "object") {
321 initFiles(filePath
, content
);
326 * Compile then stub if this file is a JavaScript file.
327 * For parsers and plugins that `require()` will import.
329 if (path
.extname(filePath
) === ".js") {
330 Object
.defineProperty(stubs
, filePath
, {
337 stub
= compile(fs
, stubs
, filePath
, content
);
339 stub
= { [ERRORED
]: error
};
341 Object
.defineProperty(stubs
, filePath
, {
354 // Load the stubbed one.
355 const { ConfigArrayFactory
} = proxyquire(ConfigArrayFactoryPath
, stubs
);
357 // Override the default cwd.
361 RelativeModuleResolver
,
362 ConfigArrayFactory
: cwd
=== process
.cwd
364 : class extends ConfigArrayFactory
{
365 constructor(options
) {
366 super({ cwd
: cwd(), ...options
});
373 * Define stubbed `CascadingConfigArrayFactory` class what uses the in-memory file system.
374 * @param {Object} options The options.
375 * @param {() => string} [options.cwd] The current working directory.
376 * @param {Object} [options.files] The initial files definition in the in-memory file system.
377 * @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.
379 function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({
383 const { fs
, stubs
, RelativeModuleResolver
, ConfigArrayFactory
} =
384 defineConfigArrayFactoryWithInMemoryFileSystem({ cwd
, files
});
385 const loadRules
= proxyquire(LoadRulesPath
, stubs
);
386 const { CascadingConfigArrayFactory
} =
387 proxyquire(CascadingConfigArrayFactoryPath
, {
388 "./config-array-factory": { ConfigArrayFactory
},
389 "./load-rules": loadRules
392 // Override the default cwd.
395 RelativeModuleResolver
,
397 CascadingConfigArrayFactory
: cwd
=== process
.cwd
398 ? CascadingConfigArrayFactory
399 : class extends CascadingConfigArrayFactory
{
400 constructor(options
) {
401 super({ cwd
: cwd(), ...options
});
408 * Define stubbed `FileEnumerator` class what uses the in-memory file system.
409 * @param {Object} options The options.
410 * @param {() => string} [options.cwd] The current working directory.
411 * @param {Object} [options.files] The initial files definition in the in-memory file system.
412 * @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.
414 function defineFileEnumeratorWithInMemoryFileSystem({
420 RelativeModuleResolver
,
422 CascadingConfigArrayFactory
424 defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd
, files
});
425 const { FileEnumerator
} = proxyquire(FileEnumeratorPath
, {
427 "./cascading-config-array-factory": { CascadingConfigArrayFactory
}
430 // Override the default cwd.
433 RelativeModuleResolver
,
435 CascadingConfigArrayFactory
,
436 FileEnumerator
: cwd
=== process
.cwd
438 : class extends FileEnumerator
{
439 constructor(options
) {
440 super({ cwd
: cwd(), ...options
});
447 * Define stubbed `CLIEngine` class what uses the in-memory file system.
448 * @param {Object} options The options.
449 * @param {() => string} [options.cwd] The current working directory.
450 * @param {Object} [options.files] The initial files definition in the in-memory file system.
451 * @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.
453 function defineCLIEngineWithInMemoryFileSystem({
459 RelativeModuleResolver
,
461 CascadingConfigArrayFactory
,
464 defineFileEnumeratorWithInMemoryFileSystem({ cwd
, files
});
465 const { CLIEngine
, getCLIEngineInternalSlots
} = proxyquire(CLIEnginePath
, {
467 "./cascading-config-array-factory": { CascadingConfigArrayFactory
},
468 "./file-enumerator": { FileEnumerator
},
469 "../shared/relative-module-resolver": RelativeModuleResolver
472 // Override the default cwd.
475 RelativeModuleResolver
,
477 CascadingConfigArrayFactory
,
479 CLIEngine
: cwd
=== process
.cwd
481 : class extends CLIEngine
{
482 constructor(options
) {
483 super({ cwd
: cwd(), ...options
});
486 getCLIEngineInternalSlots
491 * Define stubbed `ESLint` class that uses the in-memory file system.
492 * @param {Object} options The options.
493 * @param {() => string} [options.cwd] The current working directory.
494 * @param {Object} [options.files] The initial files definition in the in-memory file system.
495 * @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.
497 function defineESLintWithInMemoryFileSystem({
503 RelativeModuleResolver
,
505 CascadingConfigArrayFactory
,
508 getCLIEngineInternalSlots
509 } = defineCLIEngineWithInMemoryFileSystem({ cwd
, files
});
510 const { ESLint
, getESLintPrivateMembers
} = proxyquire(ESLintPath
, {
511 "../cli-engine/cli-engine": { CLIEngine
, getCLIEngineInternalSlots
}
514 // Override the default cwd.
517 RelativeModuleResolver
,
519 CascadingConfigArrayFactory
,
522 getCLIEngineInternalSlots
,
523 ESLint
: cwd
=== process
.cwd
525 : class extends ESLint
{
526 constructor(options
) {
527 super({ cwd
: cwd(), ...options
});
530 getESLintPrivateMembers
536 defineConfigArrayFactoryWithInMemoryFileSystem
,
537 defineCascadingConfigArrayFactoryWithInMemoryFileSystem
,
538 defineFileEnumeratorWithInMemoryFileSystem
,
539 defineCLIEngineWithInMemoryFileSystem
,
540 defineESLintWithInMemoryFileSystem