From 31ab48fae7b6190fd77ad9a528e196ddfc2fbc08 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 1 Dec 2022 13:17:32 +0100 Subject: [PATCH 1/2] fix(angular): fix standalone support for angular --- e2e/linter/src/linter.test.ts | 72 ++++++++++++++++++- .../add-linting/add-linting.spec.ts | 19 ++--- .../src/generators/add-linting/add-linting.ts | 50 ++++++++----- .../lib/add-angular-eslint-dependencies.ts | 6 +- .../lib/add-project-lint-target.ts | 6 +- .../lib/create-eslint-configuration.ts | 57 +++++++++++++++ .../__snapshots__/application.spec.ts.snap | 6 ++ .../application/application.spec.ts | 6 ++ .../src/generators/application/application.ts | 2 +- .../convert-tslint-to-eslint.spec.ts.snap | 6 ++ .../__snapshots__/library.spec.ts.snap | 21 ++++++ .../src/generators/library/library.spec.ts | 30 +++----- 12 files changed, 222 insertions(+), 59 deletions(-) diff --git a/e2e/linter/src/linter.test.ts b/e2e/linter/src/linter.test.ts index b1297ccc6d0c7..dbbf9d7ba04fe 100644 --- a/e2e/linter/src/linter.test.ts +++ b/e2e/linter/src/linter.test.ts @@ -442,7 +442,7 @@ export function tslibC(): string { describe('Root projects migration', () => { afterEach(() => cleanupProject()); - it('should set root project config to app and e2e app and migrate when another lib is added', () => { + it('(React standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { const myapp = uniq('myapp'); const mylib = uniq('mylib'); @@ -506,6 +506,76 @@ export function tslibC(): string { expect(libEslint.overrides[1].extends).toBeUndefined(); expect(libEslint.overrides[1].extends).toBeUndefined(); }); + + it('(Angular standalone) should set root project config to app and e2e app and migrate when another lib is added', () => { + const myapp = uniq('myapp'); + const mylib = uniq('mylib'); + + newProject(); + runCLI(`generate @nrwl/angular:app ${myapp} --rootProject=true`); + + let rootEslint = readJson('.eslintrc.json'); + let e2eEslint = readJson('e2e/.eslintrc.json'); + expect(() => checkFilesExist(`.eslintrc.base.json`)).toThrow(); + + // should directly refer to nx plugin + expect(rootEslint.plugins).toEqual(['@nrwl/nx']); + expect(e2eEslint.plugins).toEqual(['@nrwl/nx']); + // should only extend framework plugin + expect(e2eEslint.extends).toEqual(['plugin:cypress/recommended']); + // should have plugin extends + expect(rootEslint.overrides[0].files).toEqual(['*.ts']); + expect(rootEslint.overrides[0].extends).toEqual([ + 'plugin:@nrwl/nx/typescript', + 'plugin:@nrwl/nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ]); + expect(Object.keys(rootEslint.overrides[0].rules)).toEqual([ + '@angular-eslint/directive-selector', + '@angular-eslint/component-selector', + ]); + expect(rootEslint.overrides[1].files).toEqual(['*.html']); + expect(rootEslint.overrides[1].extends).toEqual([ + 'plugin:@nrwl/nx/angular-template', + ]); + expect(e2eEslint.overrides[0].extends).toEqual([ + 'plugin:@nrwl/nx/typescript', + ]); + expect(e2eEslint.overrides[1].extends).toEqual([ + 'plugin:@nrwl/nx/javascript', + ]); + + runCLI(`generate @nrwl/angular:lib ${mylib}`); + // should add new tslint + expect(() => checkFilesExist(`.eslintrc.base.json`)).not.toThrow(); + const appEslint = readJson(`.eslintrc.json`); + rootEslint = readJson('.eslintrc.base.json'); + e2eEslint = readJson('e2e/.eslintrc.json'); + const libEslint = readJson(`libs/${mylib}/.eslintrc.json`); + + // should directly refer to nx plugin only in the root + expect(rootEslint.plugins).toEqual(['@nrwl/nx']); + expect(appEslint.plugins).toBeUndefined(); + expect(e2eEslint.plugins).toBeUndefined(); + // should extend framework plugin and root config + expect(appEslint.extends).toEqual(['./.eslintrc.base.json']); + expect(e2eEslint.extends).toEqual([ + 'plugin:cypress/recommended', + '../.eslintrc.base.json', + ]); + expect(libEslint.extends).toEqual(['../../.eslintrc.base.json']); + // should have no plugin extends + expect(appEslint.overrides[0].extends).toEqual([ + 'plugin:@nrwl/nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ]); + expect(e2eEslint.overrides[0].extends).toBeUndefined(); + expect(e2eEslint.overrides[1].extends).toBeUndefined(); + expect(libEslint.overrides[0].extends).toEqual([ + 'plugin:@nrwl/nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ]); + }); }); }); diff --git a/packages/angular/src/generators/add-linting/add-linting.spec.ts b/packages/angular/src/generators/add-linting/add-linting.spec.ts index 7c47060dbb902..fe1670c1ee994 100644 --- a/packages/angular/src/generators/add-linting/add-linting.spec.ts +++ b/packages/angular/src/generators/add-linting/add-linting.spec.ts @@ -25,8 +25,8 @@ describe('addLinting generator', () => { } as ProjectConfiguration); }); - it('should invoke the lint init generator', async () => { - jest.spyOn(linter, 'lintInitGenerator'); + it('should invoke the lintProjectGenerator', async () => { + jest.spyOn(linter, 'lintProjectGenerator'); await addLintingGenerator(tree, { prefix: 'myOrg', @@ -34,7 +34,7 @@ describe('addLinting generator', () => { projectRoot: appProjectRoot, }); - expect(linter.lintInitGenerator).toHaveBeenCalled(); + expect(linter.lintProjectGenerator).toHaveBeenCalled(); }); it('should add the Angular specific EsLint devDependencies', async () => { @@ -79,18 +79,7 @@ describe('addLinting generator', () => { `${appProjectRoot}/**/*.html`, ], }, + outputs: ['{options.outputFile}'], }); }); - - it('should format files', async () => { - jest.spyOn(devkit, 'formatFiles'); - - await addLintingGenerator(tree, { - prefix: 'myOrg', - projectName: appProjectName, - projectRoot: appProjectRoot, - }); - - expect(devkit.formatFiles).toHaveBeenCalled(); - }); }); diff --git a/packages/angular/src/generators/add-linting/add-linting.ts b/packages/angular/src/generators/add-linting/add-linting.ts index f368e14acea1c..6534242d2779b 100755 --- a/packages/angular/src/generators/add-linting/add-linting.ts +++ b/packages/angular/src/generators/add-linting/add-linting.ts @@ -1,33 +1,51 @@ -import type { GeneratorCallback, Tree } from '@nrwl/devkit'; -import { formatFiles } from '@nrwl/devkit'; -import { Linter, lintInitGenerator } from '@nrwl/linter'; +import { + GeneratorCallback, + joinPathFragments, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { Linter, lintProjectGenerator } from '@nrwl/linter'; +import { mapLintPattern } from '@nrwl/linter/src/generators/lint-project/lint-project'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies'; -import { addProjectLintTarget } from './lib/add-project-lint-target'; -import { createEsLintConfiguration } from './lib/create-eslint-configuration'; +import { extendAngularEslintJson } from './lib/create-eslint-configuration'; import type { AddLintingGeneratorSchema } from './schema'; export async function addLintingGenerator( tree: Tree, options: AddLintingGeneratorSchema ): Promise { - const installTask = lintInitGenerator(tree, { + const tasks: GeneratorCallback[] = []; + const rootProject = options.projectRoot === '.' || options.projectRoot === ''; + const lintTask = await lintProjectGenerator(tree, { linter: Linter.EsLint, + project: options.projectName, + tsConfigPaths: [ + joinPathFragments(options.projectRoot, 'tsconfig.app.json'), + ], unitTestRunner: options.unitTestRunner, - skipPackageJson: options.skipPackageJson, + eslintFilePatterns: [ + mapLintPattern(options.projectRoot, 'ts', rootProject), + mapLintPattern(options.projectRoot, 'html', rootProject), + ], + setParserOptionsProject: options.setParserOptionsProject, + skipFormat: true, + rootProject: rootProject, }); + tasks.push(lintTask); - if (!options.skipPackageJson) { - addAngularEsLintDependencies(tree); - } + updateJson( + tree, + joinPathFragments(options.projectRoot, '.eslintrc.json'), + (json) => extendAngularEslintJson(json, options) + ); - createEsLintConfiguration(tree, options); - addProjectLintTarget(tree, options); - - if (!options.skipFormat) { - await formatFiles(tree); + if (!options.skipPackageJson) { + const installTask = await addAngularEsLintDependencies(tree); + tasks.push(installTask); } - return installTask; + return runTasksInSerial(...tasks); } export default addLintingGenerator; diff --git a/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts b/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts index 526c04aca4b70..b2393bec98a6d 100644 --- a/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts +++ b/packages/angular/src/generators/add-linting/lib/add-angular-eslint-dependencies.ts @@ -1,9 +1,9 @@ -import type { Tree } from '@nrwl/devkit'; +import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { addDependenciesToPackageJson } from '@nrwl/devkit'; import { angularEslintVersion } from '../../../utils/versions'; -export function addAngularEsLintDependencies(tree: Tree): void { - addDependenciesToPackageJson( +export function addAngularEsLintDependencies(tree: Tree): GeneratorCallback { + return addDependenciesToPackageJson( tree, {}, { diff --git a/packages/angular/src/generators/add-linting/lib/add-project-lint-target.ts b/packages/angular/src/generators/add-linting/lib/add-project-lint-target.ts index 19e0ec52138f7..d8efac04b5dbd 100644 --- a/packages/angular/src/generators/add-linting/lib/add-project-lint-target.ts +++ b/packages/angular/src/generators/add-linting/lib/add-project-lint-target.ts @@ -3,6 +3,7 @@ import { readProjectConfiguration, updateProjectConfiguration, } from '@nrwl/devkit'; +import { mapLintPattern } from '@nrwl/linter/src/generators/lint-project/lint-project'; import type { AddLintingGeneratorSchema } from '../schema'; export function addProjectLintTarget( @@ -10,12 +11,13 @@ export function addProjectLintTarget( options: AddLintingGeneratorSchema ): void { const project = readProjectConfiguration(tree, options.projectName); + const rootProject = options.projectRoot === '.' || options.projectRoot === ''; project.targets.lint = { executor: '@nrwl/linter:eslint', options: { lintFilePatterns: [ - `${options.projectRoot}/**/*.ts`, - `${options.projectRoot}/**/*.html`, + mapLintPattern(options.projectRoot, 'ts', rootProject), + mapLintPattern(options.projectRoot, 'html', rootProject), ], }, }; diff --git a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts index 83739df838b4f..36745b34fe963 100644 --- a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts +++ b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts @@ -1,8 +1,65 @@ import type { Tree } from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, writeJson } from '@nrwl/devkit'; import { camelize, dasherize } from '@nrwl/workspace/src/utils/strings'; +import { Linter } from 'eslint'; import type { AddLintingGeneratorSchema } from '../schema'; +type EslintExtensionSchema = { + prefix: string; +}; + +export const extendAngularEslintJson = ( + json: Linter.Config, + options: EslintExtensionSchema +) => { + const overrides = [ + { + ...json.overrides[0], + files: ['*.ts'], + extends: [ + ...(json.overrides[0].extends || []), + 'plugin:@nrwl/nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: camelize(options.prefix), + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: dasherize(options.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: {}, + }, + ]; + + return { + ...json, + overrides, + }; +}; + +/** + * @deprecated Use {@link extendAngularEslintJson} instead + */ export function createEsLintConfiguration( tree: Tree, options: AddLintingGeneratorSchema diff --git a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap index 13042e4ab1f0a..88db8145f4d12 100644 --- a/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap +++ b/packages/angular/src/generators/application/__snapshots__/application.spec.ts.snap @@ -191,6 +191,9 @@ Object { "apps/my-dir/my-app/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], }, "serve": Object { "builder": "@angular-devkit/build-angular:dev-server", @@ -359,6 +362,9 @@ Object { "apps/my-app/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], }, "serve": Object { "builder": "@angular-devkit/build-angular:dev-server", diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index 488c0217aeb3a..2ce99a96cc095 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -544,6 +544,9 @@ describe('app', () => { "apps/my-app/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], } `); expect(workspaceJson.projects['my-app-e2e'].architect.lint) @@ -578,6 +581,9 @@ describe('app', () => { "apps/my-app/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], } `); expect(appTree.exists('apps/my-app-e2e/.eslintrc.json')).toBeTruthy(); diff --git a/packages/angular/src/generators/application/application.ts b/packages/angular/src/generators/application/application.ts index aad3f1a2d7c5e..4c1d70a0c8c24 100644 --- a/packages/angular/src/generators/application/application.ts +++ b/packages/angular/src/generators/application/application.ts @@ -104,7 +104,7 @@ export async function applicationGenerator( addRouterRootConfiguration(host, options); } - addLinting(host, options); + await addLinting(host, options); await addUnitTestRunner(host, options); await addE2e(host, options); updateEditorTsConfig(host, options); diff --git a/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap b/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap index ec2d0aa37a4bd..88a46d8d249b5 100644 --- a/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap +++ b/packages/angular/src/generators/convert-tslint-to-eslint/__snapshots__/convert-tslint-to-eslint.spec.ts.snap @@ -624,6 +624,9 @@ Object { "apps/angular-app-1/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], }, }, } @@ -993,6 +996,9 @@ Object { "libs/angular-lib-1/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], }, }, } diff --git a/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap index fa56813cb56f3..68f16a720736b 100644 --- a/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/angular/src/generators/library/__snapshots__/library.spec.ts.snap @@ -351,3 +351,24 @@ import { myLibRoutes } from '@proj/my-lib'; export const appRoutes: Route[] = [ { path: 'my-lib', children: myLibRoutes },]" `; + +exports[`lib --standalone should generate a library with a standalone component as entry point with routing setup and attach it to standalone parent routes as a lazy child 1`] = ` +"import { Route } from '@angular/router'; + import { MyLibComponent } from './my-lib/my-lib.component'; + + export const myLibRoutes: Route[] = [ + {path: 'second', loadChildren: () => import('@proj/second').then(m => m.secondRoutes)}, + {path: '', component: MyLibComponent} + ]" +`; + +exports[`lib --standalone should generate a library with a standalone component as entry point with routing setup and attach it to standalone parent routes as direct child 1`] = ` +"import { Route } from '@angular/router'; + import { MyLibComponent } from './my-lib/my-lib.component'; +import { secondRoutes } from '@proj/second'; + + export const myLibRoutes: Route[] = [ + { path: 'second', children: secondRoutes }, + {path: '', component: MyLibComponent} + ]" +`; diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index ea0241d0cbc04..6f2b5f0a68b5a 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -1307,6 +1307,9 @@ describe('lib', () => { "libs/my-lib/**/*.html", ], }, + "outputs": Array [ + "{options.outputFile}", + ], } `); }); @@ -1661,17 +1664,9 @@ describe('lib', () => { }); // ASSERT - expect(tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { Route } from '@angular/router'; - import { MyLibComponent } from './my-lib/my-lib.component'; - import { secondRoutes } from '@proj/second'; - - export const myLibRoutes: Route[] = [ - { path: 'second', children: secondRoutes }, - {path: '', component: MyLibComponent} - ]" - `); + expect( + tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8') + ).toMatchSnapshot(); }); it('should generate a library with a standalone component as entry point with routing setup and attach it to standalone parent routes as a lazy child', async () => { @@ -1691,16 +1686,9 @@ describe('lib', () => { }); // ASSERT - expect(tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')) - .toMatchInlineSnapshot(` - "import { Route } from '@angular/router'; - import { MyLibComponent } from './my-lib/my-lib.component'; - - export const myLibRoutes: Route[] = [ - {path: 'second', loadChildren: () => import('@proj/second').then(m => m.secondRoutes)}, - {path: '', component: MyLibComponent} - ]" - `); + expect( + tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8') + ).toMatchSnapshot(); }); it('should generate a library with a standalone component as entry point following SFC pattern', async () => { From 8aaf87dd18e6d73486531abdd48e1b68d1c02582 Mon Sep 17 00:00:00 2001 From: Miroslav Jonas Date: Thu, 1 Dec 2022 13:46:16 +0100 Subject: [PATCH 2/2] chore(angular): fix wrong eslint import method --- .../generators/add-linting/lib/create-eslint-configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts index 36745b34fe963..e7c2060175831 100644 --- a/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts +++ b/packages/angular/src/generators/add-linting/lib/create-eslint-configuration.ts @@ -1,7 +1,7 @@ import type { Tree } from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, writeJson } from '@nrwl/devkit'; import { camelize, dasherize } from '@nrwl/workspace/src/utils/strings'; -import { Linter } from 'eslint'; +import type { Linter } from 'eslint'; import type { AddLintingGeneratorSchema } from '../schema'; type EslintExtensionSchema = {