Eslint: FlatCompat utility and its work and magic, a deep dive that demystifies Flat config and translation from eslintrc

Mohamed Lamine Allal
35 min readNov 19, 2023

--

A very deep dive into FlatCompat utility. That does teach us a lot. From how it works. To a lot about how Flat config works. And how we can translate from eslintrc to Flat config

Resume and Navigation

  • Check Ok cool what are the lessons section all at the end ( CTRL|CMD + F ) [The whole resume go there]
    ◌ If you are in a hurry, you can skim through the whole, or directly pass to that resume section
  • Also, you can follow in GitHub with the code source
  • You will get to see how FlatCompat works
  • Normalization pattern with a great open-source example
  • How to transform Eslintrc config to FlatConfig
  • Eslint modules Name resolution algorithm
  • A demonstration of how you use the debugger in vscode to analyze and understand the code and it’s execution
  • Event loop in action and in relation to debugger stepping
  • generator pattern, explanation, and insight
  • Code consistency and its value in debugging, searching, and understanding the code base fast

FlatCompat tool repo

FlatCompat full usage example

FlatCompact how it works and it’s magic

✨ First, all calls to other than config() will call config()

(config())

/**
* Translates the `env` section of an ESLintRC-style config.
* @param {Object} envConfig The `env` section of an ESLintRC config.
* @returns {Object[]} An array of flag-config objects representing the environments.
*/
env(envConfig) {
return this.config({
env: envConfig
});
}

/**
* Translates the `extends` section of an ESLintRC-style config.
* @param {...string} configsToExtend The names of the configs to load.
* @returns {Object[]} An array of flag-config objects representing the config.
*/
extends(...configsToExtend) {
return this.config({
extends: configsToExtend
});
}

/**
* Translates the `plugins` section of an ESLintRC-style config.
* @param {...string} plugins The names of the plugins to load.
* @returns {Object[]} An array of flag-config objects representing the plugins.
*/
plugins(...plugins) {
return this.config({
plugins
});
}
  • So we will focus on config() function

✨ How does config() works

(config())

config(eslintrcConfig) {
const eslintrcArray = this[cafactory].create(eslintrcConfig, {
basePath: this.baseDirectory
});

const flatArray = [];
let hasIgnorePatterns = false;

eslintrcArray.forEach(configData => {
if (configData.type === "config") {
hasIgnorePatterns = hasIgnorePatterns || configData.ignorePattern;
flatArray.push(...translateESLintRC(configData, {
resolveConfigRelativeTo: path__default["default"].join(this.baseDirectory, "__placeholder.js"),
resolvePluginsRelativeTo: path__default["default"].join(this.resolvePluginsRelativeTo, "__placeholder.js"),
pluginEnvironments: eslintrcArray.pluginEnvironments,
pluginProcessors: eslintrcArray.pluginProcessors
}));
}
});

// combine ignorePatterns to emulate ESLintRC behavior better
if (hasIgnorePatterns) {
flatArray.unshift({
ignores: [filePath => {
// Compute the final config for this file.
// This filters config array elements by `files`/`excludedFiles` then merges the elements.
const finalConfig = eslintrcArray.extractConfig(filePath);
// Test the `ignorePattern` properties of the final config.
return Boolean(finalConfig.ignores) && finalConfig.ignores(filePath);
}]
});
}

return flatArray;
}
  • get our passed old eslint config
    ◌ And it would make magic to translate it to flat config

✨ Before starting, things to establish

▪️ config

  • take one plugin config at a time
  • And translate something equiv for flat config
    ◌ that would rely on the nature of overriding or deep merging of flat config

The way we use compat.config()

is

[ 
...compat.extends('plugin:prettier/recommended'),
]
  • it does return a flat config (array)

✨ Normalizing eslintrc config

(normalizing, eslintrc)

  • Factory
const eslintrcArray = this[cafactory].create(eslintrcConfig, {
basePath: this.baseDirectory
});
// Create `ConfigArray` instance from a config data.
create(configData, { basePath, filePath, name } = {}) {
if (!configData) {
return new ConfigArray();
}

const slots = internalSlotsMap$1.get(this);
const ctx = createContext(slots, "config", name, filePath, basePath);
const elements = this._normalizeConfigData(configData, ctx);

return new ConfigArray(...elements);
}

▪️ A normalization goes first before transformation

  • normalization when it comes to processing and parsing config
    ◌ is a common practice
    ◌ You create a one-config format
    -
    prepared specifically for your processing requirement
    - And it’s concise and specific
    - Not normalized (many possibilities, different formats …) => normalized (on format)
    ▶︎ Can code with it simply. It simplifies. Simple direct reading. And ease processing.

Ok, we can just stop at this.

Or let’s get curious

▪️ opening the js debugging console

  • A very powerful beauty gem of vscode
    ◌ available for years now

Let’s add breakpoints

vscode js debugger terminal break points

Let's execute eslint . on the debugging console

  • debugger attached and pause at our breakpoint
  • using the debugger like this, is the fastest way to understand the execution and the code
  • as well as the structure of variables
    ◌ especially if a lot doesn’t matter to you

Either step in or step over and you keep going step by step

Or in our case, we want to go directly to create

  • So I'll hit continue

My breakpoint wasn’t hit

Try again

I used step inin this time

  • step in again
  • I can add breakpoints now
    ◌ You can always add breakpoints while things are running

But it's fine with step in

Step over to advance

