diff --git a/docs/angular/migration/migration-angular.md b/docs/angular/migration/migration-angular.md index 90bb361519..60bf3d20dc 100644 --- a/docs/angular/migration/migration-angular.md +++ b/docs/angular/migration/migration-angular.md @@ -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. - 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 To transform a Angular CLI workspace to an Nx workspace, use the `ng add` command: diff --git a/packages/workspace/src/schematics/init/init.spec.ts b/packages/workspace/src/schematics/init/init.spec.ts index 5f2b84d75b..50227cc899 100644 --- a/packages/workspace/src/schematics/init/init.spec.ts +++ b/packages/workspace/src/schematics/init/init.spec.ts @@ -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 () => { const tree = await runSchematic('ng-add', { name: 'myApp' }, appTree); expect(readJsonInTree(tree, 'tsconfig.base.json')).toMatchSnapshot(); @@ -183,6 +201,59 @@ describe('workspace', () => { 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 () => { appTree.overwrite( '/angular.json', diff --git a/packages/workspace/src/schematics/init/init.ts b/packages/workspace/src/schematics/init/init.ts index 0fa668051e..4fb98cddd6 100755 --- a/packages/workspace/src/schematics/init/init.ts +++ b/packages/workspace/src/schematics/init/init.ts @@ -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) { return `apps/${name}/${originalPath}`; } @@ -189,14 +195,14 @@ function updateAngularCLIJson(options: Schema): Rule { function convertAsset(asset: string | any) { if (typeof asset === 'string') { return asset.startsWith(oldSourceRoot) - ? convertPath(appName, asset) + ? convertPath(appName, asset.replace(oldSourceRoot, 'src')) : asset; } else { return { ...asset, input: asset.input && asset.input.startsWith(oldSourceRoot) - ? convertPath(appName, asset.input) + ? convertPath(appName, asset.input.replace(oldSourceRoot, 'src')) : asset.input, }; } @@ -231,9 +237,7 @@ function updateAngularCLIJson(options: Schema): Rule { function updateTsConfig(options: Schema): Rule { return (host: Tree) => { - let tsConfigPath = host.exists('tsconfig.base.json') - ? 'tsconfig.base.json' - : 'tsconfig.json'; + const tsConfigPath = getRootTsConfigPath(host); return updateJsonInTree(tsConfigPath, (tsConfigJson) => setUpCompilerOptions(tsConfigJson, options.npmScope, '') ); @@ -245,19 +249,21 @@ function updateTsConfigsJson(options: Schema) { const workspaceJson = readJsonInTree(host, 'angular.json'); const app = workspaceJson.projects[options.name]; const e2eProject = getE2eProject(workspaceJson); - + const tsConfigPath = getRootTsConfigPath(host); const offset = '../../'; return chain([ 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`; return json; }), app.architect.test ? 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`; return json; }) @@ -265,6 +271,7 @@ function updateTsConfigsJson(options: Schema) { app.architect.server ? updateJsonInTree(app.architect.server.options.tsConfig, (json) => { + json.compilerOptions = json.compilerOptions || {}; json.compilerOptions.outDir = `${offset}dist/out-tsc`; return json; }) @@ -276,7 +283,7 @@ function updateTsConfigsJson(options: Schema) { (json) => { json.extends = `${offsetFromRoot( e2eProject.root - )}tsconfig.base.json`; + )}${tsConfigPath}`; json.compilerOptions = { ...json.compilerOptions, outDir: `${offsetFromRoot(e2eProject.root)}dist/out-tsc`, @@ -427,11 +434,7 @@ function moveExistingFiles(options: Schema) { ); } const oldAppSourceRoot = app.sourceRoot; - const newAppSourceRoot = join( - normalize('apps'), - options.name, - app.sourceRoot - ); + const newAppSourceRoot = join(normalize('apps'), options.name, 'src'); renameDirSyncInTree(host, oldAppSourceRoot, newAppSourceRoot, (err) => { if (!err) { context.logger.info( @@ -444,7 +447,7 @@ function moveExistingFiles(options: Schema) { }); if (e2eApp) { - const oldE2eRoot = 'e2e'; + const oldE2eRoot = join(app.root, 'e2e'); const newE2eRoot = join( normalize('apps'), getE2eKey(workspaceJson) + '-e2e' @@ -470,6 +473,7 @@ function moveExistingFiles(options: Schema) { function createAdditionalFiles(options: Schema): Rule { return (host: Tree, _context: SchematicContext) => { const workspaceJson = readJsonInTree(host, 'angular.json'); + const tsConfigPath = getRootTsConfigPath(host); host.create( 'nx.json', serializeJson({ @@ -480,7 +484,7 @@ function createAdditionalFiles(options: Schema): Rule { implicitDependencies: { 'angular.json': '*', 'package.json': '*', - 'tsconfig.base.json': '*', + [tsConfigPath]: '*', 'tslint.json': '*', '.eslintrc.json': '*', 'nx.json': '*', @@ -545,7 +549,13 @@ function checkCanConvertToWorkspace(options: Schema) { // TODO: This restriction should be lited 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'); } const e2eKey = getE2eKey(workspaceJson); @@ -575,12 +585,20 @@ function checkCanConvertToWorkspace(options: Schema) { const createNxJson = (host: Tree) => { 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( - `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( 'nx.json', serializeJson({ @@ -588,7 +606,7 @@ const createNxJson = (host: Tree) => { implicitDependencies: { 'angular.json': '*', 'package.json': '*', - 'tsconfig.base.json': '*', + [tsConfigPath]: '*', 'tslint.json': '*', '.eslintrc.json': '*', 'nx.json': '*',