feat(angular): eslint config including template linting (#3988)

* feat(angular): eslint config including template linting

* feat(angular): migration add-template-support-and-presets

* feat(angular): add support for component and directive prefix in lint config

* fix(angular): tests

* fix(angular): e2e tests

* fix(angular): update to latest and make updates

* fix(angular): update to latest and make updates

* fix(angular): lockfile

* fix(angular): update to latest and make updates

* fix(angular): bump angular-eslint

Co-authored-by: Jason Jean <jasonjean1993@gmail.com>
This commit is contained in:
James Henry 2020-11-25 22:55:22 +04:00 committed by GitHub
parent d43a6229c7
commit bd92a12c33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 944 additions and 31 deletions

View File

@ -116,7 +116,7 @@ describe('Angular Package', () => {
expectTestsPass(await runCLIAsync(`test my-dir-${myapp} --no-watch`)); expectTestsPass(await runCLIAsync(`test my-dir-${myapp} --no-watch`));
}, 1000000); }, 1000000);
it('should support eslint', async () => { it('should support eslint and pass linting on the standard generated code', async () => {
const myapp = uniq('myapp'); const myapp = uniq('myapp');
runCLI(`generate @nrwl/angular:app ${myapp} --linter=eslint`); runCLI(`generate @nrwl/angular:app ${myapp} --linter=eslint`);
expect(runCLI(`lint ${myapp}`)).toContain('All files pass linting.'); expect(runCLI(`lint ${myapp}`)).toContain('All files pass linting.');
@ -125,4 +125,83 @@ describe('Angular Package', () => {
runCLI(`generate @nrwl/angular:lib ${mylib} --linter=eslint`); runCLI(`generate @nrwl/angular:lib ${mylib} --linter=eslint`);
expect(runCLI(`lint ${mylib}`)).toContain('All files pass linting.'); expect(runCLI(`lint ${mylib}`)).toContain('All files pass linting.');
}); });
it('should support eslint and successfully lint external HTML files and inline templates', async () => {
const myapp = uniq('myapp');
runCLI(`generate @nrwl/angular:app ${myapp} --linter=eslint`);
const templateWhichFailsBananaInBoxLintCheck = `<div ([foo])="bar"></div>`;
const wrappedAsInlineTemplate = `
import { Component } from '@angular/core';
@Component({
selector: 'inline-template-component',
template: \`
${templateWhichFailsBananaInBoxLintCheck}
\`,
})
export class InlineTemplateComponent {}
`;
// External HTML template file
updateFile(
`apps/${myapp}/src/app/app.component.html`,
templateWhichFailsBananaInBoxLintCheck
);
// Inline template within component.ts file
updateFile(
`apps/${myapp}/src/app/inline-template.component.ts`,
wrappedAsInlineTemplate
);
const appLintStdOut = runCLI(`lint ${myapp}`, { silenceError: true });
expect(appLintStdOut).toContain(`apps/${myapp}/src/app/app.component.html`);
expect(appLintStdOut).toContain(
`1:6 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-box`
);
expect(appLintStdOut).toContain(
`apps/${myapp}/src/app/inline-template.component.ts`
);
expect(appLintStdOut).toContain(
`5:21 error The selector should be prefixed by one of the prefixes: 'proj' (https://angular.io/guide/styleguide#style-02-07) @angular-eslint/component-selector`
);
expect(appLintStdOut).toContain(
`7:18 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-box`
);
const mylib = uniq('mylib');
runCLI(`generate @nrwl/angular:lib ${mylib} --linter=eslint`);
// External HTML template file
updateFile(
`libs/${mylib}/src/lib/some.component.html`,
templateWhichFailsBananaInBoxLintCheck
);
// Inline template within component.ts file
updateFile(
`libs/${mylib}/src/lib/inline-template.component.ts`,
wrappedAsInlineTemplate
);
const libLintStdOut = runCLI(`lint ${mylib}`, { silenceError: true });
expect(libLintStdOut).toContain(
`libs/${mylib}/src/lib/some.component.html`
);
expect(libLintStdOut).toContain(
`1:6 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-box`
);
expect(libLintStdOut).toContain(
`libs/${mylib}/src/lib/inline-template.component.ts`
);
expect(libLintStdOut).toContain(
`5:21 error The selector should be prefixed by one of the prefixes: 'proj' (https://angular.io/guide/styleguide#style-02-07) @angular-eslint/component-selector`
);
expect(libLintStdOut).toContain(
`7:18 error Invalid binding syntax. Use [(expr)] instead @angular-eslint/template/banana-in-box`
);
});
}); });

