fix(angular): add support for migrating from empty Angular CLI workspace (#4043)

Closes #1465, #3128
This commit is contained in:
Brandon 2020-11-06 13:36:48 -06:00 committed by GitHub
parent 3825f4fe8b
commit 41ad265426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 21 deletions

View File

@ -8,6 +8,22 @@ using a monorepo approach. If you are currently using an Angular CLI workspace,
- The major version of your `Angular CLI` must align with the version of `Nx` you are upgrading to. For example, if you're using Angular CLI version 7, you must transition using the latest version 7 release of Nx. - The major version of your `Angular CLI` must align with the version of `Nx` you are upgrading to. For example, if you're using Angular CLI version 7, you must transition using the latest version 7 release of Nx.
- Currently, transforming an Angular CLI workspace to an Nx workspace automatically only supports a single project. If you have more than one project in your Angular CLI workspace, you can still migrate manually. - Currently, transforming an Angular CLI workspace to an Nx workspace automatically only supports a single project. If you have more than one project in your Angular CLI workspace, you can still migrate manually.
## Using ng add and preserving your existing structure
To add Nx to an existing Angular CLI workspace to an Nx workspace, with keeping your existing file structure in place, use the `ng add` command with the `--preserveAngularCLILayout` option:
```
ng add @nrwl/workspace --preserveAngularCLILayout
```
This installs the `@nrwl/workspace` package into your workspace and applies the following changes to your workspace:
- Adds and installs the `@nrwl/workspace` package in your development dependencies.
- Creates an nx.json file in the root of your workspace.
- Adds a `decorate-angular-cli.js` to the root of your workspace, and a `postinstall` script in your `package.json` to run the script when your dependencies are updated. The script forwards the `ng` commands to the Nx CLI(nx) to enable features such as Computation Caching.
After the process completes, you continue using the same serve/build/lint/test commands.
## Using ng add ## Using ng add
To transform a Angular CLI workspace to an Nx workspace, use the `ng add` command: To transform a Angular CLI workspace to an Nx workspace, use the `ng add` command:

View File

@ -115,6 +115,24 @@ describe('workspace', () => {
}); });
}); });
it('should error if the angular.json has only one library', async () => {
appTree.overwrite(
'/angular.json',
JSON.stringify({
projects: {
proj1: {
projectType: 'library',
},
},
})
);
try {
await runSchematic('ng-add', { name: 'myApp' }, appTree);
} catch (e) {
expect(e.message).toContain('Can only convert projects with one app');
}
});
it('should update tsconfig.base.json if present', async () => { it('should update tsconfig.base.json if present', async () => {
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree); const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(readJsonInTree(tree, 'tsconfig.base.json')).toMatchSnapshot(); expect(readJsonInTree(tree, 'tsconfig.base.json')).toMatchSnapshot();
@ -183,6 +201,59 @@ describe('workspace', () => {
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true); expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
}); });
it('should work with initial project outside of src', async () => {
appTree.overwrite(
'/angular.json',
JSON.stringify({
version: 1,
defaultProject: 'myApp',
newProjectRoot: 'projects',
projects: {
myApp: {
root: 'projects/myApp',
sourceRoot: 'projects/myApp/src',
architect: {
build: {
options: {
tsConfig: 'projects/myApp/tsconfig.app.json',
},
configurations: {},
},
test: {
options: {
tsConfig: 'projects/myApp/tsconfig.spec.json',
},
},
lint: {
options: {
tsConfig: [
'projects/myApp/tslint.json',
'projects/myApp/tsconfig.app.json',
],
},
},
e2e: {
options: {
protractorConfig: 'projects/myApp/e2e/protractor.conf.js',
},
},
},
},
},
})
);
appTree.create('/projects/myApp/tslint.json', '{"rules": {}}');
appTree.create('/projects/myApp/e2e/protractor.conf.js', '// content');
appTree.create('/projects/myApp/src/app/app.module.ts', '// content');
const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree);
expect(tree.exists('/tslint.json')).toBe(true);
expect(tree.exists('/apps/myApp/tsconfig.app.json')).toBe(true);
expect(tree.exists('/apps/myApp-e2e/protractor.conf.js')).toBe(true);
expect(tree.exists('/apps/myApp/src/app/app.module.ts')).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.overwrite( appTree.overwrite(
'/angular.json', '/angular.json',

View File

@ -83,6 +83,12 @@ function updatePackageJson() {
}); });
} }
function getRootTsConfigPath(host: Tree) {
return host.exists('tsconfig.base.json')
? 'tsconfig.base.json'
: 'tsconfig.json';
}
function convertPath(name: string, originalPath: string) { function convertPath(name: string, originalPath: string) {
return `apps/${name}/${originalPath}`; return `apps/${name}/${originalPath}`;
} }
@ -189,14 +195,14 @@ function updateAngularCLIJson(options: Schema): Rule {
function convertAsset(asset: string | any) { function convertAsset(asset: string | any) {
if (typeof asset === 'string') { if (typeof asset === 'string') {
return asset.startsWith(oldSourceRoot) return asset.startsWith(oldSourceRoot)
? convertPath(appName, asset) ? convertPath(appName, asset.replace(oldSourceRoot, 'src'))
: asset; : asset;
} else { } else {
return { return {
...asset, ...asset,
input: input:
asset.input && asset.input.startsWith(oldSourceRoot) asset.input && asset.input.startsWith(oldSourceRoot)
? convertPath(appName, asset.input) ? convertPath(appName, asset.input.replace(oldSourceRoot, 'src'))
: asset.input, : asset.input,
}; };
} }
@ -231,9 +237,7 @@ function updateAngularCLIJson(options: Schema): Rule {
function updateTsConfig(options: Schema): Rule { function updateTsConfig(options: Schema): Rule {
return (host: Tree) => { return (host: Tree) => {
let tsConfigPath = host.exists('tsconfig.base.json') const tsConfigPath = getRootTsConfigPath(host);
? 'tsconfig.base.json'
: 'tsconfig.json';
return updateJsonInTree(tsConfigPath, (tsConfigJson) => return updateJsonInTree(tsConfigPath, (tsConfigJson) =>
setUpCompilerOptions(tsConfigJson, options.npmScope, '') setUpCompilerOptions(tsConfigJson, options.npmScope, '')
); );
@ -245,19 +249,21 @@ function updateTsConfigsJson(options: Schema) {
const workspaceJson = readJsonInTree(host, 'angular.json'); const workspaceJson = readJsonInTree(host, 'angular.json');
const app = workspaceJson.projects[options.name]; const app = workspaceJson.projects[options.name];
const e2eProject = getE2eProject(workspaceJson); const e2eProject = getE2eProject(workspaceJson);
const tsConfigPath = getRootTsConfigPath(host);
const offset = '../../'; const offset = '../../';
return chain([ return chain([
updateJsonInTree(app.architect.build.options.tsConfig, (json) => { updateJsonInTree(app.architect.build.options.tsConfig, (json) => {
json.extends = `${offset}tsconfig.base.json`; json.extends = `${offset}${tsConfigPath}`;
json.compilerOptions = json.compilerOptions || {};
json.compilerOptions.outDir = `${offset}dist/out-tsc`; json.compilerOptions.outDir = `${offset}dist/out-tsc`;
return json; return json;
}), }),
app.architect.test app.architect.test
? updateJsonInTree(app.architect.test.options.tsConfig, (json) => { ? updateJsonInTree(app.architect.test.options.tsConfig, (json) => {
json.extends = `${offset}tsconfig.base.json`; json.extends = `${offset}${tsConfigPath}`;
json.compilerOptions = json.compilerOptions || {};
json.compilerOptions.outDir = `${offset}dist/out-tsc`; json.compilerOptions.outDir = `${offset}dist/out-tsc`;
return json; return json;
}) })
@ -265,6 +271,7 @@ function updateTsConfigsJson(options: Schema) {
app.architect.server app.architect.server
? updateJsonInTree(app.architect.server.options.tsConfig, (json) => { ? updateJsonInTree(app.architect.server.options.tsConfig, (json) => {
json.compilerOptions = json.compilerOptions || {};
json.compilerOptions.outDir = `${offset}dist/out-tsc`; json.compilerOptions.outDir = `${offset}dist/out-tsc`;
return json; return json;
}) })
@ -276,7 +283,7 @@ function updateTsConfigsJson(options: Schema) {
(json) => { (json) => {
json.extends = `${offsetFromRoot( json.extends = `${offsetFromRoot(
e2eProject.root e2eProject.root
)}tsconfig.base.json`; )}${tsConfigPath}`;
json.compilerOptions = { json.compilerOptions = {
...json.compilerOptions, ...json.compilerOptions,
outDir: `${offsetFromRoot(e2eProject.root)}dist/out-tsc`, outDir: `${offsetFromRoot(e2eProject.root)}dist/out-tsc`,
@ -427,11 +434,7 @@ function moveExistingFiles(options: Schema) {
); );
} }
const oldAppSourceRoot = app.sourceRoot; const oldAppSourceRoot = app.sourceRoot;
const newAppSourceRoot = join( const newAppSourceRoot = join(normalize('apps'), options.name, 'src');
normalize('apps'),
options.name,
app.sourceRoot
);
renameDirSyncInTree(host, oldAppSourceRoot, newAppSourceRoot, (err) => { renameDirSyncInTree(host, oldAppSourceRoot, newAppSourceRoot, (err) => {
if (!err) { if (!err) {
context.logger.info( context.logger.info(
@ -444,7 +447,7 @@ function moveExistingFiles(options: Schema) {
}); });
if (e2eApp) { if (e2eApp) {
const oldE2eRoot = 'e2e'; const oldE2eRoot = join(app.root, 'e2e');
const newE2eRoot = join( const newE2eRoot = join(
normalize('apps'), normalize('apps'),
getE2eKey(workspaceJson) + '-e2e' getE2eKey(workspaceJson) + '-e2e'
@ -470,6 +473,7 @@ function moveExistingFiles(options: Schema) {
function createAdditionalFiles(options: Schema): Rule { function createAdditionalFiles(options: Schema): Rule {
return (host: Tree, _context: SchematicContext) => { return (host: Tree, _context: SchematicContext) => {
const workspaceJson = readJsonInTree(host, 'angular.json'); const workspaceJson = readJsonInTree(host, 'angular.json');
const tsConfigPath = getRootTsConfigPath(host);
host.create( host.create(
'nx.json', 'nx.json',
serializeJson({ serializeJson({
@ -480,7 +484,7 @@ function createAdditionalFiles(options: Schema): Rule {
implicitDependencies: { implicitDependencies: {
'angular.json': '*', 'angular.json': '*',
'package.json': '*', 'package.json': '*',
'tsconfig.base.json': '*', [tsConfigPath]: '*',
'tslint.json': '*', 'tslint.json': '*',
'.eslintrc.json': '*', '.eslintrc.json': '*',
'nx.json': '*', 'nx.json': '*',
@ -545,7 +549,13 @@ function checkCanConvertToWorkspace(options: Schema) {
// TODO: This restriction should be lited // TODO: This restriction should be lited
const workspaceJson = readJsonInTree(host, 'angular.json'); const workspaceJson = readJsonInTree(host, 'angular.json');
if (Object.keys(workspaceJson.projects).length > 2) { const hasLibraries = Object.keys(workspaceJson.projects).find(
(project) =>
workspaceJson.projects[project].projectType &&
workspaceJson.projects[project].projectType !== 'application'
);
if (Object.keys(workspaceJson.projects).length > 2 || hasLibraries) {
throw new Error('Can only convert projects with one app'); throw new Error('Can only convert projects with one app');
} }
const e2eKey = getE2eKey(workspaceJson); const e2eKey = getE2eKey(workspaceJson);
@ -575,12 +585,20 @@ function checkCanConvertToWorkspace(options: Schema) {
const createNxJson = (host: Tree) => { const createNxJson = (host: Tree) => {
const json = JSON.parse(host.read('angular.json').toString()); const json = JSON.parse(host.read('angular.json').toString());
if (Object.keys(json.projects || {}).length !== 1) { const projects = json.projects || {};
const hasLibraries = Object.keys(projects).find(
(project) =>
projects[project].projectType &&
projects[project].projectType !== 'application'
);
if (Object.keys(projects).length !== 1 || hasLibraries) {
throw new Error( throw new Error(
`The schematic can only be used with Angular CLI workspaces with a single project.` `The schematic can only be used with Angular CLI workspaces with a single application.`
); );
} }
const name = Object.keys(json.projects)[0]; const name = Object.keys(projects)[0];
const tsConfigPath = getRootTsConfigPath(host);
host.create( host.create(
'nx.json', 'nx.json',
serializeJson({ serializeJson({
@ -588,7 +606,7 @@ const createNxJson = (host: Tree) => {
implicitDependencies: { implicitDependencies: {
'angular.json': '*', 'angular.json': '*',
'package.json': '*', 'package.json': '*',
'tsconfig.base.json': '*', [tsConfigPath]: '*',
'tslint.json': '*', 'tslint.json': '*',
'.eslintrc.json': '*', '.eslintrc.json': '*',
'nx.json': '*', 'nx.json': '*',