feat: ✨ write and manipulate eslint.config.js files
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "^1.0.1",
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/prompts": "^2.4.4"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
3
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user