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', () => { it('should handle different types of errors', () => {
// create a new AngularCLI app // create a new AngularCLI app
runNew(); runNew();
@ -353,6 +337,18 @@ forEachCli('angular', () => {
// Put src back // Put src back
runCommand('mv src-bak src'); 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()); appTree = new UnitTestTree(Tree.empty());
}); });
it('should error if no package.json is present', async () => { describe('move to nx layout', () => {
try { it('should error if no package.json is present', async () => {
await runSchematic('ng-add', { name: 'myApp' }, appTree); try {
fail('should throw'); await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) { fail('should throw');
expect(e.message).toContain('Cannot find package.json'); } catch (e) {
} expect(e.message).toContain('Cannot find package.json');
}); }
});
it('should error if no e2e/protractor.conf.js is present', async () => { it('should error if no e2e/protractor.conf.js is present', async () => {
appTree.create('/package.json', JSON.stringify({})); appTree.create('/package.json', JSON.stringify({}));
appTree.create( appTree.create(
'/angular.json', '/angular.json',
JSON.stringify({ JSON.stringify({
projects: { projects: {
proj1: { proj1: {
architect: { architect: {
e2e: { e2e: {
options: { options: {
protractorConfig: 'e2e/protractor.conf.js', 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('/package.json', JSON.stringify({}));
appTree.create('/e2e/protractor.conf.js', ''); appTree.create('/e2e/protractor.conf.js', '');
await runSchematic('ng-add', { name: 'myApp' }, appTree); appTree.create(
} catch (e) { '/angular.json',
expect(e.message).toContain('Cannot find 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 () => { it('should work without nested tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({})); appTree.create('/package.json', JSON.stringify({}));
appTree.create('/e2e/protractor.conf.js', ''); appTree.create(
appTree.create( '/angular.json',
'/angular.json', JSON.stringify({
JSON.stringify({ version: 1,
projects: { defaultProject: 'myApp',
proj1: {}, projects: {
'proj1-e2e': {}, myApp: {
proj2: {}, root: '',
'proj2-e2e': {}, sourceRoot: 'src',
}, architect: {
}) build: {
); options: {
try { tsConfig: 'tsconfig.app.json',
await runSchematic('ng-add', { name: 'myApp' }, appTree); },
} catch (e) { configurations: {},
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',
}, },
configurations: {}, test: {
}, options: {
test: { tsConfig: 'tsconfig.spec.json',
options: { },
tsConfig: 'tsconfig.spec.json',
}, },
}, lint: {
lint: { options: {
options: { tsConfig: 'tsconfig.app.json',
tsConfig: 'tsconfig.app.json', },
}, },
}, e2e: {
e2e: { options: {
options: { protractorConfig: 'e2e/protractor.conf.js',
protractorConfig: 'e2e/protractor.conf.js', },
}, },
}, },
}, },
}, },
}, })
}) );
); appTree.create(
appTree.create( '/tsconfig.app.json',
'/tsconfig.app.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create(
appTree.create( '/tsconfig.spec.json',
'/tsconfig.spec.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tsconfig.json', '{"compilerOptions": {}}'); appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/tslint.json', '{"rules": {}}'); appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/e2e/protractor.conf.js', '// content'); appTree.create('/src/app/app.module.ts', '// content');
appTree.create('/src/app/app.module.ts', '// 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/tsconfig.app.json')).toBe(true); });
});
it('should work with nested (sub-dir) tsconfig files', async () => { it('should work with nested (sub-dir) tsconfig files', async () => {
appTree.create('/package.json', JSON.stringify({})); appTree.create('/package.json', JSON.stringify({}));
appTree.create( appTree.create(
'/angular.json', '/angular.json',
JSON.stringify({ JSON.stringify({
version: 1, version: 1,
defaultProject: 'myApp', defaultProject: 'myApp',
projects: { projects: {
myApp: { myApp: {
sourceRoot: 'src', sourceRoot: 'src',
architect: { architect: {
build: { build: {
options: { options: {
tsConfig: 'src/tsconfig.app.json', tsConfig: 'src/tsconfig.app.json',
},
configurations: {},
}, },
configurations: {}, test: {
}, options: {
test: { tsConfig: 'src/tsconfig.spec.json',
options: { },
tsConfig: 'src/tsconfig.spec.json',
}, },
}, lint: {
lint: { options: {
options: { tsConfig: 'src/tsconfig.app.json',
tsConfig: 'src/tsconfig.app.json', },
}, },
}, e2e: {
e2e: { options: {
options: { protractorConfig: 'e2e/protractor.conf.js',
protractorConfig: 'e2e/protractor.conf.js', },
}, },
}, },
}, },
}, },
}, })
}) );
); appTree.create(
appTree.create( '/src/tsconfig.app.json',
'/src/tsconfig.app.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create(
appTree.create( '/src/tsconfig.spec.json',
'/src/tsconfig.spec.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tsconfig.json', '{"compilerOptions": {}}'); appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/tslint.json', '{"rules": {}}'); appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/e2e/protractor.conf.js', '// content'); appTree.create('/src/app/app.module.ts', '// content');
appTree.create('/src/app/app.module.ts', '// 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/tsconfig.app.json')).toBe(true); });
});
it('should work with missing e2e, lint, or test targets', async () => { it('should work with missing e2e, lint, or test targets', async () => {
appTree.create('/package.json', JSON.stringify({})); appTree.create('/package.json', JSON.stringify({}));
appTree.create( appTree.create(
'/angular.json', '/angular.json',
JSON.stringify({ JSON.stringify({
version: 1, version: 1,
defaultProject: 'myApp', defaultProject: 'myApp',
projects: { projects: {
myApp: { myApp: {
root: '', root: '',
sourceRoot: 'src', sourceRoot: 'src',
architect: { architect: {
build: { build: {
options: { options: {
tsConfig: 'tsconfig.app.json', tsConfig: 'tsconfig.app.json',
},
configurations: {},
}, },
configurations: {},
}, },
}, },
}, },
}, })
}) );
); appTree.create(
appTree.create( '/tsconfig.app.json',
'/tsconfig.app.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create(
appTree.create( '/tsconfig.spec.json',
'/tsconfig.spec.json', '{"extends": "../tsconfig.json", "compilerOptions": {}}'
'{"extends": "../tsconfig.json", "compilerOptions": {}}' );
); appTree.create('/tsconfig.json', '{"compilerOptions": {}}');
appTree.create('/tsconfig.json', '{"compilerOptions": {}}'); appTree.create('/tslint.json', '{"rules": {}}');
appTree.create('/tslint.json', '{"rules": {}}'); appTree.create('/e2e/protractor.conf.js', '// content');
appTree.create('/e2e/protractor.conf.js', '// content'); appTree.create('/src/app/app.module.ts', '// content');
appTree.create('/src/app/app.module.ts', '// content'); appTree.create('/karma.conf.js', '// 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/tsconfig.app.json')).toBe(true);
expect(tree.exists('/apps/myApp/karma.conf.js')).toBe(true); expect(tree.exists('/apps/myApp/karma.conf.js')).toBe(true);
expect(tree.exists('/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, renameSyncInTree,
renameDirSyncInTree, renameDirSyncInTree,
addInstallTask, addInstallTask,
addDepsToPackageJson,
} from '@nrwl/workspace'; } from '@nrwl/workspace';
import { DEFAULT_NRWL_PRETTIER_CONFIG } from '../workspace/workspace'; import { DEFAULT_NRWL_PRETTIER_CONFIG } from '../workspace/workspace';
import { JsonArray } from '@angular-devkit/core'; 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 { export default function (schema: Schema): Rule {
const options = { if (schema.preserveAngularCLILayout) {
...schema, return chain([
npmScope: toFileName(schema.npmScope || schema.name), addDepsToPackageJson({}, { '@nrwl/workspace': nxVersion }),
}; createNxJson,
const templateSource = apply(url('./files'), [ ]);
template({ } else {
tmpl: '', const options = {
}), ...schema,
]); npmScope: toFileName(schema.npmScope || schema.name),
return chain([ };
checkCanConvertToWorkspace(options), const templateSource = apply(url('./files'), [
moveExistingFiles(options), template({
mergeWith(templateSource), tmpl: '',
createAdditionalFiles(options), }),
updatePackageJson(), ]);
updateAngularCLIJson(options), return chain([
updateTsLint(), checkCanConvertToWorkspace(options),
updateProjectTsLint(options), moveExistingFiles(options),
updateTsConfig(options), mergeWith(templateSource),
updateTsConfigsJson(options), createAdditionalFiles(options),
addInstallTask(options), updatePackageJson(),
]); updateAngularCLIJson(options),
updateTsLint(),
updateProjectTsLint(options),
updateTsConfig(options),
updateTsConfigsJson(options),
addInstallTask(options),
]);
}
} }

View File

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

View File

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