View File

@ -32,6 +32,9 @@
"@angular-devkit/build-webpack": "~0.1001.3", "@angular-devkit/build-webpack": "~0.1001.3",
"@angular-devkit/core": "~10.1.3", "@angular-devkit/core": "~10.1.3",
"@angular-devkit/schematics": "~10.1.3", "@angular-devkit/schematics": "~10.1.3",
"@angular-eslint/eslint-plugin": "0.8.0-beta.1",
"@angular-eslint/eslint-plugin-template": "0.8.0-beta.1",
"@angular-eslint/template-parser": "0.8.0-beta.1",
"@angular/cli": "~10.1.3", "@angular/cli": "~10.1.3",
"@angular/common": "~10.1.0", "@angular/common": "~10.1.0",
"@angular/compiler": "~10.1.0", "@angular/compiler": "~10.1.0",

View File

@ -45,6 +45,11 @@
"version": "10.4.0-beta.3", "version": "10.4.0-beta.3",
"description": "Adjust karma and protractor setup", "description": "Adjust karma and protractor setup",
"factory": "./src/migrations/update-10-4-0/update-10-4-0" "factory": "./src/migrations/update-10-4-0/update-10-4-0"
},
"add-template-support-and-presets-to-eslint": {
"version": "10.5.0-beta.0",
"description": "Update eslint config and builder to extend from new Nx Angular presets and lint templates",
"factory": "./src/migrations/update-10-5-0/add-template-support-and-presets-to-eslint"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -13,6 +13,7 @@
"whitelistedNonPeerDependencies": [ "whitelistedNonPeerDependencies": [
"@nrwl/", "@nrwl/",
"@angular-devkit", "@angular-devkit",
"@angular-eslint/",
"@schematics", "@schematics",
"jasmine-marbles" "jasmine-marbles"
] ]

View File

@ -33,13 +33,16 @@
"migrations": "./migrations.json" "migrations": "./migrations.json"
}, },
"peerDependencies": { "peerDependencies": {
"@angular-eslint/eslint-plugin": "*",
"@angular-eslint/eslint-plugin-template": "*",
"@angular-eslint/template-parser": "*",
"@nrwl/workspace": "*" "@nrwl/workspace": "*"
}, },
"dependencies": { "dependencies": {
"@angular-devkit/schematics": "~10.1.3",
"@nrwl/cypress": "*", "@nrwl/cypress": "*",
"@nrwl/jest": "*", "@nrwl/jest": "*",
"@nrwl/linter": "*", "@nrwl/linter": "*",
"@angular-devkit/schematics": "~10.1.3",
"@schematics/angular": "~10.1.3", "@schematics/angular": "~10.1.3",
"jasmine-marbles": "~0.6.0" "jasmine-marbles": "~0.6.0"
} }

View File

