Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 10 additions & 19 deletions modules/eslint-plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ngrx/eslint-plugin",
"name": "@ngrx/schematics",
"version": "19.0.1",
"description": "NgRx ESLint Plugin",
"description": "NgRx Schematics for Angular",
"repository": {
"type": "git",
"url": "git+https://github.com/ngrx/platform.git"
Expand All @@ -10,15 +10,20 @@
"RxJS",
"Angular",
"Redux",
"NgRx"
"NgRx",
"Schematics",
"Angular CLI"
],
"author": "NgRx",
"author": "Brandon Roberts <robertsbt@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/ngrx/platform/issues"
},
"homepage": "https://github.com/ngrx/platform#readme",
"schematics": "./schematics/collection.json",
"schematics": "./collection.json",
"ng-add": {
"save": "devDependencies"
},
"ng-update": {
"packageGroupName": "@ngrx/store",
"packageGroup": [
Expand All @@ -36,19 +41,5 @@
"@ngrx/signals"
],
"migrations": "./migrations/migration.json"
},
"ng-add": {
"save": "devDependencies"
},
"sideEffects": false,
"dependencies": {
"semver": "^7.3.5",
"strip-json-comments": "3.1.1"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": "*",
"typescript-eslint": "^8.0.0",
"@typescript-eslint/utils": "^8.0.0"
}
}
231 changes: 190 additions & 41 deletions modules/eslint-plugin/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,219 @@
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 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('@ngrx/eslint-plugin')) {
context.logger.info(
`Skipping the installing, 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 '@ngrx/eslint-plugin';`
);
} 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('@ngrx/eslint-plugin');\n`
);
}
}

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}`],
};
}
}
Loading
Loading