Skip to content
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

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

Merged
merged 2 commits into from Dec 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
72 changes: 71 additions & 1 deletion e2e/linter/src/linter.test.ts
Expand Up @@ -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');

Expand Down Expand Up @@ -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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meeroslav Standalone means something different in angular. (Standalone components and APIs etc) This would probably be better titled something like Angular root-project

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a test in the linter e2e and matches our preset name so I would leave this as is, but I'll rename the PR.

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',
]);
});
});
});

Expand Down
19 changes: 4 additions & 15 deletions packages/angular/src/generators/add-linting/add-linting.spec.ts
Expand Up @@ -25,16 +25,16 @@ 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',
projectName: appProjectName,
projectRoot: appProjectRoot,
});

expect(linter.lintInitGenerator).toHaveBeenCalled();
expect(linter.lintProjectGenerator).toHaveBeenCalled();
});

it('should add the Angular specific EsLint devDependencies', async () => {
Expand Down Expand Up @@ -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();
});
});
50 changes: 34 additions & 16 deletions 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<GeneratorCallback> {
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;
@@ -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,
{},
{
Expand Down
Expand Up @@ -3,19 +3,21 @@ 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(
tree: Tree,
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),
],
},
};
Expand Down
@@ -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 type { 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
Expand Down
Expand Up @@ -191,6 +191,9 @@ Object {
"apps/my-dir/my-app/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
"serve": Object {
"builder": "@angular-devkit/build-angular:dev-server",
Expand Down Expand Up @@ -359,6 +362,9 @@ Object {
"apps/my-app/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
"serve": Object {
"builder": "@angular-devkit/build-angular:dev-server",
Expand Down
Expand Up @@ -544,6 +544,9 @@ describe('app', () => {
"apps/my-app/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
}
`);
expect(workspaceJson.projects['my-app-e2e'].architect.lint)
Expand Down Expand Up @@ -578,6 +581,9 @@ describe('app', () => {
"apps/my-app/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
}
`);
expect(appTree.exists('apps/my-app-e2e/.eslintrc.json')).toBeTruthy();
Expand Down
Expand Up @@ -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);
Expand Down
Expand Up @@ -624,6 +624,9 @@ Object {
"apps/angular-app-1/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
},
}
Expand Down Expand Up @@ -993,6 +996,9 @@ Object {
"libs/angular-lib-1/**/*.html",
],
},
"outputs": Array [
"{options.outputFile}",
],
},
},
}
Expand Down
Expand Up @@ -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}
]"
`;