]> git.proxmox.com Git - pve-eslint.git/blob - eslint/tests/_utils/in-memory-fs.js
import and build new upstream release 7.2.0
[pve-eslint.git] / eslint / tests / _utils / in-memory-fs.js
1 /**
2 * @fileoverview Define classes what use the in-memory file system.
3 *
4 * This provides utilities to test `ConfigArrayFactory`,
5 * `CascadingConfigArrayFactory`, `FileEnumerator`, `CLIEngine`, and `ESLint`.
6 *
7 * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
8 * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })`
9 * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })`
10 * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })`
11 * - `defineESLintWithInMemoryFileSystem({ cwd, files })`
12 *
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.
16 *
17 * For each test case, it makes more readable if we define minimal files the
18 * test case requires.
19 *
20 * For example:
21 *
22 * ```js
23 * const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({
24 * files: {
25 * "node_modules/eslint-config-foo/index.js": `
26 * module.exports = {
27 * parser: "./parser",
28 * rules: {
29 * "no-undef": "error"
30 * }
31 * }
32 * `,
33 * "node_modules/eslint-config-foo/parser.js": `
34 * module.exports = {
35 * parse() {}
36 * }
37 * `,
38 * ".eslintrc.json": JSON.stringify({ root: true, extends: "foo" })
39 * }
40 * });
41 * const factory = new ConfigArrayFactory();
42 * const config = factory.loadFile(".eslintrc.json");
43 *
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"));
47 *
48 * assert(config[1].name === ".eslintrc.json");
49 * assert(config[1].filePath === path.resolve(".eslintrc.json"));
50 * assert(config[1].root === true);
51 * ```
52 *
53 * @author Toru Nagashima <https://github.com/mysticatea>
54 */
55 "use strict";
56
57 const path = require("path");
58 const vm = require("vm");
59 const { Volume, createFsFromVolume } = require("memfs");
60 const Proxyquire = require("proxyquire/lib/proxyquire");
61
62 const CascadingConfigArrayFactoryPath =
63 require.resolve("../../lib/cli-engine/cascading-config-array-factory");
64 const CLIEnginePath =
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");
70 const LoadRulesPath =
71 require.resolve("../../lib/cli-engine/load-rules");
72 const ESLintPath =
73 require.resolve("../../lib/eslint/eslint");
74 const ESLintAllPath =
75 require.resolve("../../conf/eslint-all");
76 const ESLintRecommendedPath =
77 require.resolve("../../conf/eslint-recommended");
78
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);
85 require(ESLintPath);
86 require("js-yaml");
87 require("espree");
88
89 // Override `_require` in order to throw runtime errors in stubs.
90 const ERRORED = Symbol("errored");
91 const proxyquire = new class extends Proxyquire {
92 _require(...args) {
93 const retv = super._require(...args); // eslint-disable-line no-underscore-dangle
94
95 if (retv[ERRORED]) {
96 throw retv[ERRORED];
97 }
98 return retv;
99 }
100 }(module).noCallThru().noPreserveCache();
101
102 // Separated (sandbox) context to compile fixture files.
103 const context = vm.createContext();
104
105 /**
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.
110 */
111 function isExistingFile(fs, filePath) {
112 try {
113 return fs.statSync(filePath).isFile();
114 } catch {
115 return false;
116 }
117 }
118
119 /**
120 * Get some paths to test.
121 * @param {string} prefix The prefix to try.
122 * @returns {string[]} The paths to test.
123 */
124 function getTestPaths(prefix) {
125 return [
126 path.join(prefix),
127 path.join(`${prefix}.js`),
128 path.join(prefix, "index.js")
129 ];
130 }
131
132 /**
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.
137 */
138 function *iterateCandidatePaths(request, relativeTo) {
139 if (path.isAbsolute(request)) {
140 yield* getTestPaths(request);
141 return;
142 }
143 if (/^\.{1,2}[/\\]/u.test(request)) {
144 yield* getTestPaths(path.resolve(path.dirname(relativeTo), request));
145 return;
146 }
147
148 let prevPath = path.resolve(relativeTo);
149 let dirPath = path.dirname(prevPath);
150
151 while (dirPath && dirPath !== prevPath) {
152 yield* getTestPaths(path.join(dirPath, "node_modules", request));
153 prevPath = dirPath;
154 dirPath = path.dirname(dirPath);
155 }
156 }
157
158 /**
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.
163 * @returns {void}
164 */
165 function fsResolve(fs, request, relativeTo) {
166 for (const filePath of iterateCandidatePaths(request, relativeTo)) {
167 if (isExistingFile(fs, filePath)) {
168 return filePath;
169 }
170 }
171
172 throw Object.assign(
173 new Error(`Cannot find module '${request}'`),
174 { code: "MODULE_NOT_FOUND" }
175 );
176 }
177
178 /**
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.
186 */
187 function compile(fs, stubs, filePath, content) {
188 const code = `(function(exports, require, module, __filename, __dirname) { ${content} })`;
189 const f = vm.runInContext(code, context);
190 const exports = {};
191 const module = { exports };
192
193 f.call(
194 exports,
195 exports,
196 request => {
197 const modulePath = fsResolve(fs, request, filePath);
198 const stub = stubs[modulePath];
199
200 if (stub[ERRORED]) {
201 throw stub[ERRORED];
202 }
203 return stub;
204 },
205 module,
206 filePath,
207 path.dirname(filePath)
208 );
209
210 return module.exports;
211 }
212
213 /**
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.
218 * @returns {void}
219 */
220 function fsImportFresh(fs, stubs, absolutePath) {
221 if (absolutePath === ESLintAllPath) {
222 return require(ESLintAllPath);
223 }
224 if (absolutePath === ESLintRecommendedPath) {
225 return require(ESLintRecommendedPath);
226 }
227
228 if (fs.existsSync(absolutePath)) {
229 return compile(
230 fs,
231 stubs,
232 absolutePath,
233 fs.readFileSync(absolutePath, "utf8")
234 );
235 }
236
237 throw Object.assign(
238 new Error(`Cannot find module '${absolutePath}'`),
239 { code: "MODULE_NOT_FOUND" }
240 );
241 }
242
243 /**
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.
249 */
250 function defineInMemoryFs({
251 cwd = process.cwd,
252 files = {}
253 } = {}) {
254
255 /**
256 * The in-memory file system for this mock.
257 * @type {import("fs")}
258 */
259 const fs = createFsFromVolume(new Volume());
260
261 fs.mkdirSync(cwd(), { recursive: true });
262
263 /*
264 * Write all files to the in-memory file system and compile all JavaScript
265 * files then set to `stubs`.
266 */
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);
271
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 });
277 }
278 fs.writeFileSync(filePath, content);
279 } else {
280 throw new Error(`Invalid content: ${typeof content}`);
281 }
282 }
283 }(cwd(), files));
284
285 return fs;
286 }
287
288 /**
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.
294 */
295 function defineConfigArrayFactoryWithInMemoryFileSystem({
296 cwd = process.cwd,
297 files = {}
298 } = {}) {
299 const fs = defineInMemoryFs({ cwd, files });
300 const RelativeModuleResolver = { resolve: fsResolve.bind(null, fs) };
301
302 /*
303 * Stubs for proxyquire.
304 * This contains the JavaScript files in `options.files`.
305 */
306 const stubs = {};
307
308 stubs.fs = fs;
309 stubs["import-fresh"] = fsImportFresh.bind(null, fs, stubs);
310 stubs["../shared/relative-module-resolver"] = RelativeModuleResolver;
311
312 /*
313 * Write all files to the in-memory file system and compile all JavaScript
314 * files then set to `stubs`.
315 */
316 (function initFiles(directoryPath, definition) {
317 for (const [filename, content] of Object.entries(definition)) {
318 const filePath = path.resolve(directoryPath, filename);
319
320 if (typeof content === "object") {
321 initFiles(filePath, content);
322 continue;
323 }
324
325 /*
326 * Compile then stub if this file is a JavaScript file.
327 * For parsers and plugins that `require()` will import.
328 */
329 if (path.extname(filePath) === ".js") {
330 Object.defineProperty(stubs, filePath, {
331 configurable: true,
332 enumerable: true,
333 get() {
334 let stub;
335
336 try {
337 stub = compile(fs, stubs, filePath, content);
338 } catch (error) {
339 stub = { [ERRORED]: error };
340 }
341 Object.defineProperty(stubs, filePath, {
342 configurable: true,
343 enumerable: true,
344 value: stub
345 });
346
347 return stub;
348 }
349 });
350 }
351 }
352 }(cwd(), files));
353
354 // Load the stubbed one.
355 const { ConfigArrayFactory } = proxyquire(ConfigArrayFactoryPath, stubs);
356
357 // Override the default cwd.
358 return {
359 fs,
360 stubs,
361 RelativeModuleResolver,
362 ConfigArrayFactory: cwd === process.cwd
363 ? ConfigArrayFactory
364 : class extends ConfigArrayFactory {
365 constructor(options) {
366 super({ cwd: cwd(), ...options });
367 }
368 }
369 };
370 }
371
372 /**
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.
378 */
379 function defineCascadingConfigArrayFactoryWithInMemoryFileSystem({
380 cwd = process.cwd,
381 files = {}
382 } = {}) {
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
390 });
391
392 // Override the default cwd.
393 return {
394 fs,
395 RelativeModuleResolver,
396 ConfigArrayFactory,
397 CascadingConfigArrayFactory: cwd === process.cwd
398 ? CascadingConfigArrayFactory
399 : class extends CascadingConfigArrayFactory {
400 constructor(options) {
401 super({ cwd: cwd(), ...options });
402 }
403 }
404 };
405 }
406
407 /**
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.
413 */
414 function defineFileEnumeratorWithInMemoryFileSystem({
415 cwd = process.cwd,
416 files = {}
417 } = {}) {
418 const {
419 fs,
420 RelativeModuleResolver,
421 ConfigArrayFactory,
422 CascadingConfigArrayFactory
423 } =
424 defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files });
425 const { FileEnumerator } = proxyquire(FileEnumeratorPath, {
426 fs,
427 "./cascading-config-array-factory": { CascadingConfigArrayFactory }
428 });
429
430 // Override the default cwd.
431 return {
432 fs,
433 RelativeModuleResolver,
434 ConfigArrayFactory,
435 CascadingConfigArrayFactory,
436 FileEnumerator: cwd === process.cwd
437 ? FileEnumerator
438 : class extends FileEnumerator {
439 constructor(options) {
440 super({ cwd: cwd(), ...options });
441 }
442 }
443 };
444 }
445
446 /**
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.
452 */
453 function defineCLIEngineWithInMemoryFileSystem({
454 cwd = process.cwd,
455 files = {}
456 } = {}) {
457 const {
458 fs,
459 RelativeModuleResolver,
460 ConfigArrayFactory,
461 CascadingConfigArrayFactory,
462 FileEnumerator
463 } =
464 defineFileEnumeratorWithInMemoryFileSystem({ cwd, files });
465 const { CLIEngine, getCLIEngineInternalSlots } = proxyquire(CLIEnginePath, {
466 fs,
467 "./cascading-config-array-factory": { CascadingConfigArrayFactory },
468 "./file-enumerator": { FileEnumerator },
469 "../shared/relative-module-resolver": RelativeModuleResolver
470 });
471
472 // Override the default cwd.
473 return {
474 fs,
475 RelativeModuleResolver,
476 ConfigArrayFactory,
477 CascadingConfigArrayFactory,
478 FileEnumerator,
479 CLIEngine: cwd === process.cwd
480 ? CLIEngine
481 : class extends CLIEngine {
482 constructor(options) {
483 super({ cwd: cwd(), ...options });
484 }
485 },
486 getCLIEngineInternalSlots
487 };
488 }
489
490 /**
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.
496 */
497 function defineESLintWithInMemoryFileSystem({
498 cwd = process.cwd,
499 files = {}
500 } = {}) {
501 const {
502 fs,
503 RelativeModuleResolver,
504 ConfigArrayFactory,
505 CascadingConfigArrayFactory,
506 FileEnumerator,
507 CLIEngine,
508 getCLIEngineInternalSlots
509 } = defineCLIEngineWithInMemoryFileSystem({ cwd, files });
510 const { ESLint, getESLintPrivateMembers } = proxyquire(ESLintPath, {
511 "../cli-engine/cli-engine": { CLIEngine, getCLIEngineInternalSlots }
512 });
513
514 // Override the default cwd.
515 return {
516 fs,
517 RelativeModuleResolver,
518 ConfigArrayFactory,
519 CascadingConfigArrayFactory,
520 FileEnumerator,
521 CLIEngine,
522 getCLIEngineInternalSlots,
523 ESLint: cwd === process.cwd
524 ? ESLint
525 : class extends ESLint {
526 constructor(options) {
527 super({ cwd: cwd(), ...options });
528 }
529 },
530 getESLintPrivateMembers
531 };
532 }
533
534 module.exports = {
535 defineInMemoryFs,
536 defineConfigArrayFactoryWithInMemoryFileSystem,
537 defineCascadingConfigArrayFactoryWithInMemoryFileSystem,
538 defineFileEnumeratorWithInMemoryFileSystem,
539 defineCLIEngineWithInMemoryFileSystem,
540 defineESLintWithInMemoryFileSystem
541 };