Skip to content

Commit f966d0a

Browse files
feat(eslint-plugin): add schematic support for flat configs (#4747)
1 parent 846088e commit f966d0a

File tree

2 files changed

+440
-157
lines changed

2 files changed

+440
-157
lines changed
Lines changed: 191 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,220 @@
11
import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
22
import stripJsonComments from 'strip-json-comments';
33
import type { Schema } from './schema';
4+
import * as ts from 'typescript';
5+
6+
export const possibleFlatConfigPaths = [
7+
'eslint.config.js',
8+
'eslint.config.mjs',
9+
'eslint.config.cjs',
10+
];
411

512
export default function (schema: Schema): Rule {
613
return (host: Tree, context: SchematicContext) => {
7-
const eslintConfigPath = '.eslintrc.json';
14+
const jsonConfigPath = '.eslintrc.json';
15+
const flatConfigPath = possibleFlatConfigPaths.find((path) =>
16+
host.exists(path)
17+
);
818
const docs = 'https://ngrx.io/guide/eslint-plugin';
919

10-
const eslint = host.read(eslintConfigPath)?.toString('utf-8');
11-
if (!eslint) {
20+
if (flatConfigPath) {
21+
updateFlatConfig(host, context, flatConfigPath, schema, docs);
22+
return host;
23+
}
24+
25+
if (!host.exists(jsonConfigPath)) {
1226
context.logger.warn(`
13-
Could not find the ESLint config at \`${eslintConfigPath}\`.
27+
Could not find an ESLint config at any of ${possibleFlatConfigPaths.join(
28+
', '
29+
)} or \`${jsonConfigPath}\`.
1430
The NgRx ESLint Plugin is installed but not configured.
15-
1631
Please see ${docs} to configure the NgRx ESLint Plugin.
17-
`);
32+
`);
1833
return host;
1934
}
2035

21-
try {
22-
const json = JSON.parse(stripJsonComments(eslint));
23-
if (json.overrides) {
24-
if (
25-
!json.overrides.some((override: any) =>
26-
override.extends?.some((extend: any) =>
27-
extend.startsWith('plugin:@ngrx')
28-
)
29-
)
30-
) {
31-
json.overrides.push(configurePlugin(schema.config));
32-
}
33-
} else if (
34-
!json.extends?.some((extend: any) => extend.startsWith('plugin:@ngrx'))
36+
updateJsonConfig(host, context, jsonConfigPath, schema, docs);
37+
return host;
38+
};
39+
}
40+
41+
function updateFlatConfig(
42+
host: Tree,
43+
context: SchematicContext,
44+
flatConfigPath: string,
45+
schema: Schema,
46+
docs: string
47+
): void {
48+
const ngrxPlugin = '@ngrx/eslint-plugin/v9';
49+
const content = host.read(flatConfigPath)?.toString('utf-8');
50+
if (!content) {
51+
context.logger.error(
52+
`Could not read the ESLint flat config at \`${flatConfigPath}\`.`
53+
);
54+
return;
55+
}
56+
57+
if (content.includes(ngrxPlugin)) {
58+
context.logger.info(
59+
`Skipping installation, the NgRx ESLint Plugin is already installed in your flat config.`
60+
);
61+
return;
62+
}
63+
64+
if (!content.includes('tseslint.config')) {
65+
context.logger.warn(
66+
`No tseslint found, skipping the installation of the NgRx ESLint Plugin in your flat config.`
67+
);
68+
return;
69+
}
70+
71+
const source = ts.createSourceFile(
72+
flatConfigPath,
73+
content,
74+
ts.ScriptTarget.Latest,
75+
true
76+
);
77+
78+
const recorder = host.beginUpdate(flatConfigPath);
79+
addImport();
80+
addNgRxPlugin();
81+
82+
host.commitUpdate(recorder);
83+
context.logger.info(`
84+
The NgRx ESLint Plugin is installed and configured using the '${schema.config}' configuration in your flat config.
85+
See ${docs} for more details.
86+
`);
87+
88+
function addImport() {
89+
const isESM = content!.includes('export default');
90+
if (isESM) {
91+
const lastImport = source.statements
92+
.filter((statement) => ts.isImportDeclaration(statement))
93+
.reverse()[0];
94+
recorder.insertRight(
95+
lastImport?.end ?? 0,
96+
`\nimport ngrx from '${ngrxPlugin}';`
97+
);
98+
} else {
99+
const lastRequireVariableDeclaration = source.statements
100+
.filter((statement) => {
101+
if (!ts.isVariableStatement(statement)) return false;
102+
const decl = statement.declarationList.declarations[0];
103+
if (!decl.initializer) return false;
104+
return (
105+
ts.isCallExpression(decl.initializer) &&
106+
decl.initializer.expression.getText() === 'require'
107+
);
108+
})
109+
.reverse()[0];
110+
111+
recorder.insertRight(
112+
lastRequireVariableDeclaration?.end ?? 0,
113+
`\nconst ngrx = require('${ngrxPlugin}');`
114+
);
115+
}
116+
}
117+
118+
function addNgRxPlugin() {
119+
let tseslintConfigCall: ts.CallExpression | null = null;
120+
function findTsEslintConfigCalls(node: ts.Node) {
121+
if (tseslintConfigCall) {
122+
return;
123+
}
124+
125+
if (
126+
ts.isCallExpression(node) &&
127+
node.expression.getText() === 'tseslint.config'
35128
) {
36-
json.overrides = [configurePlugin(schema.config)];
129+
tseslintConfigCall = node;
37130
}
131+
ts.forEachChild(node, findTsEslintConfigCalls);
132+
}
133+
findTsEslintConfigCalls(source);
38134

39-
host.overwrite(eslintConfigPath, JSON.stringify(json, null, 2));
135+
if (tseslintConfigCall) {
136+
tseslintConfigCall = tseslintConfigCall as ts.CallExpression;
137+
const lastArgument =
138+
tseslintConfigCall.arguments[tseslintConfigCall.arguments.length - 1];
139+
const plugin = ` {
140+
files: ['**/*.ts'],
141+
extends: [
142+
...ngrx.configs.${schema.config},
143+
],
144+
rules: {},
145+
}`;
40146

41-
context.logger.info(`
42-
The NgRx ESLint Plugin is installed and configured with the '${schema.config}' config.
147+
if (lastArgument) {
148+
recorder.remove(lastArgument.pos, lastArgument.end - lastArgument.pos);
149+
recorder.insertRight(
150+
lastArgument.pos,
151+
`${lastArgument.getFullText()},\n${plugin}`
152+
);
153+
} else {
154+
recorder.insertRight(tseslintConfigCall.end - 1, `\n${plugin}\n`);
155+
}
156+
}
157+
}
158+
}
43159

44-
Take a look at the docs at ${docs} if you want to change the default configuration.
45-
`);
46-
return host;
47-
} catch (err) {
48-
const detailsContent =
49-
err instanceof Error
50-
? `
160+
function updateJsonConfig(
161+
host: Tree,
162+
context: SchematicContext,
163+
jsonConfigPath: string,
164+
schema: Schema,
165+
docs: string
166+
): void {
167+
const eslint = host.read(jsonConfigPath)?.toString('utf-8');
168+
if (!eslint) {
169+
context.logger.error(`
170+
Could not find the ESLint config at \`${jsonConfigPath}\`.
171+
The NgRx ESLint Plugin is installed but not configured.
172+
Please see ${docs} to configure the NgRx ESLint Plugin.
173+
`);
174+
return;
175+
}
176+
177+
try {
178+
const json = JSON.parse(stripJsonComments(eslint));
179+
const plugin = {
180+
files: ['*.ts'],
181+
extends: [`plugin:@ngrx/${schema.config}`],
182+
};
183+
if (json.overrides) {
184+
if (
185+
!json.overrides.some((override: any) =>
186+
override.extends?.some((extend: any) =>
187+
extend.startsWith('plugin:@ngrx')
188+
)
189+
)
190+
) {
191+
json.overrides.push(plugin);
192+
}
193+
} else if (
194+
!json.extends?.some((extend: any) => extend.startsWith('plugin:@ngrx'))
195+
) {
196+
json.overrides = [plugin];
197+
}
198+
199+
host.overwrite(jsonConfigPath, JSON.stringify(json, null, 2));
200+
201+
context.logger.info(`
202+
The NgRx ESLint Plugin is installed and configured with the '${schema.config}' config.
203+
Take a look at the docs at ${docs} if you want to change the default configuration.
204+
`);
205+
} catch (err) {
206+
const detailsContent =
207+
err instanceof Error
208+
? `
51209
Details:
52210
${err.message}
53211
`
54-
: '';
55-
context.logger.warn(`
212+
: '';
213+
context.logger.warn(`
56214
Something went wrong while adding the NgRx ESLint Plugin.
57215
The NgRx ESLint Plugin is installed but not configured.
58-
59216
Please see ${docs} to configure the NgRx ESLint Plugin.
60217
${detailsContent}
61218
`);
62-
}
63-
};
64-
function configurePlugin(config: Schema['config']): Record<string, unknown> {
65-
return {
66-
files: ['*.ts'],
67-
extends: [`plugin:@ngrx/${config}`],
68-
};
69219
}
70220
}

0 commit comments

Comments
 (0)