feat(angular): add preserveAngularCLILayout option to ng-add

This commit is contained in:
Victor Savkin 2020-05-28 16:14:53 -04:00 committed by Victor Savkin
parent 690be207be
commit 48d953e4d7
5 changed files with 305 additions and 223 deletions

View File

@ -293,22 +293,6 @@ forEachCli('angular', () => {
]);
});
// TODO(FrozenPandaz): reenable after angular 9
xit('should convert a project with common libraries in the ecosystem', () => {
// create a new AngularCLI app
runNew();
// Add some Angular libraries
runNgAdd('add @angular/elements');
runNgAdd('add @angular/material');
runNgAdd('add @angular/pwa');
runNgAdd('add @ngrx/store');
runNgAdd('add @ngrx/effects');
// Add Nx
runNgAdd('add @nrwl/workspace --skip-install');
});
it('should handle different types of errors', () => {
// create a new AngularCLI app
runNew();
@ -353,6 +337,18 @@ forEachCli('angular', () => {
// Put src back
runCommand('mv src-bak src');
});
it('should support preserveAngularCLILayout', () => {
runNew('', false, false);
runNgAdd('add @nrwl/workspace --preserveAngularCLILayout');
const updatedAngularCLIJson = readJson('angular.json');
expect(updatedAngularCLIJson.projects.proj.root).toEqual('');
expect(updatedAngularCLIJson.projects.proj.sourceRoot).toEqual('src');
const output = runCLI('build');
expect(output).toContain(`> ng run proj:build`);
});
});
});

View File

@ -9,222 +9,258 @@ describe('workspace', () => {
appTree = new UnitTestTree(Tree.empty());
});
it('should error if no package.json is present', async () => {
try {
await runSchematic('ng-add', { name: 'myApp' }, appTree);
fail('should throw');
} catch (e) {
expect(e.message).toContain('Cannot find package.json');
}
});
describe('move to nx layout', () => {
it('should error if no package.json is present', async () => {
try {
await runSchematic('ng-add', { name: 'myApp' }, appTree);
fail('should throw');
} catch (e) {
expect(e.message).toContain('Cannot find package.json');
}
});
it('should error if no e2e/protractor.conf.js is present', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
projects: {
proj1: {
architect: {
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
it('should error if no e2e/protractor.conf.js is present', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
projects: {
proj1: {
architect: {
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
},
},
},
},
},
},
})
);
try {
await runSchematic('ng-add', { name: 'proj1' }, appTree);
} catch (e) {
expect(e.message).toContain(
'An e2e project was specified but e2e/protractor.conf.js could not be found.'
})
);
}
});
it('should error if no angular.json is present', async () => {
try {
try {
await runSchematic('ng-add', { name: 'proj1' }, appTree);
} catch (e) {
expect(e.message).toContain(
'An e2e project was specified but e2e/protractor.conf.js could not be found.'
);
}
});
it('should error if no angular.json is present', async () => {
try {
appTree.create('/package.json', JSON.stringify({}));
appTree.create('/e2e/protractor.conf.js', '');
await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) {
expect(e.message).toContain('Cannot find angular.json');
}
});
it('should error if the angular.json specifies more than one app', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create('/e2e/protractor.conf.js', '');
await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) {
expect(e.message).toContain('Cannot find angular.json');
}
});
appTree.create(
'/angular.json',
JSON.stringify({
projects: {
proj1: {},
'proj1-e2e': {},
proj2: {},
'proj2-e2e': {},
},
})
);
try {
await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) {
expect(e.message).toContain('Can only convert projects with one app');
}
});
it('should error if the angular.json specifies more than one app', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create('/e2e/protractor.conf.js', '');
appTree.create(
'/angular.json',
JSON.stringify({
projects: {
proj1: {},
'proj1-e2e': {},
proj2: {},
'proj2-e2e': {},
},
})
);
try {
await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) {
expect(e.message).toContain('Can only convert projects with one app');
}
});
it('should work without nested tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
root: '',
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'tsconfig.app.json',
it('should work without nested tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
root: '',
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'tsconfig.app.json',
},
configurations: {},
},
configurations: {},
},
test: {
options: {
tsConfig: 'tsconfig.spec.json',
test: {
options: {
tsConfig: 'tsconfig.spec.json',
},
},
},
lint: {
options: {
tsConfig: 'tsconfig.app.json',
lint: {
options: {
tsConfig: 'tsconfig.app.json',
},
},
},
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
},
},
},
},
},
},
})
);
appTree.create(
'/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
});
})
);
appTree.create(
'/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
});
it('should work with nested (sub-dir) tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'src/tsconfig.app.json',
it('should work with nested (sub-dir) tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'src/tsconfig.app.json',
},
configurations: {},
},
configurations: {},
},
test: {
options: {
tsConfig: 'src/tsconfig.spec.json',
test: {
options: {
tsConfig: 'src/tsconfig.spec.json',
},
},
},
lint: {
options: {
tsConfig: 'src/tsconfig.app.json',
lint: {
options: {
tsConfig: 'src/tsconfig.app.json',
},
},
},
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
e2e: {
options: {
protractorConfig: 'e2e/protractor.conf.js',
},
},
},
},
},
},
})
);
appTree.create(
'/src/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/src/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
});
})
);
appTree.create(
'/src/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/src/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
});
it('should work with missing e2e, lint, or test targets', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
root: '',
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'tsconfig.app.json',
it('should work with missing e2e, lint, or test targets', async () => {
appTree.create('/package.json', JSON.stringify({}));
appTree.create(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
projects: {
myApp: {
root: '',
sourceRoot: 'src',
architect: {
build: {
options: {
tsConfig: 'tsconfig.app.json',
},
configurations: {},
},
configurations: {},
},
},
},
},
})
);
appTree.create(
'/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
appTree.create('/karma.conf.js', '// content');
})
);
appTree.create(
'/tsconfig.app.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create(
'/tsconfig.spec.json',
'{"extends": "../tsconfig.json", "compilerOptions": {}}'
);
appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/src/app/app.module.ts', '// content');
appTree.create('/karma.conf.js', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
expect(tree.exists('/apps/myApp/karma.conf.js')).toBe(true);
expect(tree.exists('/karma.conf.js')).toBe(true);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
expect(tree.exists('/apps/myApp/karma.conf.js')).toBe(true);
expect(tree.exists('/karma.conf.js')).toBe(true);
});
});
describe('preserve angular cli layout', () => {
beforeEach(() => {
appTree.create('/package.json', JSON.stringify({ devDependencies: {} }));
appTree.create(
'/angular.json',
JSON.stringify({ projects: { myproj: {} } })
);
});
it('should update package.json', async () => {
const tree = await runSchematic(
'ng-add',
{ preserveAngularCLILayout: true },
appTree
);
const d = JSON.parse(tree.readContent('/package.json')).devDependencies;
expect(d['@nrwl/workspace']).toBeDefined();
expect(d['@nrwl/angular']).not.toBeDefined();
});
it('should create nx.json', async () => {
const tree = await runSchematic(
'ng-add',
{ preserveAngularCLILayout: true },
appTree
);
const nxJson = JSON.parse(tree.readContent('/nx.json'));
expect(nxJson.projects).toEqual({ myproj: { tags: [] } });
expect(nxJson.npmScope).toEqual('myproj');
});
});
});