@ -0,0 +1,405 @@
import { Tree } from '@angular-devkit/schematics';
import { readJsonInTree, updateWorkspace } from '@nrwl/workspace';
import { callRule, createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runMigration } from '../../utils/testing';
describe('add-template-support-and-presets-to-eslint', () => {
describe('tslint-only workspace', () => {
let tree: Tree;
beforeEach(async () => {
tree = Tree.empty();
tree = createEmptyWorkspace(tree);
tree = await callRule(
updateWorkspace((workspace) => {
workspace.projects.add({
name: 'app1',
root: 'apps/app1',
sourceRoot: 'apps/app1/src',
projectType: 'application',
targets: {
lint: {
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: [
'apps/app1/tsconfig.app.json',
'apps/app1/tsconfig.spec.json',
],
exclude: ['**/node_modules/**', '!apps/app1/**/*'],
},
},
},
});
workspace.projects.add({
name: 'lib1',
root: 'libs/lib1',
sourceRoot: 'apps/lib1/src',
projectType: 'library',
targets: {
lint: {
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: [
'libs/lib1/tsconfig.app.json',
'libs/lib1/tsconfig.spec.json',
],
exclude: ['**/node_modules/**', '!libs/lib1/**/*'],
},
},
},
});
}),
tree
);
});
it('should do nothing', async () => {
const packageJsonBefore = JSON.parse(
tree.read('package.json').toString()
);
const result = await runMigration(
'add-template-support-and-presets-to-eslint',
{},
tree
);
expect(packageJsonBefore).toEqual(
JSON.parse(result.read('package.json').toString())
);
});
});
describe('workspace with at least one eslint project', () => {
let tree: Tree;
beforeEach(async () => {
tree = Tree.empty();
tree = createEmptyWorkspace(tree);
tree = await callRule(
updateWorkspace((workspace) => {
workspace.projects.add({
name: 'app1',
root: 'apps/app1',
sourceRoot: 'apps/app1/src',
projectType: 'application',
prefix: 'customprefix',
targets: {
lint: {
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['apps/app1/src/**/*.ts'],
},
},
},
});
// App still using TSLint, will be unaffected
workspace.projects.add({
name: 'app2',
root: 'apps/app2',
sourceRoot: 'apps/app2/src',
projectType: 'library',
targets: {
lint: {
builder: '@angular-devkit/build-angular:tslint',
options: {
tsConfig: [
'apps/app2/tsconfig.app.json',
'apps/app2/tsconfig.spec.json',
],
exclude: ['**/node_modules/**', '!apps/app2/**/*'],
},
},
},
});
workspace.projects.add({
name: 'lib1',
root: 'libs/lib1',
sourceRoot: 'libs/lib1/src',
projectType: 'application',
// No custom prefix, will fall back to npm scope in nx.json
prefix: undefined,
targets: {
lint: {
builder: '@nrwl/linter:eslint',
options: {
lintFilePatterns: ['libs/lib1/src/**/*.ts'],
},
},
},
});
}),
tree
);
});
it('should do nothing if the root eslint config has not been updated to use overrides by the latest migrations', async () => {
tree.create(
'.eslintrc.json',
JSON.stringify({
// no overrides here, so can't have been updated/generated by latest Nx
rules: {},
})
);
const packageJsonBefore = JSON.parse(
tree.read('package.json').toString()
);
const result = await runMigration(
'add-template-support-and-presets-to-eslint',
{},
tree
);
expect(packageJsonBefore).toEqual(
JSON.parse(result.read('package.json').toString())
);
});
it(`should update the workspace package.json if they are using the latest eslint config from Nx`, async () => {
tree.create(
'.eslintrc.json',
JSON.stringify({
overrides: [
{
files: [],
rules: {},
},
],
})
);
const packageJsonBefore = JSON.parse(
tree.read('package.json').toString()
);
const result = await runMigration(
'add-template-support-and-presets-to-eslint',
{},
tree
);
expect(packageJsonBefore).toMatchInlineSnapshot(`
Object {
"dependencies": Object {},
"devDependencies": Object {},
"name": "test-name",
}
`);
expect(JSON.parse(result.read('package.json').toString()))
.toMatchInlineSnapshot(`
Object {
"dependencies": Object {},
"devDependencies": Object {
"@angular-eslint/eslint-plugin": "0.8.0-beta.1",
"@angular-eslint/eslint-plugin-template": "0.8.0-beta.1",
"@angular-eslint/template-parser": "0.8.0-beta.1",
},
"name": "test-name",
}
`);
});
it(`should update any relevant project .eslintrc.json files`, async () => {
tree.create(
'.eslintrc.json',
JSON.stringify({
overrides: [
{
files: [],
rules: {},
},
],
})
);
tree.create(
'apps/app1/.eslintrc.json',
JSON.stringify({
rules: {},
})
);
tree.create(
'libs/lib1/.eslintrc.json',
JSON.stringify({
rules: {},
})
);
const result = await runMigration(
'add-template-support-and-presets-to-eslint',
{},
tree
);
expect(JSON.parse(result.read('apps/app1/.eslintrc.json').toString()))
.toMatchInlineSnapshot(`
Object {
"extends": "../../.eslintrc.json",
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"apps/app1/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "customprefix",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "customprefix",
"style": "camelCase",
"type": "attribute",
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {},
},
],
}
`);
expect(JSON.parse(result.read('libs/lib1/.eslintrc.json').toString()))
.toMatchInlineSnapshot(`
Object {
"extends": "../../.eslintrc.json",
"ignorePatterns": Array [
"!**/*",
],
"overrides": Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"libs/lib1/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "camelCase",
"type": "attribute",
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {},
},
],
}
`);
});
it(`should update any relevant project builder configuration to include HTML templates`, async () => {
tree.create(
'.eslintrc.json',
JSON.stringify({
overrides: [
{
files: [],
rules: {},
},
],
})
);
tree.create(
'apps/app1/.eslintrc.json',
JSON.stringify({
rules: {},
})
);
tree.create(
'libs/lib1/.eslintrc.json',
JSON.stringify({
rules: {},
})
);
const result = await runMigration(
'add-template-support-and-presets-to-eslint',
{},
tree
);
const workspace = readJsonInTree(result, 'workspace.json');
expect(workspace.projects['app1'].architect).toMatchInlineSnapshot(`
Object {
"lint": Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/app1/src/**/*.ts",
"apps/app1/src/**/*.html",
],
},
},
}
`);
expect(workspace.projects['lib1'].architect).toMatchInlineSnapshot(`
Object {
"lint": Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"libs/lib1/src/**/*.ts",
"libs/lib1/src/**/*.html",
],
},
},
}
`);
});
});
});

