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

feat(angular): support migrating angular cli workspaces with multiple projects when keeping the angular cli layout #9649

Merged
merged 1 commit into from Apr 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
52 changes: 46 additions & 6 deletions e2e/angular-core/src/ng-add.test.ts
Expand Up @@ -387,14 +387,54 @@ describe('convert Angular CLI workspace to an Nx workspace', () => {
});
});

it('should support preserveAngularCliLayout', () => {
it('should support --preserve-angular-cli-layout', () => {
// add another app and a library
runCommand(`ng g @schematics/angular:application app2`);
runCommand(`ng g @schematics/angular:library lib1`);

runNgAdd('@nrwl/angular', '--preserve-angular-cli-layout');

const updatedAngularCLIJson = readJson('angular.json');
expect(updatedAngularCLIJson.projects[project].root).toEqual('');
expect(updatedAngularCLIJson.projects[project].sourceRoot).toEqual('src');
// check config still uses Angular CLI layout
const updatedAngularJson = readJson('angular.json');
expect(updatedAngularJson.projects[project].root).toEqual('');
expect(updatedAngularJson.projects[project].sourceRoot).toEqual('src');
expect(updatedAngularJson.projects.app2.root).toEqual('projects/app2');
expect(updatedAngularJson.projects.app2.sourceRoot).toEqual(
'projects/app2/src'
);
expect(updatedAngularJson.projects.lib1.root).toEqual('projects/lib1');
expect(updatedAngularJson.projects.lib1.sourceRoot).toEqual(
'projects/lib1/src'
);

// check building an app
let output = runCLI(`build ${project} --outputHashing none`);
expect(output).toContain(
`> nx run ${project}:build:production --outputHashing=none`
);
expect(output).toContain(
`Successfully ran target build for project ${project}`
);
checkFilesExist(`dist/${project}/main.js`);

const output = runCLI('build');
expect(output).toContain(`> nx run ${project}:build:production`);
output = runCLI(`build ${project} --outputHashing none`);
expect(output).toContain(
`> nx run ${project}:build:production --outputHashing=none [existing outputs match the cache, left as is]`
);
expect(output).toContain(
`Successfully ran target build for project ${project}`
);

// check building lib1
output = runCLI('build lib1');
expect(output).toContain('> nx run lib1:build:production');
expect(output).toContain('Successfully ran target build for project lib1');
checkFilesExist('dist/lib1/package.json');

output = runCLI('build lib1');
expect(output).toContain(
'> nx run lib1:build:production [existing outputs match the cache, left as is]'
);
expect(output).toContain('Successfully ran target build for project lib1');
});
});
@@ -1,5 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`workspace --preserve-angular-cli-layout should create nx.json 1`] = `
Object {
"affected": Object {
"defaultBase": "main",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
},
"npmScope": "my-scope",
"targetDependencies": Object {
"build": Array [
Object {
"projects": "dependencies",
"target": "build",
},
],
},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "nx/tasks-runners/default",
},
},
"workspaceLayout": Object {
"appsDir": "",
"libsDir": "",
},
}
`;

exports[`workspace --preserve-angular-cli-layout should support multiple projects 1`] = `
Object {
"affected": Object {
"defaultBase": "main",
},
"implicitDependencies": Object {
".eslintrc.json": "*",
"package.json": Object {
"dependencies": "*",
"devDependencies": "*",
},
},
"npmScope": "my-scope",
"targetDependencies": Object {
"build": Array [
Object {
"projects": "dependencies",
"target": "build",
},
],
},
"tasksRunnerOptions": Object {
"default": Object {
"options": Object {
"cacheableOperations": Array [
"build",
"lint",
"test",
"e2e",
],
},
"runner": "nx/tasks-runners/default",
},
},
"workspaceLayout": Object {
"appsDir": "projects",
"libsDir": "projects",
},
}
`;

