feat: write and manipulate eslint.config.js files

This commit is contained in:
Guz013
2023-08-08 16:08:23 -03:00
parent 978f06605e
commit 983d4958f2
4 changed files with 181 additions and 5 deletions

View File

@@ -37,6 +37,7 @@
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/estree": "^1.0.1",
"@types/node": "^20.4.2",
"@types/prompts": "^2.4.4"
}

View File

@@ -84,7 +84,11 @@ export default class Cli {
console.log(packages[0].config);
packages.map(pkg => {pkg.configFile = writer.generateFileObj(pkg); return pkg;});
packages.map(async pkg => {
pkg.configFile = writer.generateObj(pkg);
console.log(await writer.write(pkg.configFile));
return pkg;
});
}

View File

@@ -1,6 +1,8 @@
import path from 'node:path';
import {} from 'recast';
import notNull from './lib/notNull.js';
import * as recast from 'recast';
import fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
/**
* @param {import('./types').ConfigFile['imports']} map1 - The map to has it values merged from map2
@@ -68,7 +70,7 @@ export default class ConfigsWriter {
* @param {import('./types').Package} pkg The package to generate the config string from
* @returns {import('./types').ConfigFile} The config file object
*/
generateFileObj(pkg) {
generateObj(pkg) {
/** @type {import('./types').ConfigFile} */
const configObj = {
path: path.join(pkg.path, 'eslint.config.js'),
@@ -118,9 +120,175 @@ export default class ConfigsWriter {
}
console.log(configObj);
return configObj;
}
/**
* ! NOTE:
* These functions declared bellow are notably hard to read and have lots of exceptions and
* disabled eslint and typescript checks. Unfortunately this is something that I wasn't able to
* prevent because a lot of the AST typescript types are somewhat wrong or simply hard to work
* with them.
*
* But for somewhat help developing and prevent unwanted errors in the future, the types and eslint
* errors are explicitly disabled and types are explicitly overridden. This is why there are so
* many JSDoc type annotations and comments in general.
*
* Any help to make this code more readable and robust is appreciated
*/
/**
* @param {import('./types').ConfigFile['configs']} configs The configs objects defined in the config file
* @returns {(import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression)[]}
* The ast expressions nodes to be printed
*/
createConfigExpressions(configs) {
return configs
.map(c => {
/** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const e = recast.parse(c).program.body[0].expression; return e;
})
.filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type));
}
/**
* @param {import('./types').ConfigFile['presets']} presets The presets objects defined in the config file
* @returns {import('estree').SpreadElement[]}
* The ast expressions nodes to be printed
*/
createPresetExpressions(presets) {
return presets
.map(p => {
/** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const e = recast.parse(p).program.body[0].expression; return e;
})
.filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type))
.map(e => {
/** @type {import('estree').SpreadElement} */
const spreadElement = {
type: 'SpreadElement',
argument: e,
};
return spreadElement;
});
}
/**
* @param {import('./types').ConfigFile['rules']} rules The rules objects defined in the config file
* @returns {import('estree').ObjectExpression}
* The ast object expression nodes to be printed
*/
createRulesExpression(rules) {
/** @type {import('estree').ObjectExpression} */
const rulesObjectExpression = rules
.map(r => {
/** @type {import('estree').MemberExpression | import('estree').Identifier | import('estree').CallExpression} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const e = recast.parse(r).program.body[0].expression; return e;
})
.filter(e => ['MemberExpression', 'Identifier', 'CallExpression'].includes(e.type))
.map(e => {
/** @type {import('estree').SpreadElement} */
const spreadElement = {
type: 'SpreadElement',
argument: e,
};
return spreadElement;
}).reduce((acc, spreadElement) => {
acc.properties.push(spreadElement);
return acc;
}, {
/** @type {import('estree').ObjectExpression['type']} */
type: 'ObjectExpression',
/** @type {import('estree').ObjectExpression['properties']} */
properties: [],
});
/** @type {import('estree').ObjectExpression} */
const rulesExpression = {
type: 'ObjectExpression',
properties: [{
// @ts-expect-error because ObjectProperty doesn't exist in estree types
type: 'ObjectProperty',
key: { type: 'Identifier', name: 'rules' },
value: rulesObjectExpression,
}],
};
return rulesExpression;
}
/**
* @param {import('./types').ConfigFile} config The config file object to be transformed into a eslint.config.js file
* @returns {Promise<void>}
*/
async write(config) {
const existingConfig = existsSync(config.path) ? await fs.readFile(config.path, 'utf-8') : '';
/** @type {import('estree').ExportDefaultDeclaration}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const defaultExportTemplate = recast.parse([
'/** @type {import(\'eslint\').Linter.FlatConfig[]} */',
'export default [',
'',
'];',
].join('\n'), {
parser: (await import('recast/parsers/babel.js')),
}).program.body.find((/** @type {{ type: string; }} */ n) => n.type === 'ExportDefaultDeclaration');
/** @type {{program: import('estree').Program}} */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { program: ast } = recast.parse(existingConfig, { parser: (await import('recast/parsers/babel.js')) });
/** @type {import('estree').ExportDefaultDeclaration | undefined} */
// @ts-expect-error because the types don't match, but are expected to be correct here
// as the type needs to be ExportDefaultDeclaration
let defaultExport = ast.body.find(n => n.type === 'ExportDefaultDeclaration');
if (!defaultExport) {
defaultExport = defaultExportTemplate;
}
else if (defaultExport.declaration.type !== 'ArrayExpression') {
ast.body.push({
type: 'VariableDeclaration',
kind: 'const',
declarations: [{
type: 'VariableDeclarator',
id: { type: 'Identifier', name: 'oldConfig' },
// @ts-expect-error because defaultExport's declaration is a ArrayExpression
init: defaultExport.declaration,
}],
});
defaultExport = defaultExportTemplate;
// @ts-expect-error because defaultExport's declaration is a ArrayExpression
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
defaultExport.declaration.elements.push({
type: 'SpreadElement',
argument: { type: 'Identifier', name: 'oldConfig' },
});
}
const elementsExpressions = [];
if (config.presets.length > 0)
elementsExpressions.push(...this.createPresetExpressions(config.presets));
if (config.configs.length > 0)
elementsExpressions.push(...this.createConfigExpressions(config.configs));
if (config.rules.length > 0)
elementsExpressions.push(this.createRulesExpression(config.rules));
// @ts-expect-error because defaultExport's declaration is a ArrayExpression
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
defaultExport.declaration.elements.push(...elementsExpressions);
const idx = ast.body.findIndex(n => n.type === 'ExportDefaultDeclaration');
if (idx > -1) ast.body.splice(idx, 1);
ast.body.push(defaultExport);
const finalCode = recast.prettyPrint(ast, { parser: (await import('recast/parsers/babel.js')) }).code;
console.log(finalCode);
}
}

3
pnpm-lock.yaml generated
View File

@@ -121,6 +121,9 @@ importers:
specifier: ^2.3.1
version: 2.3.1
devDependencies:
'@types/estree':
specifier: ^1.0.1
version: 1.0.1
'@types/node':
specifier: ^20.4.2
version: 20.4.2