View File

@ -0,0 +1,149 @@
import { normalize } from '@angular-devkit/core';
import { chain, Rule, Tree } from '@angular-devkit/schematics';
import {
addDepsToPackageJson,
formatFiles,
getNpmScope,
offsetFromRoot,
readJsonInTree,
readWorkspace,
updateJsonInTree,
updateWorkspaceInTree,
} from '@nrwl/workspace';
import { join } from 'path';
/**
* It was decided with Jason that we would do a simple replacement in this migration
* because Angular + ESLint support has been experimental until this point.
*/
function updateESLintConfigForProject(
projectRoot: string,
prefix: string
): Rule {
return updateJsonInTree(join(normalize(projectRoot), '.eslintrc.json'), () =>
createAngularEslintJson(projectRoot, prefix)
);
}
function addHTMLPatternToBuilderConfig(
projectName: string,
projectSourceRoot: string,
targetName: string
): Rule {
return updateWorkspaceInTree((workspaceJson) => {
workspaceJson.projects[projectName].architect[
targetName
].options.lintFilePatterns.push(`${projectSourceRoot}/**/*.html`);
return workspaceJson;
});
}
function updateProjectESLintConfigsAndBuilders(host: Tree): Rule {
/**
* Make sure user is already using ESLint and is up to date with
* previous migrations
*/
if (!host.exists('.eslintrc.json')) {
return;
}
if (!readJsonInTree(host, '.eslintrc.json').overrides?.length) {
return;
}
const workspace = readWorkspace(host);
const rules = [];
let addedExtraDevDeps = false;
Object.keys(workspace.projects).forEach((projectName) => {
const project = workspace.projects[projectName];
Object.keys(project.architect).forEach((targetName) => {
const target = project.architect[targetName];
if (target.builder !== '@nrwl/linter:eslint') {
return;
}
/**
* To reach this point we must have found that at least one project is configured
* to use ESLint, therefore we should install the extra devDependencies to ensure
* that the updated ESLint config will work correctly
*/
if (!addedExtraDevDeps) {
rules.push(
addDepsToPackageJson(
{},
{
'@angular-eslint/eslint-plugin': '0.8.0-beta.1',
'@angular-eslint/eslint-plugin-template': '0.8.0-beta.1',
'@angular-eslint/template-parser': '0.8.0-beta.1',
},
false
)
);
addedExtraDevDeps = true;
}
// Using the npm scope as the fallback replicates the generation behavior
const projectPrefx = project.prefix || getNpmScope(host);
rules.push(updateESLintConfigForProject(project.root, projectPrefx));
rules.push(
addHTMLPatternToBuilderConfig(
projectName,
project.sourceRoot,
targetName
)
);
});
});
return chain(rules);
}
export default function () {
return chain([updateProjectESLintConfigsAndBuilders, formatFiles()]);
}
/**
* This is effectively a duplicate of the current (at the time of writing this migration) combined
* logic (across workspace utils/lint.ts and angular utils/lint.ts) for an Angular Project's ESLint config.
*/
function createAngularEslintJson(projectRoot: string, prefix: string) {
return {
extends: `${offsetFromRoot(projectRoot)}.eslintrc.json`,
ignorePatterns: ['!**/*'],
overrides: [
{
files: ['*.ts'],
extends: [
'plugin:@nrwl/nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
parserOptions: {
project: [`${projectRoot}/tsconfig.*?.json`],
},
rules: {
'@angular-eslint/directive-selector': [
'error',
{ type: 'attribute', prefix, style: 'camelCase' },
],
'@angular-eslint/component-selector': [
'error',
{ type: 'element', prefix, style: 'kebab-case' },
],
},
},
{
files: ['*.html'],
extends: ['plugin:@nrwl/nx/angular-template'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
},
],
};
}

View File

@ -1,27 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app --linter eslint should add an architect target for lint 1`] = `
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app/src/**/*.ts",
],
},
}
`;
exports[`app --linter eslint should add an architect target for lint 2`] = `
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app-e2e/**/*.{js,ts}",
],
},
}
`;
exports[`app nested should update workspace.json 1`] = ` exports[`app nested should update workspace.json 1`] = `
Object { Object {
"architect": Object { "architect": Object {

View File

@ -335,12 +335,85 @@ describe('app', () => {
appTree appTree
); );
const workspaceJson = readJsonInTree(tree, 'workspace.json'); const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect( expect(workspaceJson.projects['my-app'].architect.lint)
workspaceJson.projects['my-app'].architect.lint .toMatchInlineSnapshot(`
).toMatchSnapshot(); Object {
expect( "builder": "@nrwl/linter:eslint",
workspaceJson.projects['my-app-e2e'].architect.lint "options": Object {
).toMatchSnapshot(); "lintFilePatterns": Array [
"apps/my-app/src/**/*.ts",
"apps/my-app/src/**/*.html",
],
},
}
`);
expect(workspaceJson.projects['my-app-e2e'].architect.lint)
.toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-app-e2e/**/*.{js,ts}",
],
},
}
`);
});
it('should add valid eslint JSON configuration which extends from Nx presets', async () => {
const tree = await runSchematic(
'app',
{ name: 'myApp', linter: 'eslint' },
appTree
);
const eslintConfig = readJsonInTree(tree, 'apps/my-app/.eslintrc.json');
expect(eslintConfig.overrides).toMatchInlineSnapshot(`
Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"apps/my-app/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "camelCase",
"type": "attribute",
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {},
},
]
`);
}); });
}); });
}); });

