-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(eslint-plugin): add schematic support for flat configs #4747
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,70 +1,220 @@ | ||
| import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; | ||
| import stripJsonComments from 'strip-json-comments'; | ||
| import type { Schema } from './schema'; | ||
| import * as ts from 'typescript'; | ||
|
|
||
| export const possibleFlatConfigPaths = [ | ||
| 'eslint.config.js', | ||
| 'eslint.config.mjs', | ||
| 'eslint.config.cjs', | ||
| ]; | ||
|
|
||
| export default function addNgRxESLintPlugin(schema: Schema): Rule { | ||
| return (host: Tree, context: SchematicContext) => { | ||
| const eslintConfigPath = '.eslintrc.json'; | ||
| const jsonConfigPath = '.eslintrc.json'; | ||
| const flatConfigPath = possibleFlatConfigPaths.find((path) => | ||
| host.exists(path) | ||
| ); | ||
| const docs = 'https://ngrx.io/guide/eslint-plugin'; | ||
|
|
||
| const eslint = host.read(eslintConfigPath)?.toString('utf-8'); | ||
| if (!eslint) { | ||
| if (flatConfigPath) { | ||
| updateFlatConfig(host, context, flatConfigPath, schema, docs); | ||
| return host; | ||
| } | ||
|
|
||
| if (!host.exists(jsonConfigPath)) { | ||
| context.logger.warn(` | ||
| Could not find the ESLint config at \`${eslintConfigPath}\`. | ||
| Could not find an ESLint config at any of ${possibleFlatConfigPaths.join( | ||
| ', ' | ||
| )} or \`${jsonConfigPath}\`. | ||
| The NgRx ESLint Plugin is installed but not configured. | ||
|
|
||
| Please see ${docs} to configure the NgRx ESLint Plugin. | ||
| `); | ||
| `); | ||
| return host; | ||
| } | ||
|
|
||
| try { | ||
| const json = JSON.parse(stripJsonComments(eslint)); | ||
| if (json.overrides) { | ||
| if ( | ||
| !json.overrides.some((override: any) => | ||
| override.extends?.some((extend: any) => | ||
| extend.startsWith('plugin:@ngrx') | ||
| ) | ||
| ) | ||
| ) { | ||
| json.overrides.push(configurePlugin(schema.config)); | ||
| } | ||
| } else if ( | ||
| !json.extends?.some((extend: any) => extend.startsWith('plugin:@ngrx')) | ||
| updateJsonConfig(host, context, jsonConfigPath, schema, docs); | ||
| return host; | ||
| }; | ||
| } | ||
|
|
||
| function updateFlatConfig( | ||
| host: Tree, | ||
| context: SchematicContext, | ||
| flatConfigPath: string, | ||
| schema: Schema, | ||
| docs: string | ||
| ): void { | ||
| const ngrxPlugin = '@ngrx/eslint-plugin/v9'; | ||
| const content = host.read(flatConfigPath)?.toString('utf-8'); | ||
| if (!content) { | ||
| context.logger.error( | ||
| `Could not read the ESLint flat config at \`${flatConfigPath}\`.` | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (content.includes(ngrxPlugin)) { | ||
| context.logger.info( | ||
| `Skipping installation, the NgRx ESLint Plugin is already installed in your flat config.` | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (!content.includes('tseslint.config')) { | ||
| context.logger.warn( | ||
| `No tseslint found, skipping the installation of the NgRx ESLint Plugin in your flat config.` | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const source = ts.createSourceFile( | ||
| flatConfigPath, | ||
| content, | ||
| ts.ScriptTarget.Latest, | ||
| true | ||
| ); | ||
|
|
||
| const recorder = host.beginUpdate(flatConfigPath); | ||
| addImport(); | ||
| addNgRxPlugin(); | ||
|
|
||
| host.commitUpdate(recorder); | ||
| context.logger.info(` | ||
| The NgRx ESLint Plugin is installed and configured using the '${schema.config}' configuration in your flat config. | ||
| See ${docs} for more details. | ||
| `); | ||
|
|
||
| function addImport() { | ||
| const isESM = content!.includes('export default'); | ||
| if (isESM) { | ||
| const lastImport = source.statements | ||
| .filter((statement) => ts.isImportDeclaration(statement)) | ||
| .reverse()[0]; | ||
| recorder.insertRight( | ||
| lastImport?.end ?? 0, | ||
| `\nimport ngrx from '${ngrxPlugin}';` | ||
| ); | ||
| } else { | ||
| const lastRequireVariableDeclaration = source.statements | ||
| .filter((statement) => { | ||
| if (!ts.isVariableStatement(statement)) return false; | ||
| const decl = statement.declarationList.declarations[0]; | ||
| if (!decl.initializer) return false; | ||
| return ( | ||
| ts.isCallExpression(decl.initializer) && | ||
| decl.initializer.expression.getText() === 'require' | ||
| ); | ||
| }) | ||
| .reverse()[0]; | ||
|
|
||
| recorder.insertRight( | ||
| lastRequireVariableDeclaration?.end ?? 0, | ||
| `\nconst ngrx = require('${ngrxPlugin}');` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function addNgRxPlugin() { | ||
| let tseslintConfigCall: ts.CallExpression | null = null; | ||
| function findTsEslintConfigCalls(node: ts.Node) { | ||
| if (tseslintConfigCall) { | ||
| return; | ||
| } | ||
|
|
||
| if ( | ||
| ts.isCallExpression(node) && | ||
| node.expression.getText() === 'tseslint.config' | ||
| ) { | ||
| json.overrides = [configurePlugin(schema.config)]; | ||
| tseslintConfigCall = node; | ||
| } | ||
| ts.forEachChild(node, findTsEslintConfigCalls); | ||
| } | ||
| findTsEslintConfigCalls(source); | ||
|
|
||
| host.overwrite(eslintConfigPath, JSON.stringify(json, null, 2)); | ||
| if (tseslintConfigCall) { | ||
| tseslintConfigCall = tseslintConfigCall as ts.CallExpression; | ||
| const lastArgument = | ||
| tseslintConfigCall.arguments[tseslintConfigCall.arguments.length - 1]; | ||
| const plugin = ` { | ||
| files: ['**/*.ts'], | ||
| extends: [ | ||
| ...ngrx.configs.${schema.config}, | ||
| ], | ||
| rules: {}, | ||
| }`; | ||
|
|
||
| context.logger.info(` | ||
| The NgRx ESLint Plugin is installed and configured with the '${schema.config}' config. | ||
| if (lastArgument) { | ||
| recorder.remove(lastArgument.pos, lastArgument.end - lastArgument.pos); | ||
| recorder.insertRight( | ||
| lastArgument.pos, | ||
| `${lastArgument.getFullText()},\n${plugin}` | ||
| ); | ||
| } else { | ||
| recorder.insertRight(tseslintConfigCall.end - 1, `\n${plugin}\n`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Take a look at the docs at ${docs} if you want to change the default configuration. | ||
| `); | ||
| return host; | ||
| } catch (err) { | ||
| const detailsContent = | ||
| err instanceof Error | ||
| ? ` | ||
| function updateJsonConfig( | ||
| host: Tree, | ||
| context: SchematicContext, | ||
| jsonConfigPath: string, | ||
| schema: Schema, | ||
| docs: string | ||
| ): void { | ||
| const eslint = host.read(jsonConfigPath)?.toString('utf-8'); | ||
| if (!eslint) { | ||
| context.logger.error(` | ||
| Could not find the ESLint config at \`${jsonConfigPath}\`. | ||
| The NgRx ESLint Plugin is installed but not configured. | ||
| Please see ${docs} to configure the NgRx ESLint Plugin. | ||
| `); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const json = JSON.parse(stripJsonComments(eslint)); | ||
| const plugin = { | ||
| files: ['*.ts'], | ||
| extends: [`plugin:@ngrx/${schema.config}`], | ||
| }; | ||
| if (json.overrides) { | ||
| if ( | ||
| !json.overrides.some((override: any) => | ||
| override.extends?.some((extend: any) => | ||
| extend.startsWith('plugin:@ngrx') | ||
| ) | ||
| ) | ||
| ) { | ||
| json.overrides.push(plugin); | ||
| } | ||
| } else if ( | ||
| !json.extends?.some((extend: any) => extend.startsWith('plugin:@ngrx')) | ||
| ) { | ||
| json.overrides = [plugin]; | ||
| } | ||
|
|
||
| host.overwrite(jsonConfigPath, JSON.stringify(json, null, 2)); | ||
|
|
||
| context.logger.info(` | ||
| The NgRx ESLint Plugin is installed and configured with the '${schema.config}' config. | ||
| Take a look at the docs at ${docs} if you want to change the default configuration. | ||
| `); | ||
| } catch (err) { | ||
| const detailsContent = | ||
| err instanceof Error | ||
| ? ` | ||
| Details: | ||
| ${err.message} | ||
| ` | ||
| : ''; | ||
| context.logger.warn(` | ||
| : ''; | ||
| context.logger.warn(` | ||
| Something went wrong while adding the NgRx ESLint Plugin. | ||
| The NgRx ESLint Plugin is installed but not configured. | ||
|
|
||
| Please see ${docs} to configure the NgRx ESLint Plugin. | ||
| ${detailsContent} | ||
| `); | ||
| } | ||
| }; | ||
| function configurePlugin(config: Schema['config']): Record<string, unknown> { | ||
| return { | ||
| files: ['*.ts'], | ||
| extends: [`plugin:@ngrx/${config}`], | ||
| }; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.