step over

  • you can see, I can see the structure of the slots

And we can now, see things in action and how they evolve

at this step, we can go step in and discover the createContext() or i can skip

  • stepped in we could have skipped

Step over multiple times

I can check the variables as well as I'm seeing the code

  • the context format for now is
return { filePath, matchBasePath, name, pluginBasePath, type };

values

We could have skipped

And we get the context after

I stepped in again

  • normalization
    ◌ we can see the validation part of normalization
    ◌ And it’s a critical and common step in normalization
  • we can see the config passed to it
  • we can see the validate schema
  • And basically, it’s eslintrc config schema
  • validation pass with no issue
  • checking for ecmaFeatures
  • in our case there is none
    ◌ Which is a deprecated feature and only for warning
  • Validation of schema done

We can move on

getting me back ❗

because i over step over the function (mistake) ❗

  • Elements here is a generator

✨ Use of ignores at the start of the flat config

  • For time purposes, I will not keep stepping in
  • criteria null
  • override tester seems to test for override criteria
    ◌ We can imagine what the component does

We see overrides props are handled through the OverrideTester

And that this tool does help us with such config

At this step because elements is a generator, let’s go with step in

And see how the generator is working

And what’s doing

This function handles the different config properties

In our case we have extends only

and it would be handled in that block

  • _loadExtends()
  • Search in lib folder

▪️ We see all similar functions

  • for load functions
  • for the different properties
  • In a project with good convention and good consistency
    Searching that way is too efficient and would show you all similar things
    ◌ that’s the power of good editors
// Flatten `extends`.
for (const extendName of extendList.filter(Boolean)) {
yield* this._loadExtends(extendName, ctx);
}

▪️ let’s explain the yield (Skip or skim, if you are already familiar with generators)

  • we are inside a generator
    function, with syntax-ic sugar for iterators
    ◌ just like async await
    Generators (if not familiar), are popular across all languages
    ◌ And yield is the equiv of await or return
    - yield return the value and pause the execution. Until generator.next() is called again. Where the execution will resume from that last yield pause place in the code and that’s why it’s a syntaxic sugar like await. The generator syntax allows the creation of an iterator easily (just like await does with promises).
    - A Generator is a function. In which yieldis used. That return an iterator. The iterator with next() API is used to iterate through the execution context (defined by the yield points). Each next() call will start the execution from the last yield point to the next one and pause. Till the next next() call will be made. If return is encountered. The iterator comes to an end.
    - In the loop above yield will return the value of loadExtends() and pause the execution. Until generator.next() is called. Where the execution will continue from that same yield point.
    - gen.next() => exec till encounter yield => pause, return val
    — once we call gen.next() again => resume execution from last yield (pause point) => run until the encounter of another yield => pause, return val =>
  • for of support iterators
  • for of is working by each time calling
    elements.next() 🔥
    when that happens => our function will execute 🔥
    until it reaches a yield 🔥
  • ◌ at that point as with await or return
    ◌ The value of next() is returned
    ◌ in this case it will be stored in element variable
    ◌ our loop block will execute
    ◌ Then again of will go 🔥
    ◌ and .next() called again 🔥
    however this time
    ▶︎ the execution of the function will start just after the line containing yield 🔥
    ▶︎ generator and yield allow a breakpoint for stopping
    ▶ ︎and later it would resume from there
    ▶︎ check out a course about generators or [my article about it][Will be added later, TK]
    - check how to translate generators to iterators
    - ✨ The best way also, to play with that is to go to babel playground 🔥
    - and use a generator, for example, the function above
    - And check the output in iterator format
    - basically, what the syntax-ic sugar translate to

You can see the iterator

and the usage of switch

and next context to hold which block to run next ….

  • Function in generator format
function* _normalizeObjectConfigDataBody(
{
env,
extends: extend,
globals,
ignorePatterns,
noInlineConfig,
parser: parserName,
parserOptions,
plugins: pluginList,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings,
overrides: overrideList = []
},
ctx
) {
const extendList = Array.isArray(extend) ? extend : [extend];
const ignorePattern = ignorePatterns && new IgnorePattern(
Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns],
ctx.matchBasePath
);

// Flatten `extends`.
for (const extendName of extendList.filter(Boolean)) {
yield* this._loadExtends(extendName, ctx);
}

// Load parser & plugins.
const parser = parserName && this._loadParser(parserName, ctx);
const plugins = pluginList && this._loadPlugins(pluginList, ctx);

// Yield pseudo config data for file extension processors.
if (plugins) {
yield* this._takeFileExtensionProcessors(plugins, ctx);
}

// Yield the config data except `extends` and `overrides`.
yield {

// Debug information.
type: ctx.type,
name: ctx.name,
filePath: ctx.filePath,

// Config data.
criteria: null,
env,
globals,
ignorePattern,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};

// Flatten `overries`.
for (let i = 0; i < overrideList.length; ++i) {
yield* this._normalizeObjectConfigData(
overrideList[i],
{ ...ctx, name: `${ctx.name}#overrides[${i}]` }
);
}
}
  • Function in iterator format by Babel Playground transpilation compiled for nodejs ES5
