eslint 9.0.0-alpha.1 → 9.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,7 +117,7 @@ Yes, ESLint natively supports parsing JSX syntax (this must be enabled in [confi
117
117
 
118
118
  ### What ECMAScript versions does ESLint support?
119
119
 
120
- ESLint has full support for ECMAScript 3, 5 (default), 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, and 2023. You can set your desired ECMAScript syntax (and other settings, like global variables or your target environments) through [configuration](https://eslint.org/docs/latest/use/configure).
120
+ ESLint has full support for ECMAScript 3, 5, and every year from 2015 up until the most recent stage 4 specification (the default). You can set your desired ECMAScript syntax and other settings (like global variables) through [configuration](https://eslint.org/docs/latest/use/configure).
121
121
 
122
122
  ### What about experimental features?
123
123
 
@@ -613,6 +613,13 @@ class ESLint {
613
613
  });
614
614
  }
615
615
 
616
+ // Check for the .eslintignore file, and warn if it's present.
617
+ if (existsSync(path.resolve(processedOptions.cwd, ".eslintignore"))) {
618
+ process.emitWarning(
619
+ "The \".eslintignore\" file is no longer supported. Switch to using the \"ignores\" property in \"eslint.config.js\": https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files",
620
+ "ESLintIgnoreWarning"
621
+ );
622
+ }
616
623
  }
617
624
 
618
625
  /**
@@ -177,8 +177,8 @@ class CodePath {
177
177
  // tracks the traversal steps
178
178
  const stack = [[startSegment, 0]];
179
179
 
180
- // tracks the last skipped segment during traversal
181
- let skippedSegment = null;
180
+ // segments that have been skipped during traversal
181
+ const skipped = new Set();
182
182
 
183
183
  // indicates if we exited early from the traversal
184
184
  let broken = false;
@@ -193,11 +193,7 @@ class CodePath {
193
193
  * @returns {void}
194
194
  */
195
195
  skip() {
196
- if (stack.length <= 1) {
197
- broken = true;
198
- } else {
199
- skippedSegment = stack.at(-2)[0];
200
- }
196
+ skipped.add(segment);
201
197
  },
202
198
 
203
199
  /**
@@ -222,6 +218,18 @@ class CodePath {
222
218
  );
223
219
  }
224
220
 
221
+ /**
222
+ * Checks if a given previous segment has been skipped.
223
+ * @param {CodePathSegment} prevSegment A previous segment to check.
224
+ * @returns {boolean} `true` if the segment has been skipped.
225
+ */
226
+ function isSkipped(prevSegment) {
227
+ return (
228
+ skipped.has(prevSegment) ||
229
+ segment.isLoopedPrevSegment(prevSegment)
230
+ );
231
+ }
232
+
225
233
  // the traversal
226
234
  while (stack.length > 0) {
227
235
 
@@ -258,17 +266,21 @@ class CodePath {
258
266
  continue;
259
267
  }
260
268
 
261
- // Reset the skipping flag if all branches have been skipped.
262
- if (skippedSegment && segment.prevSegments.includes(skippedSegment)) {
263
- skippedSegment = null;
264
- }
265
269
  visited.add(segment);
266
270
 
271
+
272
+ // Skips the segment if all previous segments have been skipped.
273
+ const shouldSkip = (
274
+ skipped.size > 0 &&
275
+ segment.prevSegments.length > 0 &&
276
+ segment.prevSegments.every(isSkipped)
277
+ );
278
+
267
279
  /*
268
280
  * If the most recent segment hasn't been skipped, then we call
269
281
  * the callback, passing in the segment and the controller.
270
282
  */
271
- if (!skippedSegment) {
283
+ if (!shouldSkip) {
272
284
  resolvedCallback.call(this, segment, controller);
273
285
 
274
286
  // exit if we're at the last segment
@@ -284,6 +296,10 @@ class CodePath {
284
296
  if (broken) {
285
297
  break;
286
298
  }
299
+ } else {
300
+
301
+ // If the most recent segment has been skipped, then mark it as skipped.
302
+ skipped.add(segment);
287
303
  }
288
304
  }
289
305
 
@@ -13,10 +13,12 @@
13
13
  const
14
14
  assert = require("assert"),
15
15
  util = require("util"),
16
+ path = require("path"),
16
17
  equal = require("fast-deep-equal"),
17
18
  Traverser = require("../shared/traverser"),
18
19
  { getRuleOptionsSchema } = require("../config/flat-config-helpers"),
19
- { Linter, SourceCodeFixer, interpolate } = require("../linter");
20
+ { Linter, SourceCodeFixer, interpolate } = require("../linter"),
21
+ stringify = require("json-stable-stringify-without-jsonify");
20
22
 
21
23
  const { FlatConfigArray } = require("../config/flat-config-array");
22
24
  const { defaultConfig } = require("../config/default-config");
@@ -26,6 +28,7 @@ const ajv = require("../shared/ajv")({ strictDefaults: true });
26
28
  const parserSymbol = Symbol.for("eslint.RuleTester.parser");
27
29
  const { SourceCode } = require("../source-code");
28
30
  const { ConfigArraySymbol } = require("@humanwhocodes/config-array");
31
+ const { isSerializable } = require("../shared/serialization");
29
32
 
30
33
  //------------------------------------------------------------------------------
31
34
  // Typedefs
@@ -499,6 +502,9 @@ class RuleTester {
499
502
  linter = this.linter,
500
503
  ruleId = `rule-to-test/${ruleName}`;
501
504
 
505
+ const seenValidTestCases = new Set();
506
+ const seenInvalidTestCases = new Set();
507
+
502
508
  if (!rule || typeof rule !== "object" || typeof rule.create !== "function") {
503
509
  throw new TypeError("Rule must be an object with a `create` method");
504
510
  }
@@ -577,7 +583,15 @@ class RuleTester {
577
583
  * @private
578
584
  */
579
585
  function runRuleForItem(item) {
580
- const configs = new FlatConfigArray(testerConfig, { baseConfig });
586
+ const flatConfigArrayOptions = {
587
+ baseConfig
588
+ };
589
+
590
+ if (item.filename) {
591
+ flatConfigArrayOptions.basePath = path.parse(item.filename).root;
592
+ }
593
+
594
+ const configs = new FlatConfigArray(testerConfig, flatConfigArrayOptions);
581
595
 
582
596
  /*
583
597
  * Modify the returned config so that the parser is wrapped to catch
@@ -794,6 +808,32 @@ class RuleTester {
794
808
  }
795
809
  }
796
810
 
811
+ /**
812
+ * Check if this test case is a duplicate of one we have seen before.
813
+ * @param {Object} item test case object
814
+ * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
815
+ * @returns {void}
816
+ * @private
817
+ */
818
+ function checkDuplicateTestCase(item, seenTestCases) {
819
+ if (!isSerializable(item)) {
820
+
821
+ /*
822
+ * If we can't serialize a test case (because it contains a function, RegExp, etc), skip the check.
823
+ * This might happen with properties like: options, plugins, settings, languageOptions.parser, languageOptions.parserOptions.
824
+ */
825
+ return;
826
+ }
827
+
828
+ const serializedTestCase = stringify(item);
829
+
830
+ assert(
831
+ !seenTestCases.has(serializedTestCase),
832
+ "detected duplicate test case"
833
+ );
834
+ seenTestCases.add(serializedTestCase);
835
+ }
836
+
797
837
  /**
798
838
  * Check if the template is valid or not
799
839
  * all valid cases go through this
@@ -809,6 +849,8 @@ class RuleTester {
809
849
  assert.ok(typeof item.name === "string", "Optional test case property 'name' must be a string");
810
850
  }
811
851
 
852
+ checkDuplicateTestCase(item, seenValidTestCases);
853
+
812
854
  const result = runRuleForItem(item);
813
855
  const messages = result.messages;
814
856
 
@@ -860,6 +902,8 @@ class RuleTester {
860
902
  assert.fail("Invalid cases must have at least one error");
861
903
  }
862
904
 
905
+ checkDuplicateTestCase(item, seenInvalidTestCases);
906
+
863
907
  const ruleHasMetaMessages = hasOwnProperty(rule, "meta") && hasOwnProperty(rule.meta, "messages");
864
908
  const friendlyIDList = ruleHasMetaMessages ? `[${Object.keys(rule.meta.messages).map(key => `'${key}'`).join(", ")}]` : null;
865
909
 
@@ -188,6 +188,7 @@ function getNonEmptyOperand(node) {
188
188
  /** @type {import('../shared/types').Rule} */
189
189
  module.exports = {
190
190
  meta: {
191
+ hasSuggestions: true,
191
192
  type: "suggestion",
192
193
 
193
194
  docs: {
@@ -229,7 +230,8 @@ module.exports = {
229
230
  }],
230
231
 
231
232
  messages: {
232
- useRecommendation: "use `{{recommendation}}` instead."
233
+ implicitCoercion: "Unexpected implicit coercion encountered. Use `{{recommendation}}` instead.",
234
+ useRecommendation: "Use `{{recommendation}}` instead."
233
235
  }
234
236
  },
235
237
 
@@ -241,32 +243,54 @@ module.exports = {
241
243
  * Reports an error and autofixes the node
242
244
  * @param {ASTNode} node An ast node to report the error on.
243
245
  * @param {string} recommendation The recommended code for the issue
246
+ * @param {bool} shouldSuggest Whether this report should offer a suggestion
244
247
  * @param {bool} shouldFix Whether this report should fix the node
245
248
  * @returns {void}
246
249
  */
247
- function report(node, recommendation, shouldFix) {
250
+ function report(node, recommendation, shouldSuggest, shouldFix) {
251
+
252
+ /**
253
+ * Fix function
254
+ * @param {RuleFixer} fixer The fixer to fix.
255
+ * @returns {Fix} The fix object.
256
+ */
257
+ function fix(fixer) {
258
+ const tokenBefore = sourceCode.getTokenBefore(node);
259
+
260
+ if (
261
+ tokenBefore?.range[1] === node.range[0] &&
262
+ !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
263
+ ) {
264
+ return fixer.replaceText(node, ` ${recommendation}`);
265
+ }
266
+
267
+ return fixer.replaceText(node, recommendation);
268
+ }
269
+
248
270
  context.report({
249
271
  node,
250
- messageId: "useRecommendation",
251
- data: {
252
- recommendation
253
- },
272
+ messageId: "implicitCoercion",
273
+ data: { recommendation },
254
274
  fix(fixer) {
255
275
  if (!shouldFix) {
256
276
  return null;
257
277
  }
258
278
 
259
- const tokenBefore = sourceCode.getTokenBefore(node);
260
-
261
- if (
262
- tokenBefore &&
263
- tokenBefore.range[1] === node.range[0] &&
264
- !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
265
- ) {
266
- return fixer.replaceText(node, ` ${recommendation}`);
279
+ return fix(fixer);
280
+ },
281
+ suggest: [
282
+ {
283
+ messageId: "useRecommendation",
284
+ data: { recommendation },
285
+ fix(fixer) {
286
+ if (shouldFix || !shouldSuggest) {
287
+ return null;
288
+ }
289
+
290
+ return fix(fixer);
291
+ }
267
292
  }
268
- return fixer.replaceText(node, recommendation);
269
- }
293
+ ]
270
294
  });
271
295
  }
272
296
 
@@ -278,8 +302,10 @@ module.exports = {
278
302
  operatorAllowed = options.allow.includes("!!");
279
303
  if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
280
304
  const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
305
+ const variable = astUtils.getVariableByName(sourceCode.getScope(node), "Boolean");
306
+ const booleanExists = variable?.identifiers.length === 0;
281
307
 
282
- report(node, recommendation, true);
308
+ report(node, recommendation, true, booleanExists);
283
309
  }
284
310
 
285
311
  // ~foo.indexOf(bar)
@@ -290,7 +316,7 @@ module.exports = {
290
316
  const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
291
317
  const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
292
318
 
293
- report(node, recommendation, false);
319
+ report(node, recommendation, false, false);
294
320
  }
295
321
 
296
322
  // +foo
@@ -298,7 +324,7 @@ module.exports = {
298
324
  if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
299
325
  const recommendation = `Number(${sourceCode.getText(node.argument)})`;
300
326
 
301
- report(node, recommendation, true);
327
+ report(node, recommendation, true, false);
302
328
  }
303
329
 
304
330
  // -(-foo)
@@ -306,7 +332,7 @@ module.exports = {
306
332
  if (!operatorAllowed && options.number && node.operator === "-" && node.argument.type === "UnaryExpression" && node.argument.operator === "-" && !isNumeric(node.argument.argument)) {
307
333
  const recommendation = `Number(${sourceCode.getText(node.argument.argument)})`;
308
334
 
309
- report(node, recommendation, false);
335
+ report(node, recommendation, true, false);
310
336
  }
311
337
  },
312
338
 
@@ -322,7 +348,7 @@ module.exports = {
322
348
  if (nonNumericOperand) {
323
349
  const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
324
350
 
325
- report(node, recommendation, true);
351
+ report(node, recommendation, true, false);
326
352
  }
327
353
 
328
354
  // foo - 0
@@ -330,7 +356,7 @@ module.exports = {
330
356
  if (!operatorAllowed && options.number && node.operator === "-" && node.right.type === "Literal" && node.right.value === 0 && !isNumeric(node.left)) {
331
357
  const recommendation = `Number(${sourceCode.getText(node.left)})`;
332
358
 
333
- report(node, recommendation, true);
359
+ report(node, recommendation, true, false);
334
360
  }
335
361
 
336
362
  // "" + foo
@@ -338,7 +364,7 @@ module.exports = {
338
364
  if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
339
365
  const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
340
366
 
341
- report(node, recommendation, true);
367
+ report(node, recommendation, true, false);
342
368
  }
343
369
  },
344
370
 
@@ -351,7 +377,7 @@ module.exports = {
351
377
  const code = sourceCode.getText(getNonEmptyOperand(node));
352
378
  const recommendation = `${code} = String(${code})`;
353
379
 
354
- report(node, recommendation, true);
380
+ report(node, recommendation, true, false);
355
381
  }
356
382
  },
357
383
 
@@ -389,7 +415,7 @@ module.exports = {
389
415
  const code = sourceCode.getText(node.expressions[0]);
390
416
  const recommendation = `String(${code})`;
391
417
 
392
- report(node, recommendation, true);
418
+ report(node, recommendation, true, false);
393
419
  }
394
420
  };
395
421
  }
@@ -161,17 +161,25 @@ module.exports = {
161
161
  (Object.hasOwn(options[0], "paths") || Object.hasOwn(options[0], "patterns"));
162
162
 
163
163
  const restrictedPaths = (isPathAndPatternsObject ? options[0].paths : context.options) || [];
164
- const restrictedPathMessages = restrictedPaths.reduce((memo, importSource) => {
164
+ const groupedRestrictedPaths = restrictedPaths.reduce((memo, importSource) => {
165
+ const path = typeof importSource === "string"
166
+ ? importSource
167
+ : importSource.name;
168
+
169
+ if (!memo[path]) {
170
+ memo[path] = [];
171
+ }
172
+
165
173
  if (typeof importSource === "string") {
166
- memo[importSource] = { message: null };
174
+ memo[path].push({});
167
175
  } else {
168
- memo[importSource.name] = {
176
+ memo[path].push({
169
177
  message: importSource.message,
170
178
  importNames: importSource.importNames
171
- };
179
+ });
172
180
  }
173
181
  return memo;
174
- }, {});
182
+ }, Object.create(null));
175
183
 
176
184
  // Handle patterns too, either as strings or groups
177
185
  let restrictedPatterns = (isPathAndPatternsObject ? options[0].patterns : []) || [];
@@ -203,57 +211,59 @@ module.exports = {
203
211
  * @private
204
212
  */
205
213
  function checkRestrictedPathAndReport(importSource, importNames, node) {
206
- if (!Object.hasOwn(restrictedPathMessages, importSource)) {
214
+ if (!Object.hasOwn(groupedRestrictedPaths, importSource)) {
207
215
  return;
208
216
  }
209
217
 
210
- const customMessage = restrictedPathMessages[importSource].message;
211
- const restrictedImportNames = restrictedPathMessages[importSource].importNames;
218
+ groupedRestrictedPaths[importSource].forEach(restrictedPathEntry => {
219
+ const customMessage = restrictedPathEntry.message;
220
+ const restrictedImportNames = restrictedPathEntry.importNames;
212
221
 
213
- if (restrictedImportNames) {
214
- if (importNames.has("*")) {
215
- const specifierData = importNames.get("*")[0];
222
+ if (restrictedImportNames) {
223
+ if (importNames.has("*")) {
224
+ const specifierData = importNames.get("*")[0];
225
+
226
+ context.report({
227
+ node,
228
+ messageId: customMessage ? "everythingWithCustomMessage" : "everything",
229
+ loc: specifierData.loc,
230
+ data: {
231
+ importSource,
232
+ importNames: restrictedImportNames,
233
+ customMessage
234
+ }
235
+ });
236
+ }
216
237
 
238
+ restrictedImportNames.forEach(importName => {
239
+ if (importNames.has(importName)) {
240
+ const specifiers = importNames.get(importName);
241
+
242
+ specifiers.forEach(specifier => {
243
+ context.report({
244
+ node,
245
+ messageId: customMessage ? "importNameWithCustomMessage" : "importName",
246
+ loc: specifier.loc,
247
+ data: {
248
+ importSource,
249
+ customMessage,
250
+ importName
251
+ }
252
+ });
253
+ });
254
+ }
255
+ });
256
+ } else {
217
257
  context.report({
218
258
  node,
219
- messageId: customMessage ? "everythingWithCustomMessage" : "everything",
220
- loc: specifierData.loc,
259
+ messageId: customMessage ? "pathWithCustomMessage" : "path",
221
260
  data: {
222
261
  importSource,
223
- importNames: restrictedImportNames,
224
262
  customMessage
225
263
  }
226
264
  });
227
265
  }
228
-
229
- restrictedImportNames.forEach(importName => {
230
- if (importNames.has(importName)) {
231
- const specifiers = importNames.get(importName);
232
-
233
- specifiers.forEach(specifier => {
234
- context.report({
235
- node,
236
- messageId: customMessage ? "importNameWithCustomMessage" : "importName",
237
- loc: specifier.loc,
238
- data: {
239
- importSource,
240
- customMessage,
241
- importName
242
- }
243
- });
244
- });
245
- }
246
- });
247
- } else {
248
- context.report({
249
- node,
250
- messageId: customMessage ? "pathWithCustomMessage" : "path",
251
- data: {
252
- importSource,
253
- customMessage
254
- }
255
- });
256
- }
266
+ });
257
267
  }
258
268
 
259
269
  /**
@@ -197,11 +197,26 @@ module.exports = {
197
197
  return;
198
198
  }
199
199
 
200
+ /**
201
+ * A collection of nodes to avoid duplicate reports.
202
+ * @type {Set<ASTNode>}
203
+ */
204
+ const reported = new Set();
205
+
200
206
  codePath.traverseSegments((segment, controller) => {
201
207
  const info = segInfoMap[segment.id];
208
+ const invalidNodes = info.invalidNodes
209
+ .filter(
210
+
211
+ /*
212
+ * Avoid duplicate reports.
213
+ * When there is a `finally`, invalidNodes may contain already reported node.
214
+ */
215
+ node => !reported.has(node)
216
+ );
202
217
 
203
- for (let i = 0; i < info.invalidNodes.length; ++i) {
204
- const invalidNode = info.invalidNodes[i];
218
+ for (const invalidNode of invalidNodes) {
219
+ reported.add(invalidNode);
205
220
 
206
221
  context.report({
207
222
  messageId: "noBeforeSuper",
@@ -273,14 +288,12 @@ module.exports = {
273
288
  const info = segInfoMap[segment.id];
274
289
 
275
290
  if (info.superCalled) {
276
- info.invalidNodes = [];
277
291
  controller.skip();
278
292
  } else if (
279
293
  segment.prevSegments.length > 0 &&
280
294
  segment.prevSegments.every(isCalled)
281
295
  ) {
282
296
  info.superCalled = true;
283
- info.invalidNodes = [];
284
297
  }
285
298
  }
286
299
  );
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Serialization utils.
3
+ * @author Bryan Mishkin
4
+ */
5
+
6
+ "use strict";
7
+
8
+ /**
9
+ * Check if a value is a primitive or plain object created by the Object constructor.
10
+ * @param {any} val the value to check
11
+ * @returns {boolean} true if so
12
+ * @private
13
+ */
14
+ function isSerializablePrimitiveOrPlainObject(val) {
15
+ return (
16
+ val === null ||
17
+ typeof val === "string" ||
18
+ typeof val === "boolean" ||
19
+ typeof val === "number" ||
20
+ (typeof val === "object" && val.constructor === Object) ||
21
+ Array.isArray(val)
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Check if a value is serializable.
27
+ * Functions or objects like RegExp cannot be serialized by JSON.stringify().
28
+ * Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
29
+ * @param {any} val the value
30
+ * @returns {boolean} true if the value is serializable
31
+ */
32
+ function isSerializable(val) {
33
+ if (!isSerializablePrimitiveOrPlainObject(val)) {
34
+ return false;
35
+ }
36
+ if (typeof val === "object") {
37
+ for (const property in val) {
38
+ if (Object.hasOwn(val, property)) {
39
+ if (!isSerializablePrimitiveOrPlainObject(val[property])) {
40
+ return false;
41
+ }
42
+ if (typeof val[property] === "object") {
43
+ if (!isSerializable(val[property])) {
44
+ return false;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return true;
51
+ }
52
+
53
+ module.exports = {
54
+ isSerializable
55
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "9.0.0-alpha.1",
3
+ "version": "9.0.0-alpha.2",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "bin": {
@@ -66,7 +66,7 @@
66
66
  "@eslint-community/eslint-utils": "^4.2.0",
67
67
  "@eslint-community/regexpp": "^4.6.1",
68
68
  "@eslint/eslintrc": "^3.0.0",
69
- "@eslint/js": "9.0.0-alpha.0",
69
+ "@eslint/js": "9.0.0-alpha.2",
70
70
  "@humanwhocodes/config-array": "^0.11.14",
71
71
  "@humanwhocodes/module-importer": "^1.0.1",
72
72
  "@nodelib/fs.walk": "^1.2.8",
@@ -77,7 +77,7 @@
77
77
  "escape-string-regexp": "^4.0.0",
78
78
  "eslint-scope": "^8.0.0",
79
79
  "eslint-visitor-keys": "^3.4.3",
80
- "espree": "^9.6.1",
80
+ "espree": "^10.0.0",
81
81
  "esquery": "^1.4.2",
82
82
  "esutils": "^2.0.2",
83
83
  "fast-deep-equal": "^3.1.3",
@@ -135,7 +135,7 @@
135
135
  "load-perf": "^0.2.0",
136
136
  "markdown-it": "^12.2.0",
137
137
  "markdown-it-container": "^3.0.0",
138
- "markdownlint": "^0.32.0",
138
+ "markdownlint": "^0.33.0",
139
139
  "markdownlint-cli": "^0.38.0",
140
140
  "marked": "^4.0.8",
141
141
  "memfs": "^3.0.1",