exports[`workspace move to nx layout cypress should handle project configuration without cypress-run or cypress-open 1`] = `
Object {
"implicitDependencies": Array [
Expand Down
Expand Up @@ -632,41 +632,102 @@ describe('workspace', () => {
});
});

describe('preserve angular cli layout', () => {
describe('--preserve-angular-cli-layout', () => {
beforeEach(() => {
tree.write('/package.json', JSON.stringify({ devDependencies: {} }));
tree.write('/angular.json', JSON.stringify({ projects: { myproj: {} } }));
tree.write('/tsconfig.json', '{"compilerOptions": {}}');
tree.write(
'package.json',
JSON.stringify({ name: 'my-scope', devDependencies: {} })
);
tree.write('angular.json', JSON.stringify({ projects: { myproj: {} } }));
tree.write('tsconfig.json', '{"compilerOptions": {}}');
});

it('should update package.json', async () => {
await migrateFromAngularCli(tree, {
preserveAngularCliLayout: true,
});
await migrateFromAngularCli(tree, { preserveAngularCliLayout: true });

const d = readJson(tree, '/package.json').devDependencies;
expect(d['@nrwl/workspace']).toBeDefined();
expect(d['@nrwl/angular']).not.toBeDefined();
const { devDependencies } = readJson(tree, 'package.json');
expect(devDependencies['@nrwl/workspace']).toBeDefined();
expect(devDependencies['nx']).toBeDefined();
});

it('should create nx.json', async () => {
await migrateFromAngularCli(tree, {
preserveAngularCliLayout: true,
});
await migrateFromAngularCli(tree, { preserveAngularCliLayout: true });

const nxJson = readJson(tree, '/nx.json');
expect(nxJson.npmScope).toEqual('myproj');
expect(nxJson.workspaceLayout).toBeTruthy();
expect(readJson(tree, 'nx.json')).toMatchSnapshot();
});

it('should create decorate-angular-cli.js', async () => {
await migrateFromAngularCli(tree, {
preserveAngularCliLayout: true,
});
const s = readJson(tree, '/package.json').scripts;
await migrateFromAngularCli(tree, { preserveAngularCliLayout: true });

expect(tree.exists('/decorate-angular-cli.js')).toBe(true);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe('node ./decorate-angular-cli.js');
});

it('should support multiple projects', async () => {
const angularJson = {
$schema: './node_modules/@angular/cli/lib/config/schema.json',
version: 1,
defaultProject: 'app1',
newProjectRoot: 'projects',
projects: {
app1: {
root: '',
sourceRoot: 'src',
architect: {
build: {
options: { tsConfig: 'tsconfig.app.json' },
},
test: {
options: { tsConfig: 'tsconfig.spec.json' },
},
e2e: {
builder: '@angular-devkit/build-angular:protractor',
options: { protractorConfig: 'e2e/protractor.conf.js' },
},
},
},
app2: {
root: 'projects/app2',
sourceRoot: 'projects/app2/src',
architect: {
build: {
options: { tsConfig: 'projects/app2/tsconfig.app.json' },
},
test: {
options: { tsConfig: 'projects/app2/tsconfig.spec.json' },
},
e2e: {
builder: '@angular-devkit/build-angular:protractor',
options: {
protractorConfig: 'projects/app2/e2e/protractor.conf.js',
},
},
},
},
lib1: {
root: 'projects/lib1',
sourceRoot: 'projects/lib1/src',
architect: {
build: {
options: { tsConfig: 'projects/lib1/tsconfig.lib.json' },
},
test: {
options: { tsConfig: 'projects/lib1/tsconfig.spec.json' },
},
},
},
},
};
tree.write('/angular.json', JSON.stringify(angularJson));

await migrateFromAngularCli(tree, { preserveAngularCliLayout: true });

expect(tree.read('/decorate-angular-cli.js')).not.toBe(null);
expect(s.postinstall).toEqual('node ./decorate-angular-cli.js');
expect(readJson(tree, 'angular.json')).toStrictEqual(angularJson);
expect(tree.exists('/decorate-angular-cli.js')).toBe(true);
const { scripts } = readJson(tree, 'package.json');
expect(scripts.postinstall).toBe('node ./decorate-angular-cli.js');
expect(readJson(tree, 'nx.json')).toMatchSnapshot();
});
});
});
Expand Up @@ -28,7 +28,7 @@ export async function migrateFromAngularCli(
rawOptions: GeneratorOptions
) {
const projects = getAllProjects(tree);
const options = normalizeOptions(rawOptions, projects);
const options = normalizeOptions(tree, rawOptions, projects);

if (options.preserveAngularCliLayout) {
addDependenciesToPackageJson(
Expand Down
@@ -1,24 +1,33 @@
import { names } from '@nrwl/devkit';
import { joinPathFragments, names, readJson, Tree } from '@nrwl/devkit';
import { GeneratorOptions } from '../schema';
import { WorkspaceProjects } from './types';

export function normalizeOptions(
tree: Tree,
options: GeneratorOptions,
projects: WorkspaceProjects
): GeneratorOptions {
// TODO: this restrictions will be removed, it's here temporarily to
// execute for both a full migration and a minimal one to maintain
// the current behavior
const hasLibraries = projects.libs.length > 0;
if (projects.apps.length > 2 || hasLibraries) {
throw new Error('Can only convert projects with one app');
}

let npmScope = options.npmScope ?? options.name;
if (npmScope) {
npmScope = names(npmScope).fileName;
} else {
npmScope = projects.apps[0].name;
} else if (projects.libs.length > 0) {
// try get the scope from any library that have one
for (const lib of projects.libs) {
const { name } = readJson(
tree,
joinPathFragments(lib.config.root, 'package.json')
);
if (name.startsWith('@')) {
npmScope = name.split('/')[0].substring(1);
break;
}
}
}

if (!npmScope) {
// use the name (scope if exists) in the root package.json
const { name } = readJson(tree, 'package.json');
npmScope = name.startsWith('@') ? name.split('/')[0].substring(1) : name;
}

return { ...options, npmScope };
Expand Down
20 changes: 7 additions & 13 deletions packages/angular/src/generators/ng-add/utilities/workspace.ts
Expand Up @@ -43,6 +43,12 @@ export function validateWorkspace(
throw new Error('Cannot find angular.json');
}

// TODO: this restrictions will be removed when support for multiple
// projects is added
if (projects.apps.length > 2 || projects.libs.length > 0) {
throw new Error('Can only convert projects with one app');
}

const e2eKey = getE2eKey(projects);
const e2eApp = getE2eProject(projects);

Expand Down Expand Up @@ -97,19 +103,7 @@ export function createNxJson(
options: GeneratorOptions,
setWorkspaceLayoutAsNewProjectRoot: boolean = false
): void {
const { projects = {}, newProjectRoot = '' } = readJson(tree, 'angular.json');
// TODO: temporarily leaving this here because it's the old behavior for a
// minimal migration, will be removed in a later PR
const hasLibraries = Object.keys(projects).find(
(project) =>
projects[project].projectType &&
projects[project].projectType !== 'application'
);
if (Object.keys(projects).length !== 1 || hasLibraries) {
throw new Error(
`The schematic can only be used with Angular CLI workspaces with a single application.`
);
}
const { newProjectRoot = '' } = readJson(tree, 'angular.json');

writeJson<NxJsonConfiguration>(tree, 'nx.json', {
npmScope: options.npmScope,
Expand Down