fix(angular): fix root-project support for Angular (#13534)

This commit is contained in:
Miroslav Jonaš 2022-12-01 15:14:08 +01:00 committed by GitHub
parent 92df716b66
commit 22e70d614e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 222 additions and 59 deletions

View File

@ -442,7 +442,7 @@ export function tslibC(): string {
describe('Root projects migration', () => { describe('Root projects migration', () => {
afterEach(() => cleanupProject()); 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 myapp = uniq('myapp');
const mylib = uniq('mylib'); const mylib = uniq('mylib');
@ -506,6 +506,76 @@ export function tslibC(): string {
expect(libEslint.overrides[1].extends).toBeUndefined(); expect(libEslint.overrides[1].extends).toBeUndefined();
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',
]);
});
}); });
}); });

View File

@ -25,8 +25,8 @@ describe('addLinting generator', () => {
} as ProjectConfiguration); } as ProjectConfiguration);
}); });
it('should invoke the lint init generator', async () => { it('should invoke the lintProjectGenerator', async () => {
jest.spyOn(linter, 'lintInitGenerator'); jest.spyOn(linter, 'lintProjectGenerator');
await addLintingGenerator(tree, { await addLintingGenerator(tree, {
prefix: 'myOrg', prefix: 'myOrg',
@ -34,7 +34,7 @@ describe('addLinting generator', () => {
projectRoot: appProjectRoot, projectRoot: appProjectRoot,
}); });
expect(linter.lintInitGenerator).toHaveBeenCalled(); expect(linter.lintProjectGenerator).toHaveBeenCalled();
}); });
it('should add the Angular specific EsLint devDependencies', async () => { it('should add the Angular specific EsLint devDependencies', async () => {
@ -79,18 +79,7 @@ describe('addLinting generator', () => {
`${appProjectRoot}/**/*.html`, `${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();
});
}); });

View File

@ -1,33 +1,51 @@
import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import {
import { formatFiles } from '@nrwl/devkit'; GeneratorCallback,
import { Linter, lintInitGenerator } from '@nrwl/linter'; 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 { addAngularEsLintDependencies } from './lib/add-angular-eslint-dependencies';
import { addProjectLintTarget } from './lib/add-project-lint-target'; import { extendAngularEslintJson } from './lib/create-eslint-configuration';
import { createEsLintConfiguration } from './lib/create-eslint-configuration';
import type { AddLintingGeneratorSchema } from './schema'; import type { AddLintingGeneratorSchema } from './schema';
export async function addLintingGenerator( export async function addLintingGenerator(
tree: Tree, tree: Tree,
options: AddLintingGeneratorSchema options: AddLintingGeneratorSchema
): Promise<GeneratorCallback> { ): Promise<GeneratorCallback> {
const installTask = lintInitGenerator(tree, { const tasks: GeneratorCallback[] = [];
const rootProject = options.projectRoot === '.' || options.projectRoot === '';
const lintTask = await lintProjectGenerator(tree, {
linter: Linter.EsLint, linter: Linter.EsLint,
project: options.projectName,
tsConfigPaths: [
joinPathFragments(options.projectRoot, 'tsconfig.app.json'),
],
unitTestRunner: options.unitTestRunner, 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);
updateJson(
tree,
joinPathFragments(options.projectRoot, '.eslintrc.json'),
(json) => extendAngularEslintJson(json, options)
);
if (!options.skipPackageJson) { if (!options.skipPackageJson) {
addAngularEsLintDependencies(tree); const installTask = await addAngularEsLintDependencies(tree);
tasks.push(installTask);
} }
createEsLintConfiguration(tree, options); return runTasksInSerial(...tasks);
addProjectLintTarget(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);
}
return installTask;
} }
export default addLintingGenerator; export default addLintingGenerator;

View File

@ -1,9 +1,9 @@
import type { Tree } from '@nrwl/devkit'; import type { GeneratorCallback, Tree } from '@nrwl/devkit';
import { addDependenciesToPackageJson } from '@nrwl/devkit'; import { addDependenciesToPackageJson } from '@nrwl/devkit';
import { angularEslintVersion } from '../../../utils/versions'; import { angularEslintVersion } from '../../../utils/versions';
export function addAngularEsLintDependencies(tree: Tree): void { export function addAngularEsLintDependencies(tree: Tree): GeneratorCallback {
addDependenciesToPackageJson( return addDependenciesToPackageJson(
tree, tree,
{}, {},
{ {

View File

@ -3,6 +3,7 @@ import {
readProjectConfiguration, readProjectConfiguration,
updateProjectConfiguration, updateProjectConfiguration,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { mapLintPattern } from '@nrwl/linter/src/generators/lint-project/lint-project';
import type { AddLintingGeneratorSchema } from '../schema'; import type { AddLintingGeneratorSchema } from '../schema';
export function addProjectLintTarget( export function addProjectLintTarget(
@ -10,12 +11,13 @@ export function addProjectLintTarget(
options: AddLintingGeneratorSchema options: AddLintingGeneratorSchema
): void { ): void {
const project = readProjectConfiguration(tree, options.projectName); const project = readProjectConfiguration(tree, options.projectName);
const rootProject = options.projectRoot === '.' || options.projectRoot === '';
project.targets.lint = { project.targets.lint = {
executor: '@nrwl/linter:eslint', executor: '@nrwl/linter:eslint',
options: { options: {
lintFilePatterns: [ lintFilePatterns: [
`${options.projectRoot}/**/*.ts`, mapLintPattern(options.projectRoot, 'ts', rootProject),
`${options.projectRoot}/**/*.html`, mapLintPattern(options.projectRoot, 'html', rootProject),
], ],
}, },
}; };

View File

@ -1,8 +1,65 @@
import type { Tree } from '@nrwl/devkit'; import type { Tree } from '@nrwl/devkit';
import { joinPathFragments, offsetFromRoot, writeJson } from '@nrwl/devkit'; import { joinPathFragments, offsetFromRoot, writeJson } from '@nrwl/devkit';
import { camelize, dasherize } from '@nrwl/workspace/src/utils/strings'; import { camelize, dasherize } from '@nrwl/workspace/src/utils/strings';
import type { Linter } from 'eslint';
import type { AddLintingGeneratorSchema } from '../schema'; 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( export function createEsLintConfiguration(
tree: Tree, tree: Tree,
options: AddLintingGeneratorSchema options: AddLintingGeneratorSchema

View File

@ -191,6 +191,9 @@ Object {
"apps/my-dir/my-app/**/*.html", "apps/my-dir/my-app/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
}, },
"serve": Object { "serve": Object {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
@ -359,6 +362,9 @@ Object {
"apps/my-app/**/*.html", "apps/my-app/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
}, },
"serve": Object { "serve": Object {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",

View File

@ -544,6 +544,9 @@ describe('app', () => {
"apps/my-app/**/*.html", "apps/my-app/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
} }
`); `);
expect(workspaceJson.projects['my-app-e2e'].architect.lint) expect(workspaceJson.projects['my-app-e2e'].architect.lint)
@ -578,6 +581,9 @@ describe('app', () => {
"apps/my-app/**/*.html", "apps/my-app/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
} }
`); `);
expect(appTree.exists('apps/my-app-e2e/.eslintrc.json')).toBeTruthy(); expect(appTree.exists('apps/my-app-e2e/.eslintrc.json')).toBeTruthy();

View File

@ -104,7 +104,7 @@ export async function applicationGenerator(
addRouterRootConfiguration(host, options); addRouterRootConfiguration(host, options);
} }
addLinting(host, options); await addLinting(host, options);
await addUnitTestRunner(host, options); await addUnitTestRunner(host, options);
await addE2e(host, options); await addE2e(host, options);
updateEditorTsConfig(host, options); updateEditorTsConfig(host, options);

View File

@ -624,6 +624,9 @@ Object {
"apps/angular-app-1/**/*.html", "apps/angular-app-1/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
}, },
}, },
} }
@ -993,6 +996,9 @@ Object {
"libs/angular-lib-1/**/*.html", "libs/angular-lib-1/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
}, },
}, },
} }

View File

@ -351,3 +351,24 @@ import { myLibRoutes } from '@proj/my-lib';
export const appRoutes: Route[] = [ export const appRoutes: Route[] = [
{ path: 'my-lib', children: myLibRoutes },]" { 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}
]"
`;

View File

@ -1307,6 +1307,9 @@ describe('lib', () => {
"libs/my-lib/**/*.html", "libs/my-lib/**/*.html",
], ],
}, },
"outputs": Array [
"{options.outputFile}",
],
} }
`); `);
}); });
@ -1661,17 +1664,9 @@ describe('lib', () => {
}); });
// ASSERT // ASSERT
expect(tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')) expect(
.toMatchInlineSnapshot(` tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')
"import { Route } from '@angular/router'; ).toMatchSnapshot();
import { MyLibComponent } from './my-lib/my-lib.component';
import { secondRoutes } from '@proj/second';
export const myLibRoutes: Route[] = [
{ path: 'second', children: secondRoutes },
{path: '', component: MyLibComponent}
]"
`);
}); });
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 () => { 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 // ASSERT
expect(tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')) expect(
.toMatchInlineSnapshot(` tree.read('libs/my-lib/src/lib/lib.routes.ts', 'utf-8')
"import { Route } from '@angular/router'; ).toMatchSnapshot();
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}
]"
`);
}); });
it('should generate a library with a standalone component as entry point following SFC pattern', async () => { it('should generate a library with a standalone component as entry point following SFC pattern', async () => {