View File

@ -29,6 +29,7 @@ import {
renameSyncInTree,
renameDirSyncInTree,
addInstallTask,
addDepsToPackageJson,
} from '@nrwl/workspace';
import { DEFAULT_NRWL_PRETTIER_CONFIG } from '../workspace/workspace';
import { JsonArray } from '@angular-devkit/core';
@ -557,27 +558,70 @@ function checkCanConvertToWorkspace(options: Schema) {
};
}
const createNxJson = (host: Tree) => {
const json = JSON.parse(host.read('angular.json').toString());
if (Object.keys(json.projects || {}).length !== 1) {
throw new Error(
`The schematic can only be used with Angular CLI workspaces with a single project.`
);
}
const name = Object.keys(json.projects)[0];
host.create(
'nx.json',
serializeJson({
npmScope: name,
implicitDependencies: {
'angular.json': '*',
'package.json': '*',
'tsconfig.json': '*',
'tslint.json': '*',
'nx.json': '*',
},
projects: {
[name]: {
tags: [],
},
},
tasksRunnerOptions: {
default: {
runner: '@nrwl/workspace/tasks-runners/default',
options: {
cacheableOperations: ['build', 'lint', 'test', 'e2e'],
},
},
},
})
);
};
export default function (schema: Schema): Rule {
const options = {
...schema,
npmScope: toFileName(schema.npmScope || schema.name),
};
const templateSource = apply(url('./files'), [
template({
tmpl: '',
}),
]);
return chain([
checkCanConvertToWorkspace(options),
moveExistingFiles(options),
mergeWith(templateSource),
createAdditionalFiles(options),
updatePackageJson(),
updateAngularCLIJson(options),
updateTsLint(),
updateProjectTsLint(options),
updateTsConfig(options),
updateTsConfigsJson(options),
addInstallTask(options),
]);
if (schema.preserveAngularCLILayout) {
return chain([
addDepsToPackageJson({}, { '@nrwl/workspace': nxVersion }),
createNxJson,
]);
} else {
const options = {
...schema,
npmScope: toFileName(schema.npmScope || schema.name),
};
const templateSource = apply(url('./files'), [
template({
tmpl: '',
}),
]);
return chain([
checkCanConvertToWorkspace(options),
moveExistingFiles(options),
mergeWith(templateSource),
createAdditionalFiles(options),
updatePackageJson(),
updateAngularCLIJson(options),
updateTsLint(),
updateProjectTsLint(options),
updateTsConfig(options),
updateTsConfigsJson(options),
addInstallTask(options),
]);
}
}

View File

@ -2,4 +2,5 @@ export interface Schema {
name: string;
skipInstall: boolean;
npmScope?: string;
preserveAngularCLILayout: boolean;
}

View File

@ -14,6 +14,11 @@
"description": "Skip installing after adding @nrwl/workspace",
"default": false
},
"preserveAngularCLILayout": {
"type": "boolean",
"description": "Preserve the Angular CLI layout instead of moving the app into apps.",
"default": false
},
"name": {
"type": "string",
"description": "Project name.",