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.
|
||||
- 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:
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '*',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user