jafunction _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return e; }; var t, e = {}, r = Object.prototype, n = r.hasOwnProperty, o = Object.defineProperty || function (t, e, r) { t[e] = r.value; }, i = "function" == typeof Symbol ? Symbol : {}, a = i.iterator || "@@iterator", c = i.asyncIterator || "@@asyncIterator", u = i.toStringTag || "@@toStringTag"; function define(t, e, r) { return Object.defineProperty(t, e, { value: r, enumerable: !0, configurable: !0, writable: !0 }), t[e]; } try { define({}, ""); } catch (t) { define = function define(t, e, r) { return t[e] = r; }; } function wrap(t, e, r, n) { var i = e && e.prototype instanceof Generator ? e : Generator, a = Object.create(i.prototype), c = new Context(n || []); return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a; } function tryCatch(t, e, r) { try { return { type: "normal", arg: t.call(e, r) }; } catch (t) { return { type: "throw", arg: t }; } } e.wrap = wrap; var h = "suspendedStart", l = "suspendedYield", f = "executing", s = "completed", y = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var p = {}; define(p, a, function () { return this; }); var d = Object.getPrototypeOf, v = d && d(d(values([]))); v && v !== r && n.call(v, a) && (p = v); var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p); function defineIteratorMethods(t) { ["next", "throw", "return"].forEach(function (e) { define(t, e, function (t) { return this._invoke(e, t); }); }); } function AsyncIterator(t, e) { function invoke(r, o, i, a) { var c = tryCatch(t[r], t, o); if ("throw" !== c.type) { var u = c.arg, h = u.value; return h && "object" == typeof h && n.call(h, "__await") ? e.resolve(h.__await).then(function (t) { invoke("next", t, i, a); }, function (t) { invoke("throw", t, i, a); }) : e.resolve(h).then(function (t) { u.value = t, i(u); }, function (t) { return invoke("throw", t, i, a); }); } a(c.arg); } var r; o(this, "_invoke", { value: function value(t, n) { function callInvokeWithMethodAndArg() { return new e(function (e, r) { invoke(t, n, e, r); }); } return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(e, r, n) { var o = h; return function (i, a) { if (o === f) throw new Error("Generator is already running"); if (o === s) { if ("throw" === i) throw a; return { value: t, done: !0 }; } for (n.method = i, n.arg = a;;) { var c = n.delegate; if (c) { var u = maybeInvokeDelegate(c, n); if (u) { if (u === y) continue; return u; } } if ("next" === n.method) n.sent = n._sent = n.arg;else if ("throw" === n.method) { if (o === h) throw o = s, n.arg; n.dispatchException(n.arg); } else "return" === n.method && n.abrupt("return", n.arg); o = f; var p = tryCatch(e, r, n); if ("normal" === p.type) { if (o = n.done ? s : l, p.arg === y) continue; return { value: p.arg, done: n.done }; } "throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg); } }; } function maybeInvokeDelegate(e, r) { var n = r.method, o = e.iterator[n]; if (o === t) return r.delegate = null, "throw" === n && e.iterator.return && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y; var i = tryCatch(o, e.iterator, r.arg); if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y; var a = i.arg; return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y); } function pushTryEntry(t) { var e = { tryLoc: t[0] }; 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e); } function resetTryEntry(t) { var e = t.completion || {}; e.type = "normal", delete e.arg, t.completion = e; } function Context(t) { this.tryEntries = [{ tryLoc: "root" }], t.forEach(pushTryEntry, this), this.reset(!0); } function values(e) { if (e || "" === e) { var r = e[a]; if (r) return r.call(e); if ("function" == typeof e.next) return e; if (!isNaN(e.length)) { var o = -1, i = function next() { for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next; return next.value = t, next.done = !0, next; }; return i.next = i; } } throw new TypeError(typeof e + " is not iterable"); } return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", { value: GeneratorFunctionPrototype, configurable: !0 }), o(GeneratorFunctionPrototype, "constructor", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function (t) { var e = "function" == typeof t && t.constructor; return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name)); }, e.mark = function (t) { return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t; }, e.awrap = function (t) { return { __await: t }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () { return this; }), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) { void 0 === i && (i = Promise); var a = new AsyncIterator(wrap(t, r, n, o), i); return e.isGeneratorFunction(r) ? a : a.next().then(function (t) { return t.done ? t.value : a.next(); }); }, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function () { return this; }), define(g, "toString", function () { return "[object Generator]"; }), e.keys = function (t) { var e = Object(t), r = []; for (var n in e) r.push(n); return r.reverse(), function next() { for (; r.length;) { var t = r.pop(); if (t in e) return next.value = t, next.done = !1, next; } return next.done = !0, next; }; }, e.values = values, Context.prototype = { constructor: Context, reset: function reset(e) { if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t); }, stop: function stop() { this.done = !0; var t = this.tryEntries[0].completion; if ("throw" === t.type) throw t.arg; return this.rval; }, dispatchException: function dispatchException(e) { if (this.done) throw e; var r = this; function handle(n, o) { return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o; } for (var o = this.tryEntries.length - 1; o >= 0; --o) { var i = this.tryEntries[o], a = i.completion; if ("root" === i.tryLoc) return handle("end"); if (i.tryLoc <= this.prev) { var c = n.call(i, "catchLoc"), u = n.call(i, "finallyLoc"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw new Error("try statement without catch or finally"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) { var i = o; break; } } i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if ("throw" === t.type) throw t.arg; return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, catch: function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if ("throw" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, "next" === this.method && (this.arg = t), y; } }, e; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
function _createForOfIteratorHelperLoose(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (it) return (it = it.call(o)).next.bind(it); if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; return function () { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
function _normalizeObjectConfigDataBody(_ref, ctx) {
var _this = this;
var env = _ref.env,
extend = _ref.extends,
globals = _ref.globals,
ignorePatterns = _ref.ignorePatterns,
noInlineConfig = _ref.noInlineConfig,
parserName = _ref.parser,
parserOptions = _ref.parserOptions,
pluginList = _ref.plugins,
processor = _ref.processor,
reportUnusedDisableDirectives = _ref.reportUnusedDisableDirectives,
root = _ref.root,
rules = _ref.rules,
settings = _ref.settings,
_ref$overrides = _ref.overrides,
overrideList = _ref$overrides === void 0 ? [] : _ref$overrides;
return /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
var extendList, ignorePattern, _iterator, _step, extendName, parser, plugins, i;
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
extendList = Array.isArray(extend) ? extend : [extend];
ignorePattern = ignorePatterns && new IgnorePattern(Array.isArray(ignorePatterns) ? ignorePatterns : [ignorePatterns], ctx.matchBasePath); // Flatten `extends`.
_iterator = _createForOfIteratorHelperLoose(extendList.filter(Boolean));
case 3:
if ((_step = _iterator()).done) {
_context.next = 8;
break;
}
extendName = _step.value;
return _context.delegateYield(_this._loadExtends(extendName, ctx), "t0", 6);
case 6:
_context.next = 3;
break;
case 8:
// Load parser & plugins.
parser = parserName && _this._loadParser(parserName, ctx);
plugins = pluginList && _this._loadPlugins(pluginList, ctx); // Yield pseudo config data for file extension processors.
if (!plugins) {
_context.next = 12;
break;
}
return _context.delegateYield(_this._takeFileExtensionProcessors(plugins, ctx), "t1", 12);
case 12:
_context.next = 14;
return {
// Debug information.
type: ctx.type,
name: ctx.name,
filePath: ctx.filePath,
// Config data.
criteria: null,
env,
globals,
ignorePattern,
noInlineConfig,
parser,
parserOptions,
plugins,
processor,
reportUnusedDisableDirectives,
root,
rules,
settings
};
case 14:
i = 0;
case 15:
if (!(i < overrideList.length)) {
_context.next = 20;
break;
}
return _context.delegateYield(_this._normalizeObjectConfigData(overrideList[i], _objectSpread(_objectSpread({}, ctx), {}, {
name: `${ctx.name}#overrides[${i}]`
})), "t2", 17);
case 17:
++i;
_context.next = 15;
break;
case 20:
case "end":
return _context.stop();
}
}, _callee);
})();
}

Ok [TODO] REMOVE THAT PART and set it for its own article

  • leave links only
  • let’s step in let’s step in
_loadExtends(extendName, ctx) {
debug("Loading {extends:%j} relative to %s", extendName, ctx.filePath);
try {
if (extendName.startsWith("eslint:")) {
return this._loadExtendedBuiltInConfig(extendName, ctx);
}
if (extendName.startsWith("plugin:")) {
return this._loadExtendedPluginConfig(extendName, ctx);
}
return this._loadExtendedShareableConfig(extendName, ctx);
} catch (error) {
error.message += `\nReferenced from: ${ctx.filePath || ctx.name}`;
throw error;
}
}
  • from just that block we get to know how extends works and eslint convention when it comes to it

▪️ eslint:

  • by eslint extends

▪ ️plugin:

  • by plugin extends (eslint-plugin-prettier )
    eslint-plugin-

▪️ none

  • A sharable config eslint-config-prettier
    ◌ not plugin-related
    eslint-config-

our start with plugin

let’s step in

_loadExtendedPluginConfig(extendName, ctx) {
const slashIndex = extendName.lastIndexOf("/");

if (slashIndex === -1) {
throw configInvalidError(extendName, ctx.filePath, "plugin-invalid");
}

const pluginName = extendName.slice("plugin:".length, slashIndex);
const configName = extendName.slice(slashIndex + 1);

if (isFilePath(pluginName)) {
throw new Error("'extends' cannot use a file path for plugins.");
}

const plugin = this._loadPlugin(pluginName, ctx);
const configData =
plugin.definition &&
plugin.definition.configs[configName];

if (configData) {
return this._normalizeConfigData(configData, {
...ctx,
filePath: plugin.filePath || ctx.filePath,
name: `${ctx.name} » plugin:${plugin.id}/${configName}`
});
}

throw plugin.error || configInvalidError(extendName, ctx.filePath, "extend-config-missing");
}
  • you get why / and what is it
    ◌ it’s a separation to precise which config to pickup
    ◌ if multiple are provided

loadPlugin() function

_loadPlugin(name, ctx) {
debug("Loading plugin %j from %s", name, ctx.filePath);

const { additionalPluginPool, resolver } = internalSlotsMap.get(this);
const request = naming.normalizePackageName(name, "eslint-plugin");
const id = naming.getShorthandName(request, "eslint-plugin");
const relativeTo = path.join(ctx.pluginBasePath, "__placeholder__.js");

if (name.match(/\s+/u)) {
const error = Object.assign(
new Error(`Whitespace found in plugin name '${name}'`),
{
messageTemplate: "whitespace-found",
messageData: { pluginName: request }
}
);

return new ConfigDependency({
error,
id,
importerName: ctx.name,
importerPath: ctx.filePath
});
}

// Check for additional pool.
const plugin =
additionalPluginPool.get(request) ||
additionalPluginPool.get(id);

if (plugin) {
return new ConfigDependency({
definition: normalizePlugin(plugin),
filePath: "", // It's unknown where the plugin came from.
id,
importerName: ctx.name,
importerPath: ctx.filePath
});
}

let filePath;
let error;

try {
filePath = resolver.resolve(request, relativeTo);
} catch (resolveError) {
error = resolveError;
/* istanbul ignore else */
if (error && error.code === "MODULE_NOT_FOUND") {
error.messageTemplate = "plugin-missing";
error.messageData = {
pluginName: request,
resolvePluginsRelativeTo: ctx.pluginBasePath,
importerName: ctx.name
};
}
}

if (filePath) {
try {
writeDebugLogForLoading(request, relativeTo, filePath);

const startTime = Date.now();
const pluginDefinition = require(filePath);

debug(`Plugin ${filePath} loaded in: ${Date.now() - startTime}ms`);

return new ConfigDependency({
definition: normalizePlugin(pluginDefinition),
filePath,
id,
importerName: ctx.name,
importerPath: ctx.filePath
});
} catch (loadError) {
error = loadError;
}
}

debug("Failed to load plugin '%s' declared in '%s'.", name, ctx.name);
error.message = `Failed to load plugin '${name}' declared in '${ctx.name}': ${error.message}`;
return new ConfigDependency({
error,
id,
importerName: ctx.name,
importerPath: ctx.filePath
});
}

it's a package resolution function

  • does some validations
  • and handle dependency

eslint-plugin-prettier have none

  • resolver.resolve() resolve the plugin file path
  • require
  • loaded correctly
  • now the FlatCompat tool will be able to work with it
  • The tool is doing the loading for us
  • As all was happening in the past

Another normalizing function

normalizePlugin(pluginDefinition)
  • to normalize plugin definition

Ok we can't help but be curious

function normalizePlugin(plugin) {
// first check the cache
let normalizedPlugin = normalizedPlugins.get(plugin);

if (normalizedPlugin) {
return normalizedPlugin;
}

normalizedPlugin = {
configs: plugin.configs || {},
environments: plugin.environments || {},
processors: plugin.processors || {},
rules: plugin.rules || {}
};

// save the reference for later
normalizedPlugins.set(plugin, normalizedPlugin);

return normalizedPlugin;
}
  • that’s the normalization logic
  • plugin loaded, ready to be used
  • All this, to get the plugin config data
  • Then we have to normalize it
  • When you normalize data, you can match and count on it
    Data normalization ease up working with data . By creating structure. And one format.
  • You know the function
  • And we are using recursivity
  • And you can know why?
    ◌ In case of modules that would have extends and other props as well
    - M1 (extend (M2 extend (M3 …)))
    ◌ So all needs to happen recursively

I'm passing

  • recursivity kicking in

this time extends: ["prettier"]

  • => will be resolved to eslint-config-prettier which is a fully different package
  • we are going again
    ◌ same trip

again, an extends

a sharable config

  • resolution to package name done
  • let's get the package path

Here it goes

'/Users/mohamedlamineallal/repos/dotfiles-wizard/node_modules/.pnpm/eslint-config-prettier@9.0.0_eslint@8.48.0/node_modules/eslint-config-prettier/index.js'

Same thing but this time the config will have rules only

  • plugin will be required
  • and the whole normalization will be applied as well
  • we move on

Again the same normalization function (generator)

recursivity

  • This time the object has rules only
  • So no more iterations after that

this time we will stop

there is none

So we are moving to the next logic

You can see how generators are beautiful

recursion is better

managing flow is better

Condition with flow works well

…..

  • no parser skip
  • neither plugin

Hello we are going to yield an actual data without recursion, finally

after yield , the generator continue

  • I did a step over
  • should have done a step in

done with the function return

Generator for of will stop as well

You can see that the data is being normalized

And set in their correct section

  • now after normalization finish
  • Transformation will go

▪️ plugins here are the actual loaded plugins

  • nodejs require
    ◌ ++

▪️ plugins, rules are correctly loaded

  • plugins loaded with actual objets not string
    ◌ exactly what flat config require
  • same thing as before

Done normalizing

  • recursivity upward
    ◌ I skipped a step

Hola back

We could have skipped all that journey

  • I did it only to showcase debugging usage
  • And for some curiosity
    ◌ checking the code

▪ ️Two configs are loaded fully

  • eslint-plugin-prettier plugin
    ◌ with plugin loaded correctly
  • eslint-config-prettier rules config
  • The third is empty

▪️ resolution and normalization done at this stage 🔥

  • We can start with the transformation

✨ Transformation 🔥🔥

  • missed to use step in (step over instead) it was a forEach()

We can see the transformation applied

For each eslintrc element

a block config in flat config is generated

  • we can see the one of eslint-config-prettier
    ◌ only rules
  • and the one of eslint-plugin-prettier
    rules
    plugins

We can understand well how things are working now

I'll go again and get to step on translateEslintRc()

Or wait no need. Here is the whole function the logic is clear

  • We can see the execution. Or directly read the code. Let’s break it down.
/**
* Translates an ESLintRC-style config object into a flag-config-style config
* object.
* @param {Object} eslintrcConfig An ESLintRC-style config object.
* @param {Object} options Options to help translate the config.
* @param {string} options.resolveConfigRelativeTo To the directory to resolve
* configs from.
* @param {string} options.resolvePluginsRelativeTo The directory to resolve
* plugins from.
* @param {ReadOnlyMap<string,Environment>} options.pluginEnvironments A map of plugin environment
* names to objects.
* @param {ReadOnlyMap<string,Processor>} options.pluginProcessors A map of plugin processor
* names to objects.
* @returns {Object} A flag-config-style config object.
*/
function translateESLintRC(eslintrcConfig, {
resolveConfigRelativeTo,
resolvePluginsRelativeTo,
pluginEnvironments,
pluginProcessors
}) {
const flatConfig = {};
const configs = [];
const languageOptions = {};
const linterOptions = {};
const keysToCopy = ["settings", "rules", "processor"];
const languageOptionsKeysToCopy = ["globals", "parser", "parserOptions"];
const linterOptionsKeysToCopy = ["noInlineConfig", "reportUnusedDisableDirectives"];

// copy over simple translations
for (const key of keysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
flatConfig[key] = eslintrcConfig[key];
}
}

// copy over languageOptions
for (const key of languageOptionsKeysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
// create the languageOptions key in the flat config
flatConfig.languageOptions = languageOptions;

if (key === "parser") {
debug(`Resolving parser '${languageOptions[key]}' relative to ${resolveConfigRelativeTo}`);

if (eslintrcConfig[key].error) {
throw eslintrcConfig[key].error;
}

languageOptions[key] = eslintrcConfig[key].definition;
continue;
}
// clone any object values that are in the eslintrc config
if (eslintrcConfig[key] && typeof eslintrcConfig[key] === "object") {
languageOptions[key] = {
...eslintrcConfig[key]
};
} else {
languageOptions[key] = eslintrcConfig[key];
}
}
}
// copy over linterOptions
for (const key of linterOptionsKeysToCopy) {
if (key in eslintrcConfig && typeof eslintrcConfig[key] !== "undefined") {
flatConfig.linterOptions = linterOptions;
linterOptions[key] = eslintrcConfig[key];
}
}
// move ecmaVersion a level up
if (languageOptions.parserOptions) {
if ("ecmaVersion" in languageOptions.parserOptions) {
languageOptions.ecmaVersion = languageOptions.parserOptions.ecmaVersion;
delete languageOptions.parserOptions.ecmaVersion;
}
if ("sourceType" in languageOptions.parserOptions) {
languageOptions.sourceType = languageOptions.parserOptions.sourceType;
delete languageOptions.parserOptions.sourceType;
}
// check to see if we even need parserOptions anymore and remove it if not
if (Object.keys(languageOptions.parserOptions).length === 0) {
delete languageOptions.parserOptions;
}
}
// overrides
if (eslintrcConfig.criteria) {
flatConfig.files = [absoluteFilePath => eslintrcConfig.criteria.test(absoluteFilePath)];
}
// translate plugins
if (eslintrcConfig.plugins && typeof eslintrcConfig.plugins === "object") {
debug(`Translating plugins: ${eslintrcConfig.plugins}`);
flatConfig.plugins = {};
for (const pluginName of Object.keys(eslintrcConfig.plugins)) {
debug(`Translating plugin: ${pluginName}`);
debug(`Resolving plugin '${pluginName} relative to ${resolvePluginsRelativeTo}`);
const { definition: plugin, error } = eslintrcConfig.plugins[pluginName];
if (error) {
throw error;
}
flatConfig.plugins[pluginName] = plugin;
// create a config for any processors
if (plugin.processors) {
for (const processorName of Object.keys(plugin.processors)) {
if (processorName.startsWith(".")) {
debug(`Assigning processor: ${pluginName}/${processorName}`);
configs.unshift({
files: [`**/*${processorName}`],
processor: pluginProcessors.get(`${pluginName}/${processorName}`)
});
}
}
}
}
}
// translate env - must come after plugins
if (eslintrcConfig.env && typeof eslintrcConfig.env === "object") {
for (const envName of Object.keys(eslintrcConfig.env)) {
// only add environments that are true
if (eslintrcConfig.env[envName]) {
debug(`Translating environment: ${envName}`);
if (environments.has(envName)) {
// built-in environments should be defined first
configs.unshift(...translateESLintRC({
criteria: eslintrcConfig.criteria,
...environments.get(envName)
}, {
resolveConfigRelativeTo,
resolvePluginsRelativeTo
}));
} else if (pluginEnvironments.has(envName)) {
// if the environment comes from a plugin, it should come after the plugin config
configs.push(...translateESLintRC({
criteria: eslintrcConfig.criteria,
...pluginEnvironments.get(envName)
}, {
resolveConfigRelativeTo,
resolvePluginsRelativeTo
}));
}
}
}
}
// only add if there are actually keys in the config
if (Object.keys(flatConfig).length > 0) {
configs.push(flatConfig);
}
return configs;
}

