feat: ✨ write and manipulate eslint.config.js files
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
"yaml": "^2.3.1"
|
"yaml": "^2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/estree": "^1.0.1",
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.4.2",
|
||||||
"@types/prompts": "^2.4.4"
|
"@types/prompts": "^2.4.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,11 @@ export default class Cli {
|
|||||||
|
|
||||||
console.log(packages[0].config);
|
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 path from 'node:path';
|
||||||
import {} from 'recast';
|
|
||||||
import notNull from './lib/notNull.js';
|
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
|
* @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
|
* @param {import('./types').Package} pkg The package to generate the config string from
|
||||||
* @returns {import('./types').ConfigFile} The config file object
|
* @returns {import('./types').ConfigFile} The config file object
|
||||||
*/
|
*/
|
||||||
generateFileObj(pkg) {
|
generateObj(pkg) {
|
||||||
/** @type {import('./types').ConfigFile} */
|
/** @type {import('./types').ConfigFile} */
|
||||||
const configObj = {
|
const configObj = {
|
||||||
path: path.join(pkg.path, 'eslint.config.js'),
|
path: path.join(pkg.path, 'eslint.config.js'),
|
||||||
@@ -118,9 +120,175 @@ export default class ConfigsWriter {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(configObj);
|
|
||||||
|
|
||||||
return 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
|
specifier: ^2.3.1
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/estree':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.4.2
|
specifier: ^20.4.2
|
||||||
version: 20.4.2
|
version: 20.4.2
|
||||||
|
|||||||
Reference in New Issue
Block a user