fix(angular): add support for migrating from empty Angular CLI workspace (#4043)
Closes #1465, #3128
This commit is contained in:
parent
3825f4fe8b
commit
41ad265426
@ -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:
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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': '*',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user