882 lines
26 KiB
TypeScript
882 lines
26 KiB
TypeScript
import {
|
|
ChangeType,
|
|
StringChange,
|
|
applyChangesToString,
|
|
joinPathFragments,
|
|
} from '@nx/devkit';
|
|
import { Linter } from 'eslint';
|
|
import * as ts from 'typescript';
|
|
import { mapFilePath } from './path-utils';
|
|
|
|
/**
|
|
* Remove all overrides from the config file
|
|
*/
|
|
export function removeOverridesFromLintConfig(content: string): string {
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
|
|
const exportsArray = findAllBlocks(source);
|
|
if (!exportsArray) {
|
|
return content;
|
|
}
|
|
|
|
const changes: StringChange[] = [];
|
|
exportsArray.forEach((node, i) => {
|
|
if (isOverride(node)) {
|
|
const commaOffset =
|
|
i < exportsArray.length - 1 || exportsArray.hasTrailingComma ? 1 : 0;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: node.pos,
|
|
length: node.end - node.pos + commaOffset,
|
|
});
|
|
}
|
|
});
|
|
|
|
return applyChangesToString(content, changes);
|
|
}
|
|
|
|
function findAllBlocks(source: ts.SourceFile): ts.NodeArray<ts.Node> {
|
|
return ts.forEachChild(source, function analyze(node) {
|
|
if (
|
|
ts.isExpressionStatement(node) &&
|
|
ts.isBinaryExpression(node.expression) &&
|
|
node.expression.left.getText() === 'module.exports' &&
|
|
ts.isArrayLiteralExpression(node.expression.right)
|
|
) {
|
|
return node.expression.right.elements;
|
|
}
|
|
});
|
|
}
|
|
|
|
function isOverride(node: ts.Node): boolean {
|
|
return (
|
|
(ts.isObjectLiteralExpression(node) &&
|
|
node.properties.some((p) => p.name.getText() === 'files')) ||
|
|
// detect ...compat.config(...).map(...)
|
|
(ts.isSpreadElement(node) &&
|
|
ts.isCallExpression(node.expression) &&
|
|
ts.isPropertyAccessExpression(node.expression.expression) &&
|
|
ts.isArrowFunction(node.expression.arguments[0]) &&
|
|
ts.isParenthesizedExpression(node.expression.arguments[0].body))
|
|
);
|
|
}
|
|
|
|
export function hasOverride(
|
|
content: string,
|
|
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean
|
|
): boolean {
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
const exportsArray = findAllBlocks(source);
|
|
if (!exportsArray) {
|
|
return false;
|
|
}
|
|
for (const node of exportsArray) {
|
|
if (isOverride(node)) {
|
|
let objSource;
|
|
if (ts.isObjectLiteralExpression(node)) {
|
|
objSource = node.getFullText();
|
|
} else {
|
|
const fullNodeText =
|
|
node['expression'].arguments[0].body.expression.getFullText();
|
|
// strip any spread elements
|
|
objSource = fullNodeText.replace(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, '');
|
|
}
|
|
const data = JSON.parse(
|
|
objSource
|
|
// ensure property names have double quotes so that JSON.parse works
|
|
.replace(/'/g, '"')
|
|
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
|
|
);
|
|
if (lookup(data)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const STRIP_SPREAD_ELEMENTS = /\s*\.\.\.[a-zA-Z0-9_]+,?\n?/g;
|
|
|
|
function parseTextToJson(text: string): any {
|
|
return JSON.parse(
|
|
text
|
|
// ensure property names have double quotes so that JSON.parse works
|
|
.replace(/'/g, '"')
|
|
.replace(/\s([a-zA-Z0-9_]+)\s*:/g, ' "$1": ')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Finds an override matching the lookup function and applies the update function to it
|
|
*/
|
|
export function replaceOverride(
|
|
content: string,
|
|
root: string,
|
|
lookup: (override: Linter.ConfigOverride<Linter.RulesRecord>) => boolean,
|
|
update: (
|
|
override: Linter.ConfigOverride<Linter.RulesRecord>
|
|
) => Linter.ConfigOverride<Linter.RulesRecord>
|
|
): string {
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
const exportsArray = findAllBlocks(source);
|
|
if (!exportsArray) {
|
|
return content;
|
|
}
|
|
const changes: StringChange[] = [];
|
|
exportsArray.forEach((node) => {
|
|
if (isOverride(node)) {
|
|
let objSource;
|
|
let start, end;
|
|
if (ts.isObjectLiteralExpression(node)) {
|
|
objSource = node.getFullText();
|
|
start = node.properties.pos + 1; // keep leading line break
|
|
end = node.properties.end;
|
|
} else {
|
|
const fullNodeText =
|
|
node['expression'].arguments[0].body.expression.getFullText();
|
|
// strip any spread elements
|
|
objSource = fullNodeText.replace(STRIP_SPREAD_ELEMENTS, '');
|
|
start =
|
|
node['expression'].arguments[0].body.expression.properties.pos +
|
|
(fullNodeText.length - objSource.length);
|
|
end = node['expression'].arguments[0].body.expression.properties.end;
|
|
}
|
|
const data = parseTextToJson(objSource);
|
|
if (lookup(data)) {
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start,
|
|
length: end - start,
|
|
});
|
|
const updatedData = update(data);
|
|
mapFilePaths(updatedData);
|
|
changes.push({
|
|
type: ChangeType.Insert,
|
|
index: start,
|
|
text: JSON.stringify(updatedData, null, 2).slice(2, -2), // remove curly braces and start/end line breaks since we are injecting just properties
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return applyChangesToString(content, changes);
|
|
}
|
|
|
|
/**
|
|
* Adding require statement to the top of the file
|
|
*/
|
|
export function addImportToFlatConfig(
|
|
content: string,
|
|
variable: string | string[],
|
|
imp: string
|
|
): string {
|
|
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
|
|
const foundBindingVars: ts.NodeArray<ts.BindingElement> = ts.forEachChild(
|
|
source,
|
|
function analyze(node) {
|
|
// we can only combine object binding patterns
|
|
if (!Array.isArray(variable)) {
|
|
return;
|
|
}
|
|
if (
|
|
ts.isVariableStatement(node) &&
|
|
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
|
|
ts.isObjectBindingPattern(node.declarationList.declarations[0].name) &&
|
|
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
|
|
node.declarationList.declarations[0].initializer.expression.getText() ===
|
|
'require' &&
|
|
ts.isStringLiteral(
|
|
node.declarationList.declarations[0].initializer.arguments[0]
|
|
) &&
|
|
node.declarationList.declarations[0].initializer.arguments[0].text ===
|
|
imp
|
|
) {
|
|
return node.declarationList.declarations[0].name.elements;
|
|
}
|
|
}
|
|
);
|
|
|
|
if (foundBindingVars && Array.isArray(variable)) {
|
|
const newVariables = variable.filter(
|
|
(v) => !foundBindingVars.some((fv) => v === fv.name.getText())
|
|
);
|
|
if (newVariables.length === 0) {
|
|
return content;
|
|
}
|
|
const isMultiLine = foundBindingVars.hasTrailingComma;
|
|
const pos = foundBindingVars.end;
|
|
const nodes = ts.factory.createNodeArray(
|
|
newVariables.map((v) =>
|
|
ts.factory.createBindingElement(undefined, undefined, v)
|
|
)
|
|
);
|
|
const insert = printer.printList(
|
|
ts.ListFormat.ObjectBindingPatternElements,
|
|
nodes,
|
|
source
|
|
);
|
|
return applyChangesToString(content, [
|
|
{
|
|
type: ChangeType.Insert,
|
|
index: pos,
|
|
text: isMultiLine ? `,\n${insert}` : `,${insert}`,
|
|
},
|
|
]);
|
|
}
|
|
|
|
const hasSameIdentifierVar: boolean = ts.forEachChild(
|
|
source,
|
|
function analyze(node) {
|
|
// we are searching for a single variable
|
|
if (Array.isArray(variable)) {
|
|
return;
|
|
}
|
|
if (
|
|
ts.isVariableStatement(node) &&
|
|
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
|
|
ts.isIdentifier(node.declarationList.declarations[0].name) &&
|
|
node.declarationList.declarations[0].name.getText() === variable &&
|
|
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
|
|
node.declarationList.declarations[0].initializer.expression.getText() ===
|
|
'require' &&
|
|
ts.isStringLiteral(
|
|
node.declarationList.declarations[0].initializer.arguments[0]
|
|
) &&
|
|
node.declarationList.declarations[0].initializer.arguments[0].text ===
|
|
imp
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
);
|
|
|
|
if (hasSameIdentifierVar) {
|
|
return content;
|
|
}
|
|
|
|
// the import was not found, create a new one
|
|
const requireStatement = generateRequire(
|
|
typeof variable === 'string'
|
|
? variable
|
|
: ts.factory.createObjectBindingPattern(
|
|
variable.map((v) =>
|
|
ts.factory.createBindingElement(undefined, undefined, v)
|
|
)
|
|
),
|
|
imp
|
|
);
|
|
const insert = printer.printNode(
|
|
ts.EmitHint.Unspecified,
|
|
requireStatement,
|
|
source
|
|
);
|
|
return applyChangesToString(content, [
|
|
{
|
|
type: ChangeType.Insert,
|
|
index: 0,
|
|
text: `${insert}\n`,
|
|
},
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Injects new ts.expression to the end of the module.exports array.
|
|
*/
|
|
export function addBlockToFlatConfigExport(
|
|
content: string,
|
|
config: ts.Expression | ts.SpreadElement,
|
|
options: { insertAtTheEnd?: boolean; checkBaseConfig?: boolean } = {
|
|
insertAtTheEnd: true,
|
|
}
|
|
): string {
|
|
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
|
|
const exportsArray = ts.forEachChild(source, function analyze(node) {
|
|
if (
|
|
ts.isExpressionStatement(node) &&
|
|
ts.isBinaryExpression(node.expression) &&
|
|
node.expression.left.getText() === 'module.exports' &&
|
|
ts.isArrayLiteralExpression(node.expression.right)
|
|
) {
|
|
return node.expression.right.elements;
|
|
}
|
|
});
|
|
const insert = printer.printNode(ts.EmitHint.Expression, config, source);
|
|
if (options.insertAtTheEnd) {
|
|
const index =
|
|
exportsArray.length > 0
|
|
? exportsArray.at(exportsArray.length - 1).end
|
|
: exportsArray.pos;
|
|
return applyChangesToString(content, [
|
|
{
|
|
type: ChangeType.Insert,
|
|
index,
|
|
text: `,\n${insert}`,
|
|
},
|
|
]);
|
|
} else {
|
|
const index =
|
|
exportsArray.length > 0 ? exportsArray.at(0).pos : exportsArray.pos;
|
|
return applyChangesToString(content, [
|
|
{
|
|
type: ChangeType.Insert,
|
|
index,
|
|
text: `\n${insert},`,
|
|
},
|
|
]);
|
|
}
|
|
}
|
|
|
|
export function removePlugin(
|
|
content: string,
|
|
pluginName: string,
|
|
pluginImport: string
|
|
) {
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
const changes: StringChange[] = [];
|
|
ts.forEachChild(source, function analyze(node) {
|
|
if (
|
|
ts.isVariableStatement(node) &&
|
|
ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
|
|
ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
|
|
node.declarationList.declarations[0].initializer.arguments.length &&
|
|
ts.isStringLiteral(
|
|
node.declarationList.declarations[0].initializer.arguments[0]
|
|
) &&
|
|
node.declarationList.declarations[0].initializer.arguments[0].text ===
|
|
pluginImport
|
|
) {
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: node.pos,
|
|
length: node.end - node.pos,
|
|
});
|
|
}
|
|
});
|
|
ts.forEachChild(source, function analyze(node) {
|
|
if (
|
|
ts.isExpressionStatement(node) &&
|
|
ts.isBinaryExpression(node.expression) &&
|
|
node.expression.left.getText() === 'module.exports' &&
|
|
ts.isArrayLiteralExpression(node.expression.right)
|
|
) {
|
|
const blockElements = node.expression.right.elements;
|
|
blockElements.forEach((element) => {
|
|
if (ts.isObjectLiteralExpression(element)) {
|
|
const pluginsElem = element.properties.find(
|
|
(prop) => prop.name?.getText() === 'plugins'
|
|
) as ts.PropertyAssignment;
|
|
if (!pluginsElem) {
|
|
return;
|
|
}
|
|
if (ts.isArrayLiteralExpression(pluginsElem.initializer)) {
|
|
const pluginsArray = pluginsElem.initializer;
|
|
const plugins = parseTextToJson(
|
|
pluginsElem.initializer
|
|
.getText()
|
|
.replace(STRIP_SPREAD_ELEMENTS, '')
|
|
);
|
|
|
|
if (plugins.length > 1) {
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: pluginsArray.pos,
|
|
length: pluginsArray.end - pluginsArray.pos,
|
|
});
|
|
changes.push({
|
|
type: ChangeType.Insert,
|
|
index: pluginsArray.pos,
|
|
text: JSON.stringify(plugins.filter((p) => p !== pluginName)),
|
|
});
|
|
} else {
|
|
const keys = element.properties.map((prop) =>
|
|
prop.name?.getText()
|
|
);
|
|
if (keys.length > 1) {
|
|
const removeComma =
|
|
keys.indexOf('plugins') < keys.length - 1 ||
|
|
element.properties.hasTrailingComma;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: pluginsElem.pos + (removeComma ? 1 : 0),
|
|
length:
|
|
pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0),
|
|
});
|
|
} else {
|
|
const removeComma =
|
|
blockElements.indexOf(element) < blockElements.length - 1 ||
|
|
blockElements.hasTrailingComma;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: element.pos + (removeComma ? 1 : 0),
|
|
length: element.end - element.pos + (removeComma ? 1 : 0),
|
|
});
|
|
}
|
|
}
|
|
} else if (ts.isObjectLiteralExpression(pluginsElem.initializer)) {
|
|
const pluginsObj = pluginsElem.initializer;
|
|
if (pluginsElem.initializer.properties.length > 1) {
|
|
const plugin = pluginsObj.properties.find(
|
|
(prop) => prop.name?.['text'] === pluginName
|
|
) as ts.PropertyAssignment;
|
|
const removeComma =
|
|
pluginsObj.properties.indexOf(plugin) <
|
|
pluginsObj.properties.length - 1 ||
|
|
pluginsObj.properties.hasTrailingComma;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: plugin.pos + (removeComma ? 1 : 0),
|
|
length: plugin.end - plugin.pos + (removeComma ? 1 : 0),
|
|
});
|
|
} else {
|
|
const keys = element.properties.map((prop) =>
|
|
prop.name?.getText()
|
|
);
|
|
if (keys.length > 1) {
|
|
const removeComma =
|
|
keys.indexOf('plugins') < keys.length - 1 ||
|
|
element.properties.hasTrailingComma;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: pluginsElem.pos + (removeComma ? 1 : 0),
|
|
length:
|
|
pluginsElem.end - pluginsElem.pos + (removeComma ? 1 : 0),
|
|
});
|
|
} else {
|
|
const removeComma =
|
|
blockElements.indexOf(element) < blockElements.length - 1 ||
|
|
blockElements.hasTrailingComma;
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: element.pos + (removeComma ? 1 : 0),
|
|
length: element.end - element.pos + (removeComma ? 1 : 0),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return applyChangesToString(content, changes);
|
|
}
|
|
|
|
export function removeCompatExtends(
|
|
content: string,
|
|
compatExtends: string[]
|
|
): string {
|
|
const source = ts.createSourceFile(
|
|
'',
|
|
content,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
const changes: StringChange[] = [];
|
|
findAllBlocks(source).forEach((node) => {
|
|
if (
|
|
ts.isSpreadElement(node) &&
|
|
ts.isCallExpression(node.expression) &&
|
|
ts.isArrowFunction(node.expression.arguments[0]) &&
|
|
ts.isParenthesizedExpression(node.expression.arguments[0].body) &&
|
|
ts.isPropertyAccessExpression(node.expression.expression) &&
|
|
ts.isCallExpression(node.expression.expression.expression)
|
|
) {
|
|
const callExp = node.expression.expression.expression;
|
|
if (
|
|
((callExp.expression.getText() === 'compat.config' &&
|
|
callExp.arguments[0].getText().includes('extends')) ||
|
|
callExp.expression.getText() === 'compat.extends') &&
|
|
compatExtends.some((ext) =>
|
|
callExp.arguments[0].getText().includes(ext)
|
|
)
|
|
) {
|
|
// remove the whole node
|
|
changes.push({
|
|
type: ChangeType.Delete,
|
|
start: node.pos,
|
|
length: node.end - node.pos,
|
|
});
|
|
// and replace it with new one
|
|
const paramName =
|
|
node.expression.arguments[0].parameters[0].name.getText();
|
|
const body = node.expression.arguments[0].body.expression.getFullText();
|
|
changes.push({
|
|
type: ChangeType.Insert,
|
|
index: node.pos,
|
|
text:
|
|
'\n' +
|
|
body.replace(
|
|
new RegExp('[ \t]s*...' + paramName + '[ \t]*,?\\s*', 'g'),
|
|
''
|
|
),
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
return applyChangesToString(content, changes);
|
|
}
|
|
|
|
/**
|
|
* Add plugins block to the top of the export blocks
|
|
*/
|
|
export function addPluginsToExportsBlock(
|
|
content: string,
|
|
plugins: { name: string; varName: string; imp: string }[]
|
|
): string {
|
|
const pluginsBlock = ts.factory.createObjectLiteralExpression(
|
|
[
|
|
ts.factory.createPropertyAssignment(
|
|
'plugins',
|
|
ts.factory.createObjectLiteralExpression(
|
|
plugins.map(({ name, varName }) => {
|
|
return ts.factory.createPropertyAssignment(
|
|
ts.factory.createStringLiteral(name),
|
|
ts.factory.createIdentifier(varName)
|
|
);
|
|
})
|
|
)
|
|
),
|
|
],
|
|
false
|
|
);
|
|
return addBlockToFlatConfigExport(content, pluginsBlock, {
|
|
insertAtTheEnd: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds compat if missing to flat config
|
|
*/
|
|
export function addCompatToFlatConfig(content: string) {
|
|
let result = content;
|
|
result = addImportToFlatConfig(result, 'js', '@eslint/js');
|
|
if (result.includes('const compat = new FlatCompat')) {
|
|
return result;
|
|
}
|
|
result = addImportToFlatConfig(result, 'FlatCompat', '@eslint/eslintrc');
|
|
const index = result.indexOf('module.exports');
|
|
return applyChangesToString(result, [
|
|
{
|
|
type: ChangeType.Insert,
|
|
index: index - 1,
|
|
text: `${DEFAULT_FLAT_CONFIG}\n`,
|
|
},
|
|
]);
|
|
}
|
|
|
|
const DEFAULT_FLAT_CONFIG = `
|
|
const compat = new FlatCompat({
|
|
baseDirectory: __dirname,
|
|
recommendedConfig: js.configs.recommended,
|
|
});
|
|
`;
|
|
|
|
/**
|
|
* Generate node list representing the imports and the exports blocks
|
|
* Optionally add flat compat initialization
|
|
*/
|
|
export function createNodeList(
|
|
importsMap: Map<string, string>,
|
|
exportElements: ts.Expression[],
|
|
isFlatCompatNeeded: boolean
|
|
): ts.NodeArray<
|
|
ts.VariableStatement | ts.Identifier | ts.ExpressionStatement | ts.SourceFile
|
|
> {
|
|
const importsList = [];
|
|
if (isFlatCompatNeeded) {
|
|
importsMap.set('@eslint/js', 'js');
|
|
|
|
importsList.push(
|
|
generateRequire(
|
|
ts.factory.createObjectBindingPattern([
|
|
ts.factory.createBindingElement(undefined, undefined, 'FlatCompat'),
|
|
]),
|
|
'@eslint/eslintrc'
|
|
)
|
|
);
|
|
}
|
|
|
|
// generateRequire(varName, imp, ts.factory);
|
|
Array.from(importsMap.entries()).forEach(([imp, varName]) => {
|
|
importsList.push(generateRequire(varName, imp));
|
|
});
|
|
|
|
return ts.factory.createNodeArray([
|
|
// add plugin imports
|
|
...importsList,
|
|
ts.createSourceFile(
|
|
'',
|
|
isFlatCompatNeeded ? DEFAULT_FLAT_CONFIG : '',
|
|
ts.ScriptTarget.Latest,
|
|
false,
|
|
ts.ScriptKind.JS
|
|
),
|
|
// creates:
|
|
// module.exports = [ ... ];
|
|
ts.factory.createExpressionStatement(
|
|
ts.factory.createBinaryExpression(
|
|
ts.factory.createPropertyAccessExpression(
|
|
ts.factory.createIdentifier('module'),
|
|
ts.factory.createIdentifier('exports')
|
|
),
|
|
ts.factory.createToken(ts.SyntaxKind.EqualsToken),
|
|
ts.factory.createArrayLiteralExpression(exportElements, true)
|
|
)
|
|
),
|
|
]);
|
|
}
|
|
|
|
export function generateSpreadElement(name: string): ts.SpreadElement {
|
|
return ts.factory.createSpreadElement(ts.factory.createIdentifier(name));
|
|
}
|
|
|
|
export function generatePluginExtendsElement(
|
|
plugins: string[]
|
|
): ts.SpreadElement {
|
|
return ts.factory.createSpreadElement(
|
|
ts.factory.createCallExpression(
|
|
ts.factory.createPropertyAccessExpression(
|
|
ts.factory.createIdentifier('compat'),
|
|
ts.factory.createIdentifier('extends')
|
|
),
|
|
undefined,
|
|
plugins.map((plugin) => ts.factory.createStringLiteral(plugin))
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Stringifies TS nodes to file content string
|
|
*/
|
|
export function stringifyNodeList(
|
|
nodes: ts.NodeArray<
|
|
| ts.VariableStatement
|
|
| ts.Identifier
|
|
| ts.ExpressionStatement
|
|
| ts.SourceFile
|
|
>
|
|
): string {
|
|
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
const resultFile = ts.createSourceFile(
|
|
'',
|
|
'',
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.JS
|
|
);
|
|
return (
|
|
printer
|
|
.printList(ts.ListFormat.MultiLine, nodes, resultFile)
|
|
// add new line before compat initialization
|
|
.replace(
|
|
/const compat = new FlatCompat/,
|
|
'\nconst compat = new FlatCompat'
|
|
)
|
|
// add new line before module.exports = ...
|
|
.replace(/module\.exports/, '\nmodule.exports')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* generates AST require statement
|
|
*/
|
|
export function generateRequire(
|
|
variableName: string | ts.ObjectBindingPattern,
|
|
imp: string
|
|
): ts.VariableStatement {
|
|
return ts.factory.createVariableStatement(
|
|
undefined,
|
|
ts.factory.createVariableDeclarationList(
|
|
[
|
|
ts.factory.createVariableDeclaration(
|
|
variableName,
|
|
undefined,
|
|
undefined,
|
|
ts.factory.createCallExpression(
|
|
ts.factory.createIdentifier('require'),
|
|
undefined,
|
|
[ts.factory.createStringLiteral(imp)]
|
|
)
|
|
),
|
|
],
|
|
ts.NodeFlags.Const
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generates AST object or spread element based on JSON override object
|
|
*/
|
|
export function generateFlatOverride(
|
|
override: Linter.ConfigOverride<Linter.RulesRecord>
|
|
): ts.ObjectLiteralExpression | ts.SpreadElement {
|
|
mapFilePaths(override);
|
|
if (
|
|
!override.env &&
|
|
!override.extends &&
|
|
!override.plugins &&
|
|
!override.parser
|
|
) {
|
|
return generateAst(override);
|
|
}
|
|
const { files, excludedFiles, rules, ...rest } = override;
|
|
|
|
const objectLiteralElements: ts.ObjectLiteralElementLike[] = [
|
|
ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
|
|
];
|
|
addTSObjectProperty(objectLiteralElements, 'files', files);
|
|
addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles);
|
|
addTSObjectProperty(objectLiteralElements, 'rules', rules);
|
|
|
|
return ts.factory.createSpreadElement(
|
|
ts.factory.createCallExpression(
|
|
ts.factory.createPropertyAccessExpression(
|
|
ts.factory.createCallExpression(
|
|
ts.factory.createPropertyAccessExpression(
|
|
ts.factory.createIdentifier('compat'),
|
|
ts.factory.createIdentifier('config')
|
|
),
|
|
undefined,
|
|
[generateAst(rest)]
|
|
),
|
|
ts.factory.createIdentifier('map')
|
|
),
|
|
undefined,
|
|
[
|
|
ts.factory.createArrowFunction(
|
|
undefined,
|
|
undefined,
|
|
[
|
|
ts.factory.createParameterDeclaration(
|
|
undefined,
|
|
undefined,
|
|
'config'
|
|
),
|
|
],
|
|
undefined,
|
|
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
|
|
ts.factory.createParenthesizedExpression(
|
|
ts.factory.createObjectLiteralExpression(
|
|
objectLiteralElements,
|
|
true
|
|
)
|
|
)
|
|
),
|
|
]
|
|
)
|
|
);
|
|
}
|
|
|
|
export function mapFilePaths(
|
|
override: Linter.ConfigOverride<Linter.RulesRecord>
|
|
) {
|
|
if (override.files) {
|
|
override.files = Array.isArray(override.files)
|
|
? override.files
|
|
: [override.files];
|
|
override.files = override.files.map((file) => mapFilePath(file));
|
|
}
|
|
if (override.excludedFiles) {
|
|
override.excludedFiles = Array.isArray(override.excludedFiles)
|
|
? override.excludedFiles
|
|
: [override.excludedFiles];
|
|
override.excludedFiles = override.excludedFiles.map((file) =>
|
|
mapFilePath(file)
|
|
);
|
|
}
|
|
}
|
|
|
|
function addTSObjectProperty(
|
|
elements: ts.ObjectLiteralElementLike[],
|
|
key: string,
|
|
value: unknown
|
|
) {
|
|
if (value) {
|
|
elements.push(ts.factory.createPropertyAssignment(key, generateAst(value)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates an AST from a JSON-type input
|
|
*/
|
|
export function generateAst<T>(input: unknown): T {
|
|
if (Array.isArray(input)) {
|
|
return ts.factory.createArrayLiteralExpression(
|
|
input.map((item) => generateAst<ts.Expression>(item)),
|
|
input.length > 1 // multiline only if more than one item
|
|
) as T;
|
|
}
|
|
if (input === null) {
|
|
return ts.factory.createNull() as T;
|
|
}
|
|
if (typeof input === 'object') {
|
|
return ts.factory.createObjectLiteralExpression(
|
|
Object.entries(input)
|
|
.filter(([_, value]) => value !== undefined)
|
|
.map(([key, value]) =>
|
|
ts.factory.createPropertyAssignment(
|
|
isValidKey(key) ? key : ts.factory.createStringLiteral(key),
|
|
generateAst<ts.Expression>(value)
|
|
)
|
|
),
|
|
Object.keys(input).length > 1 // multiline only if more than one property
|
|
) as T;
|
|
}
|
|
if (typeof input === 'string') {
|
|
return ts.factory.createStringLiteral(input) as T;
|
|
}
|
|
if (typeof input === 'number') {
|
|
return ts.factory.createNumericLiteral(input) as T;
|
|
}
|
|
if (typeof input === 'boolean') {
|
|
return (input ? ts.factory.createTrue() : ts.factory.createFalse()) as T;
|
|
}
|
|
// since we are parsing JSON, this should never happen
|
|
throw new Error(`Unknown type: ${typeof input} `);
|
|
}
|
|
|
|
function isValidKey(key: string): boolean {
|
|
return /^[a-zA-Z0-9_]+$/.test(key);
|
|
}
|