▪️ for every resolved config from the normalization step

  • array of configs eslintrc
  • for each one
    ◌ we do translate it
    ◌ according to the rules in the function above

Then we add that to the FlatList

which later we gonna spread

  • you can see some props are just copied
  • language options keys need to be moved to languageOptions
  • linterOptions same thing

handling of ecmaVersion and sourceType

  • move one level up
  • and check if parserOptions is needed anymore

If it was an override config

Set files with a function resolver

  • Apparently, files does support both strings and function resolvers
  • (didn’t see that in the doc, maybe I didn’t pay attention well, or I just didn't cross it)

Plugins are set as follows

  • The same name as the plugin name is used for naming the plugin
  • eslint-plugin-prettier => prettier (remove eslint-plugin-)

already resolved in the normalization

Plugin source

import all from './configs/all';
import base from './configs/base';
import disableTypeChecked from './configs/disable-type-checked';
import eslintRecommended from './configs/eslint-recommended';
import recommended from './configs/recommended';
import recommendedTypeChecked from './configs/recommended-type-checked';
import strict from './configs/strict';
import strictTypeChecked from './configs/strict-type-checked';
import stylistic from './configs/stylistic';
import stylisticTypeChecked from './configs/stylistic-type-checked';
import rules from './rules';

export = {
configs: {
all,
base,
'disable-type-checked': disableTypeChecked,
'eslint-recommended': eslintRecommended,
recommended,
/** @deprecated - please use "recommended-type-checked" instead. */
'recommended-requiring-type-checking': recommendedTypeChecked,
'recommended-type-checked': recommendedTypeChecked,
strict,
'strict-type-checked': strictTypeChecked,
stylistic,
'stylistic-type-checked': stylisticTypeChecked,
},
rules,
};
  • /eslint-recommended
/**
* This is a compatibility ruleset that:
* - disables rules from eslint:recommended which are already handled by TypeScript.
* - enables rules that make sense due to TS's typechecking / transpilation.
*/
export = {
overrides: [
{
files: ['*.ts', '*.tsx', '*.mts', '*.cts'],
rules: {
'constructor-super': 'off', // ts(2335) & ts(2377)
'getter-return': 'off', // ts(2378)
'no-const-assign': 'off', // ts(2588)
'no-dupe-args': 'off', // ts(2300)
'no-dupe-class-members': 'off', // ts(2393) & ts(2300)
'no-dupe-keys': 'off', // ts(1117)
'no-func-assign': 'off', // ts(2539)
'no-import-assign': 'off', // ts(2539) & ts(2540)
'no-new-symbol': 'off', // ts(7009)
'no-obj-calls': 'off', // ts(2349)
'no-redeclare': 'off', // ts(2451)
'no-setter-return': 'off', // ts(2408)
'no-this-before-super': 'off', // ts(2376)
'no-undef': 'off', // ts(2304)
'no-unreachable': 'off', // ts(7027)
'no-unsafe-negation': 'off', // ts(2365) & ts(2360) & ts(2358)
'no-var': 'error', // ts transpiles let/const to var, so no need for vars any more
'prefer-const': 'error', // ts provides better types with const
'prefer-rest-params': 'error', // ts provides better types with rest args over arguments
'prefer-spread': 'error', // ts transpiles spread to apply, so no need for manual apply
},
},
],
};
  • For name resolution, it would be
    @typescript-eslint/eslint-plugin => typescriptEslint

Here is the normalization logic for plugins

  • node_modules/@eslint/eslintrc/lib/shared/naming.js
function normalizePackageName(name, prefix) {
let normalizedName = name;

/**
* On Windows, name can come in with Windows slashes instead of Unix slashes.
* Normalize to Unix first to avoid errors later on.
* https://github.com/eslint/eslint/issues/5644
*/
if (normalizedName.includes("\\")) {
normalizedName = normalizedName.replace(/\\/gu, "/");
}

if (normalizedName.charAt(0) === "@") {
/**
* it's a scoped package
* package name is the prefix, or just a username
*/
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`, "u"),
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`, "u");

if (scopedPackageShortcutRegex.test(normalizedName)) {
normalizedName = normalizedName.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
} else if (!scopedPackageNameRegex.test(normalizedName.split("/")[1])) {
/**
* for scoped packages, insert the prefix after the first / unless
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
*/
normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`);
}
} else if (!normalizedName.startsWith(`${prefix}-`)) {
normalizedName = `${prefix}-${normalizedName}`;
}
return normalizedName;
}
if (normalizedName.charAt(0) === "@") {
/**
* it's a scoped package
* package name is the prefix, or just a username
*/
const scopedPackageShortcutRegex = new RegExp(`^(@[^/]+)(?:/(?:${prefix})?)?$`, "u"),
scopedPackageNameRegex = new RegExp(`^${prefix}(-|$)`, "u");
if (scopedPackageShortcutRegex.test(normalizedName)) {
normalizedName = normalizedName.replace(scopedPackageShortcutRegex, `$1/${prefix}`);
} else if (!scopedPackageNameRegex.test(normalizedName.split("/")[1])) {
/**
* for scoped packages, insert the prefix after the first / unless
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
*/
normalizedName = normalizedName.replace(/^@([^/]+)\/(.*)$/u, `@$1/${prefix}-$2`);
}
} else if (!normalizedName.startsWith(`${prefix}-`)) {
normalizedName = `${prefix}-${normalizedName}`;
}
  • if scoped package
    ◌ is it a domain ?
    ◌ or just a username ?
/**
* for scoped packages, insert the prefix after the first / unless
* the path is already @scope/eslint or @scope/eslint-xxx-yyy
*/
  • @{domain}/eslint-plugin
    ◌ or
    @{username}/eslint-plugin-{some_name}

That will normalize the package name

  • For shorthand which is used in the flat Config
function getShorthandName(fullname, prefix) {
if (fullname[0] === "@") {
let matchResult = new RegExp(`^(@[^/]+)/${prefix}$`, "u").exec(fullname);

if (matchResult) {
return matchResult[1];
}

matchResult = new RegExp(`^(@[^/]+)/${prefix}-(.+)$`, "u").exec(fullname);
if (matchResult) {
return `${matchResult[1]}/${matchResult[2]}`;
}
} else if (fullname.startsWith(`${prefix}-`)) {
return fullname.slice(prefix.length + 1);
}

return fullname;
}
  • if @{domain}/eslint-plugin
    @{domain}
  • Otherwise if @{username}/eslint-plugin-{some_name}
    @{username}/{some_name}
  • plugin name @typescript-eslint with @

What does it resolve to in normalization ???

  • lets simply see
  • setting break point after the installation of the module
  • and lets roll
  • error needed to pass string for compat.plugins()

The name is @typescript-eslint

let’s see in the flat config

  • it’s just the same
    @typescript-eslint

✨ Ok cool what are the lessons 🔥🔥🔥

✨ Naming convention when it comes to translating old plugins 🔥

  • eslint-plugin-{name}
    ◌ name {name}
  • @{scope}/eslint-plugin
    ◌ name @{scope}
  • Otherwise if @{scope}/eslint-plugin-{some_name}
    ◌ name @{scope}/{some_name}

Examples:

  • eslint-plugin-prettier
    ◌ name prettier
  • @typescript-eslint/eslint-plugin
    ◌ name @typescript-eslint

✨ Plugins that disable or override other plugins or configs settings

  • The one that disables others. Need to go last. Or after what they disable (disabling override)
  • The one that acts as a base extends. They go first.

Example eslint-config-prettier

  • saying you can name typescriptEslint
  • I would say name it "@typescript-eslint": typescriptEslintPlugin
    ◌ it’s what FlatCompat do
    ◌ less confusing to people
    ◌ natural to match with the conventions
  • you can also verify that in rules 🔥 👆
    ◌ that’s what is used
    ◌ You can navigate fast to the rules using CMD|CTRL + CLICK
  • but if typescriptEslint works
  • that means somehow eslint is converting that to match @typescript-eslint
    ◌ basically, adding @ and camel case to -
  • Personally, I will go with the full name
  • "@typescript-eslint": typescriptEslintPlugin
  • and directly I would go check the rules 🔥
  • and see what they used
  • ctrl|cmd + F when you are searching to handle it fast

✨ How to translate and use Eslint modules in FlatConfig 🔥

▪ Know what is involved

  • Plugin, rules, extends, overrides, mixture
    ◌ If it’s extends you can check the package source. And determine what is used.

▪️ Know the properties mapping from eslintrc to FlatConfig

  • Code Link to the translation logic in FlatCompat tool (as ref)
  • TK [LINK TO main article]
  • The ones that get directly copied
    ◌ First level directly copied
    - [“settings”, “rules”, “processor”]
    Language Options keys that get moved to languageOptions
    - [“globals”, “parser”, “parserOptions”]
    ▶︎ for parser you have to copy the definition
languageOptions[key] = eslintrcConfig[key].definition; // key == 'parser'

▶︎ For parserOptions

  • ecmaVersion and sourceType need to go into languageOptions directly
  • In old eslintrc they were in parserOptions. In FlatConfig they are in languageOptions

Linter Options keys that get moved to linterOptions
- [“noInlineConfig”, “reportUnusedDisableDirectives”]

▪️ For plugins

  • Set plugins in plugins prop object.
  • Make sure to name them following the name convention mentioned above
  • If the plugin does hold special processors per files (name starts with .). Then you can add at the beginning of Flat config. Object as follows:
[
{
files: [`**/*${processorName}`],
processor: pluginProcessors.get(`${pluginName}/${processorName}`)
},
// ...
]
  • Also, that’s the way if you need to use any custom processor provided by a plugin <pluginName>/<processorName>.

▪️ For env

  • You have to set globals in languageOptions.globals and use the globals package. And you can get sense and examples from the eslintrc built-in environments. And you set ecmaVersion in languageOptions.ecmaVersion. ecmaFeatures would go in languageOptions.parserOptions.ecmaFeatures . That gives you a good sense.

▪️ For overrides

  • overrides already follow a structure similar to FlatConfig
  • You would basically, keep the same order. And make the same transformation as in the above.
  • Keep in mind the order and when there is a configuration that disables others, it would need to come after what it would disable.

For extends

  • You have to check the module. The best way is to CMD|CTRL + CLICK in vscode and it would take you to the module code source. There check the structure. And determine the parts that are explained above.
  • For all the parts that make sense and can go in a particular part of your FlatConfig . Directly add them granularly to the right part. (A great example is Flat config: Setting up Eslint and Prettier with eslint-prettier-plugin article).
    ◌ For example instead of [{ <base> }, {}] , You do [{ directly granular object merging }] when it makes sense.
  • And you can use a separate config with the right overriding order depending on the thing. This would be the way to go, when there is a need for deep merging. That you can’t simply do through the granular way.

Check the full practical example

patterns in the code ✨

  • generators
  • recursivity with generators
  • data normalization
  • module or plugin resolution within normalization
  • Dependency management or resolution
  • Data transformation and remapping

My Other Eslint related articles ✨

▪️ Flat config system

▪️ Eslint prettier

▪️ String, name resolution (plugin, config)

--

--

Mohamed Lamine Allal

Developper, Entrepreneur, CTO, Writer! A magic chaser! And A passionate thinker! Obsessed with Performance! And magic for life! And deep thinking!