View File

@ -43,8 +43,13 @@ import {
updateWorkspaceInTree, updateWorkspaceInTree,
appsDir, appsDir,
} from '@nrwl/workspace/src/utils/ast-utils'; } from '@nrwl/workspace/src/utils/ast-utils';
import {
createAngularEslintJson,
extraEslintDependencies,
} from '../../utils/lint';
interface NormalizedSchema extends Schema { interface NormalizedSchema extends Schema {
prefix: string; // we set a default for this in normalizeOptions, so it is no longer optional
appProjectRoot: string; appProjectRoot: string;
e2eProjectName: string; e2eProjectName: string;
e2eProjectRoot: string; e2eProjectRoot: string;
@ -499,6 +504,7 @@ function updateProject(options: NormalizedSchema): Rule {
fixedProject.architect.lint.builder = '@nrwl/linter:eslint'; fixedProject.architect.lint.builder = '@nrwl/linter:eslint';
fixedProject.architect.lint.options.lintFilePatterns = [ fixedProject.architect.lint.options.lintFilePatterns = [
`${options.appProjectRoot}/src/**/*.ts`, `${options.appProjectRoot}/src/**/*.ts`,
`${options.appProjectRoot}/src/**/*.html`,
]; ];
delete fixedProject.architect.lint.options.tsConfig; delete fixedProject.architect.lint.options.tsConfig;
delete fixedProject.architect.lint.options.exclude; delete fixedProject.architect.lint.options.exclude;
@ -816,6 +822,14 @@ export default function (schema: Schema): Rule {
options.routing ? addRouterRootConfiguration(options) : noop(), options.routing ? addRouterRootConfiguration(options) : noop(),
addLintFiles(options.appProjectRoot, options.linter, { addLintFiles(options.appProjectRoot, options.linter, {
onlyGlobal: options.linter === Linter.TsLint, // local lint files are added differently when tslint onlyGlobal: options.linter === Linter.TsLint, // local lint files are added differently when tslint
localConfig:
options.linter === Linter.TsLint
? undefined
: createAngularEslintJson(options.appProjectRoot, options.prefix),
extraPackageDeps:
options.linter === Linter.TsLint
? undefined
: extraEslintDependencies,
}), }),
options.linter === 'tslint' ? updateTsLintConfig(options) : noop(), options.linter === 'tslint' ? updateTsLintConfig(options) : noop(),
options.unitTestRunner === 'jest' options.unitTestRunner === 'jest'

View File

@ -9,4 +9,5 @@ export interface NormalizedSchema extends Schema {
moduleName: string; moduleName: string;
projectDirectory: string; projectDirectory: string;
parsedTags: string[]; parsedTags: string[];
prefix: string; // we set a default for this in normalizeOptions, so it is no longer optional
} }

View File

@ -170,6 +170,7 @@ export function updateProject(options: NormalizedSchema): Rule {
fixedProject.architect.lint.builder = '@nrwl/linter:eslint'; fixedProject.architect.lint.builder = '@nrwl/linter:eslint';
fixedProject.architect.lint.options.lintFilePatterns = [ fixedProject.architect.lint.options.lintFilePatterns = [
`${options.projectRoot}/src/**/*.ts`, `${options.projectRoot}/src/**/*.ts`,
`${options.projectRoot}/src/**/*.html`,
]; ];
delete fixedProject.architect.lint.options.tsConfig; delete fixedProject.architect.lint.options.tsConfig;
delete fixedProject.architect.lint.options.exclude; delete fixedProject.architect.lint.options.exclude;

View File

@ -1204,4 +1204,85 @@ describe('lib', () => {
); );
}); });
}); });
describe('--linter', () => {
describe('eslint', () => {
it('should add an architect target for lint', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', linter: 'eslint' },
appTree
);
const workspaceJson = readJsonInTree(tree, 'workspace.json');
expect(workspaceJson.projects['my-lib'].architect.lint)
.toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"libs/my-lib/src/**/*.ts",
"libs/my-lib/src/**/*.html",
],
},
}
`);
});
it('should add valid eslint JSON configuration which extends from Nx presets', async () => {
const tree = await runSchematic(
'lib',
{ name: 'myLib', linter: 'eslint' },
appTree
);
const eslintConfig = readJsonInTree(tree, 'libs/my-lib/.eslintrc.json');
expect(eslintConfig.overrides).toMatchInlineSnapshot(`
Array [
Object {
"extends": Array [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates",
],
"files": Array [
"*.ts",
],
"parserOptions": Object {
"project": Array [
"libs/my-lib/tsconfig.*?.json",
],
},
"rules": Object {
"@angular-eslint/component-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "kebab-case",
"type": "element",
},
],
"@angular-eslint/directive-selector": Array [
"error",
Object {
"prefix": "proj",
"style": "camelCase",
"type": "attribute",
},
],
},
},
Object {
"extends": Array [
"plugin:@nrwl/nx/angular-template",
],
"files": Array [
"*.html",
],
"rules": Object {},
},
]
`);
});
});
});
}); });

View File

@ -22,6 +22,10 @@ import { updateProject } from './lib/update-project';
import { updateTsConfig } from './lib/update-tsconfig'; import { updateTsConfig } from './lib/update-tsconfig';
import { Schema } from './schema'; import { Schema } from './schema';
import { enableStrictTypeChecking } from './lib/enable-strict-type-checking'; import { enableStrictTypeChecking } from './lib/enable-strict-type-checking';
import {
createAngularEslintJson,
extraEslintDependencies,
} from '../../utils/lint';
export default function (schema: Schema): Rule { export default function (schema: Schema): Rule {
return (host: Tree): Rule => { return (host: Tree): Rule => {
@ -47,6 +51,14 @@ export default function (schema: Schema): Rule {
}), }),
addLintFiles(options.projectRoot, options.linter, { addLintFiles(options.projectRoot, options.linter, {
onlyGlobal: options.linter === Linter.TsLint, onlyGlobal: options.linter === Linter.TsLint,
localConfig:
options.linter === Linter.TsLint
? undefined
: createAngularEslintJson(options.projectRoot, options.prefix),
extraPackageDeps:
options.linter === Linter.TsLint
? undefined
: extraEslintDependencies,
}), }),
addUnitTestRunner(options), addUnitTestRunner(options),
// TODO: Remove this after Angular 10.1.0 // TODO: Remove this after Angular 10.1.0

View File

@ -0,0 +1,47 @@
import { angularEslintVersion } from './versions';
export const extraEslintDependencies = {
dependencies: {},
devDependencies: {
'@angular-eslint/eslint-plugin': angularEslintVersion,
'@angular-eslint/eslint-plugin-template': angularEslintVersion,
'@angular-eslint/template-parser': angularEslintVersion,
},
};
export const createAngularEslintJson = (
projectRoot: string,
prefix: string
) => ({
overrides: [
{
files: ['*.ts'],
extends: [
'plugin:@nrwl/nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
parserOptions: {
project: [`${projectRoot}/tsconfig.*?.json`],
},
rules: {
'@angular-eslint/directive-selector': [
'error',
{ type: 'attribute', prefix, style: 'camelCase' },
],
'@angular-eslint/component-selector': [
'error',
{ type: 'element', prefix, style: 'kebab-case' },
],
},
},
{
files: ['*.html'],
extends: ['plugin:@nrwl/nx/angular-template'],
/**
* Having an empty rules object present makes it more obvious to the user where they would
* extend things from if they needed to
*/
rules: {},
},
],
});

View File

@ -5,3 +5,4 @@ export const angularJsVersion = '1.7.9';
export const ngrxVersion = '10.0.0'; export const ngrxVersion = '10.0.0';
export const rxjsVersion = '~6.5.5'; export const rxjsVersion = '~6.5.5';
export const jestPresetAngularVersion = '8.3.1'; export const jestPresetAngularVersion = '8.3.1';
export const angularEslintVersion = '0.8.0-beta.1';

View File

@ -0,0 +1,19 @@
/**
* This configuration is intended to be applied to ALL .html files in Angular
* projects within an Nx workspace, as well as extracted inline templates from
* .component.ts files (or similar).
*
* It should therefore NOT contain any rules or plugins which are related to
* Angular source code.
*
* NOTE: The processor to extract the inline templates is applied in users'
* configs by the relevant schematic.
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default {
plugins: ['@angular-eslint/template'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {},
};

View File

@ -0,0 +1,16 @@
/**
* This configuration is intended to be applied to ALL .ts files in Angular
* projects within an Nx workspace.
*
* It should therefore NOT contain any rules or plugins which are related to
* Angular Templates, or more cross-cutting concerns which are not specific
* to Angular.
*
* This configuration is intended to be combined with other configs from this
* package.
*/
export default {
plugins: ['@angular-eslint'],
extends: ['plugin:@angular-eslint/recommended'],
rules: {},
};

View File

@ -4,6 +4,8 @@ import reactTmp from './configs/react-tmp';
import reactBase from './configs/react-base'; import reactBase from './configs/react-base';
import reactJsx from './configs/react-jsx'; import reactJsx from './configs/react-jsx';
import reactTypescript from './configs/react-typescript'; import reactTypescript from './configs/react-typescript';
import angularCode from './configs/angular';
import angularTemplate from './configs/angular-template';
import enforceModuleBoundaries, { import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName, RULE_NAME as enforceModuleBoundariesRuleName,
@ -17,6 +19,8 @@ module.exports = {
'react-base': reactBase, 'react-base': reactBase,
'react-typescript': reactTypescript, 'react-typescript': reactTypescript,
'react-jsx': reactJsx, 'react-jsx': reactJsx,
angular: angularCode,
'angular-template': angularTemplate,
}, },
rules: { rules: {
[enforceModuleBoundariesRuleName]: enforceModuleBoundaries, [enforceModuleBoundariesRuleName]: enforceModuleBoundaries,

View File

@ -200,6 +200,27 @@
ora "4.0.3" ora "4.0.3"
rxjs "6.5.4" rxjs "6.5.4"
"@angular-eslint/eslint-plugin-template@0.8.0-beta.1":
version "0.8.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-0.8.0-beta.1.tgz#750675884e161d162afb4e314fc5621275e383a7"
integrity sha512-nyy93m+2WBe5Fpc2IKzWPH1bGqNZYd+BU6nYhNssiYXPRcDWBqIsIhEM74dRK/0AN37tUguJ2weZ6xF6fVN8hw==
dependencies:
"@typescript-eslint/experimental-utils" "4.3.0"
"@angular-eslint/eslint-plugin@0.8.0-beta.1":
version "0.8.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-0.8.0-beta.1.tgz#154824ba3fe8589605c71762c793a42936b27f74"
integrity sha512-+vCkUpM81qjb0UwxlUUwGML0lLzmnhqf5HHsRzzfwhd0s5g3DPw8w4Z/CDNBagJmTzSUSnH1GF9uEdtyJCEprA==
dependencies:
"@typescript-eslint/experimental-utils" "4.3.0"
"@angular-eslint/template-parser@0.8.0-beta.1":
version "0.8.0-beta.1"
resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-0.8.0-beta.1.tgz#a9a9eaccc4536b5edd6c3483b7ff81fc906874e3"
integrity sha512-fiLfwlWWwYz657SxcNfPKsl4HiItqj7mNZuMPlxsiKSyT/+pwTNzMttCafy2v0144SNmHEslZS1nQfc1Nq715g==
dependencies:
eslint-scope "^5.1.0"
"@angular/cli@~10.1.3": "@angular/cli@~10.1.3":
version "10.1.3" version "10.1.3"
resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.1.3.tgz#188f99583814e97727787869065d228c1b1f4407" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-10.1.3.tgz#188f99583814e97727787869065d228c1b1f4407"
@ -10806,7 +10827,7 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-scope@^5.1.1: eslint-scope@^5.1.0, eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==