|
1 | 1 | import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
|
2 | 2 | import stripJsonComments from 'strip-json-comments';
|
3 | 3 | 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 | +]; |
4 | 11 |
|
5 | 12 | export default function (schema: Schema): Rule {
|
6 | 13 | 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 | + ); |
8 | 18 | const docs = 'https://ngrx.io/guide/eslint-plugin';
|
9 | 19 |
|
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)) { |
12 | 26 | 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}\`. |
14 | 30 | The NgRx ESLint Plugin is installed but not configured.
|
15 |
| -
|
16 | 31 | Please see ${docs} to configure the NgRx ESLint Plugin.
|
17 |
| -`); |
| 32 | + `); |
18 | 33 | return host;
|
19 | 34 | }
|
20 | 35 |
|
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' |
35 | 128 | ) {
|
36 |
| - json.overrides = [configurePlugin(schema.config)]; |
| 129 | + tseslintConfigCall = node; |
37 | 130 | }
|
| 131 | + ts.forEachChild(node, findTsEslintConfigCalls); |
| 132 | + } |
| 133 | + findTsEslintConfigCalls(source); |
38 | 134 |
|
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 | + }`; |
40 | 146 |
|
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 | +} |
43 | 159 |
|
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 | + ? ` |
51 | 209 | Details:
|
52 | 210 | ${err.message}
|
53 | 211 | `
|
54 |
| - : ''; |
55 |
| - context.logger.warn(` |
| 212 | + : ''; |
| 213 | + context.logger.warn(` |
56 | 214 | Something went wrong while adding the NgRx ESLint Plugin.
|
57 | 215 | The NgRx ESLint Plugin is installed but not configured.
|
58 |
| -
|
59 | 216 | Please see ${docs} to configure the NgRx ESLint Plugin.
|
60 | 217 | ${detailsContent}
|
61 | 218 | `);
|
62 |
| - } |
63 |
| - }; |
64 |
| - function configurePlugin(config: Schema['config']): Record<string, unknown> { |
65 |
| - return { |
66 |
| - files: ['*.ts'], |
67 |
| - extends: [`plugin:@ngrx/${config}`], |
68 |
| - }; |
69 | 219 | }
|
70 | 220 | }
|
0 commit comments