refactor: Consolidate adding dependencies

This commit is contained in:
Mike Brocchi 2018-06-06 14:03:35 -04:00 committed by Filipe Silva
parent 04893ca343
commit d3b49a5590
9 changed files with 438 additions and 140 deletions

View File

@ -29,6 +29,7 @@ import {
addProjectToWorkspace,
getWorkspace,
} from '../utility/config';
import { NodeDependencyType, addPackageJsonDependency } from '../utility/dependencies';
import { latestVersions } from '../utility/latest-versions';
import { validateProjectName } from '../utility/validation';
import { Schema as ApplicationOptions } from './schema';
@ -60,29 +61,23 @@ import { Schema as ApplicationOptions } from './schema';
function addDependenciesToPackageJson() {
return (host: Tree) => {
const packageJsonPath = 'package.json';
if (!host.exists('package.json')) { return host; }
const source = host.read('package.json');
if (!source) { return host; }
const sourceText = source.toString('utf-8');
const json = JSON.parse(sourceText);
if (!json['devDependencies']) {
json['devDependencies'] = {};
}
json.devDependencies = {
'@angular/compiler-cli': latestVersions.Angular,
'@angular-devkit/build-angular': latestVersions.DevkitBuildAngular,
'typescript': latestVersions.TypeScript,
// De-structure last keeps existing user dependencies.
...json.devDependencies,
};
host.overwrite(packageJsonPath, JSON.stringify(json, null, 2));
[
{
type: NodeDependencyType.Dev,
name: '@angular/compiler-cli',
version: latestVersions.Angular,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-angular',
version: latestVersions.DevkitBuildAngular,
},
{
type: NodeDependencyType.Dev,
name: 'typescript',
version: latestVersions.TypeScript,
},
].forEach(dependency => addPackageJsonDependency(host, dependency));
return host;
};

View File

@ -27,23 +27,15 @@ import {
addProjectToWorkspace,
getWorkspace,
} from '../utility/config';
import {
NodeDependencyType,
addPackageJsonDependency,
} from '../utility/dependencies';
import { latestVersions } from '../utility/latest-versions';
import { validateProjectName } from '../utility/validation';
import { Schema as LibraryOptions } from './schema';
type PackageJsonPartialType = {
scripts: {
[key: string]: string;
},
dependencies: {
[key: string]: string;
},
devDependencies: {
[key: string]: string;
},
};
interface UpdateJsonFn<T> {
(obj: T): T | void;
}
@ -96,39 +88,45 @@ function updateTsConfig(packageName: string, distRoot: string) {
function addDependenciesToPackageJson() {
return (host: Tree) => {
if (!host.exists('package.json')) { return host; }
[
{
type: NodeDependencyType.Dev,
name: '@angular/compiler-cli',
version: latestVersions.Angular,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-ng-packagr',
version: latestVersions.DevkitBuildNgPackagr,
},
{
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-angular',
version: latestVersions.DevkitBuildNgPackagr,
},
{
type: NodeDependencyType.Dev,
name: 'ng-packagr',
version: '^3.0.0',
},
{
type: NodeDependencyType.Dev,
name: 'tsickle',
version: '>=0.29.0',
},
{
type: NodeDependencyType.Dev,
name: 'tslib',
version: '^1.9.0',
},
{
type: NodeDependencyType.Dev,
name: 'typescript',
version: latestVersions.TypeScript,
},
].forEach(dependency => addPackageJsonDependency(host, dependency));
return updateJsonFile(host, 'package.json', (json: PackageJsonPartialType) => {
if (!json['dependencies']) {
json['dependencies'] = {};
}
json.dependencies = {
'@angular/common': latestVersions.Angular,
'@angular/core': latestVersions.Angular,
'@angular/compiler': latestVersions.Angular,
// De-structure last keeps existing user dependencies.
...json.dependencies,
};
if (!json['devDependencies']) {
json['devDependencies'] = {};
}
json.devDependencies = {
'@angular/compiler-cli': latestVersions.Angular,
'@angular-devkit/build-ng-packagr': latestVersions.DevkitBuildNgPackagr,
'@angular-devkit/build-angular': latestVersions.DevkitBuildNgPackagr,
'ng-packagr': '^3.0.0',
'tsickle': '>=0.29.0',
'tslib': '^1.9.0',
'typescript': latestVersions.TypeScript,
// De-structure last keeps existing user dependencies.
...json.devDependencies,
};
});
return host;
};
}

View File

@ -26,12 +26,16 @@ import {
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import { AppConfig, CliConfig } from '../../utility/config';
import { latestVersions } from '../../utility/latest-versions';
import {
appendPropertyInAstObject,
NodeDependency,
NodeDependencyType,
addPackageJsonDependency,
} from '../../utility/dependencies';
import {
appendValueInAstArray,
findPropertyInAstObject,
} from './json-utils';
} from '../../utility/json-utils';
import { latestVersions } from '../../utility/latest-versions';
const defaults = {
appRoot: 'src',
@ -649,49 +653,13 @@ function updateSpecTsConfig(config: CliConfig): Rule {
function updatePackageJson(config: CliConfig) {
return (host: Tree, context: SchematicContext) => {
const pkgPath = '/package.json';
const buffer = host.read(pkgPath);
if (buffer == null) {
throw new SchematicsException('Could not read package.json');
}
const pkgAst = parseJsonAst(buffer.toString(), JsonParseMode.Strict);
if (pkgAst.kind != 'object') {
throw new SchematicsException('Error reading package.json');
}
const devDependenciesNode = findPropertyInAstObject(pkgAst, 'devDependencies');
if (devDependenciesNode && devDependenciesNode.kind != 'object') {
throw new SchematicsException('Error reading package.json; devDependency is not an object.');
}
const recorder = host.beginUpdate(pkgPath);
const depName = '@angular-devkit/build-angular';
if (!devDependenciesNode) {
// Haven't found the devDependencies key, add it to the root of the package.json.
appendPropertyInAstObject(recorder, pkgAst, 'devDependencies', {
[depName]: latestVersions.DevkitBuildAngular,
});
} else {
// Check if there's a build-angular key.
const buildAngularNode = findPropertyInAstObject(devDependenciesNode, depName);
if (!buildAngularNode) {
// No build-angular package, add it.
appendPropertyInAstObject(
recorder,
devDependenciesNode,
depName,
latestVersions.DevkitBuildAngular,
);
} else {
const { end, start } = buildAngularNode;
recorder.remove(start.offset, end.offset - start.offset);
recorder.insertRight(start.offset, JSON.stringify(latestVersions.DevkitBuildAngular));
}
}
host.commitUpdate(recorder);
const dependency: NodeDependency = {
type: NodeDependencyType.Dev,
name: '@angular-devkit/build-angular',
version: latestVersions.DevkitBuildAngular,
overwrite: true,
};
addPackageJsonDependency(host, dependency);
context.addTask(new NodePackageInstallTask({
packageManager: config.packageManager === 'default' ? undefined : config.packageManager,

View File

@ -26,11 +26,10 @@ import {
getWorkspace,
getWorkspacePath,
} from '../utility/config';
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
import { getAppModulePath } from '../utility/ng-ast-utils';
import { Schema as ServiceWorkerOptions } from './schema';
const packageJsonPath = '/package.json';
function updateConfigFile(options: ServiceWorkerOptions): Rule {
return (host: Tree, context: SchematicContext) => {
context.logger.debug('updating config file.');
@ -72,17 +71,15 @@ function addDependencies(): Rule {
return (host: Tree, context: SchematicContext) => {
const packageName = '@angular/service-worker';
context.logger.debug(`adding dependency (${packageName})`);
const buffer = host.read(packageJsonPath);
if (buffer === null) {
throw new SchematicsException('Could not find package.json');
const coreDep = getPackageJsonDependency(host, '@angular/core');
if (coreDep === null) {
throw new SchematicsException('Could not find version.');
}
const packageObject = JSON.parse(buffer.toString());
const ngCoreVersion = packageObject.dependencies['@angular/core'];
packageObject.dependencies[packageName] = ngCoreVersion;
host.overwrite(packageJsonPath, JSON.stringify(packageObject, null, 2));
const platformServerDep = {
...coreDep,
name: packageName,
};
addPackageJsonDependency(host, platformServerDep);
return host;
};

View File

@ -34,6 +34,7 @@ import * as ts from 'typescript';
import { findNode, getDecoratorMetadata } from '../utility/ast-utils';
import { InsertChange } from '../utility/change';
import { getWorkspace } from '../utility/config';
import { addPackageJsonDependency, getPackageJsonDependency } from '../utility/dependencies';
import { findBootstrapModuleCall, findBootstrapModulePath } from '../utility/ng-ast-utils';
import { Schema as UniversalOptions } from './schema';
@ -172,18 +173,15 @@ function addServerTransition(options: UniversalOptions): Rule {
function addDependencies(): Rule {
return (host: Tree) => {
const pkgPath = '/package.json';
const buffer = host.read(pkgPath);
if (buffer === null) {
throw new SchematicsException('Could not find package.json');
const coreDep = getPackageJsonDependency(host, '@angular/core');
if (coreDep === null) {
throw new SchematicsException('Could not find version.');
}
const pkg = JSON.parse(buffer.toString());
const ngCoreVersion = pkg.dependencies['@angular/core'];
pkg.dependencies['@angular/platform-server'] = ngCoreVersion;
host.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
const platformServerDep = {
...coreDep,
name: '@angular/platform-server',
};
addPackageJsonDependency(host, platformServerDep);
return host;
};

View File

@ -0,0 +1,107 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { JsonAstObject, JsonParseMode, parseJsonAst } from '@angular-devkit/core';
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import {
appendPropertyInAstObject,
findPropertyInAstObject,
insertPropertyInAstObjectInOrder,
} from './json-utils';
const pkgJsonPath = '/package.json';
export enum NodeDependencyType {
Default = 'dependencies',
Dev = 'devDependencies',
Peer = 'peerDependencies',
Optional = 'optionalDependencies',
}
export interface NodeDependency {
type: NodeDependencyType;
name: string;
version: string;
overwrite?: boolean;
}
export function addPackageJsonDependency(tree: Tree, dependency: NodeDependency): void {
const packageJsonAst = _readPackageJson(tree);
const depsNode = findPropertyInAstObject(packageJsonAst, dependency.type);
const recorder = tree.beginUpdate(pkgJsonPath);
if (!depsNode) {
// Haven't found the dependencies key, add it to the root of the package.json.
appendPropertyInAstObject(recorder, packageJsonAst, dependency.type, {
[dependency.name]: dependency.version,
}, 4);
} else if (depsNode.kind === 'object') {
// check if package already added
const depNode = findPropertyInAstObject(depsNode, dependency.name);
if (!depNode) {
// Package not found, add it.
insertPropertyInAstObjectInOrder(
recorder,
depsNode,
dependency.name,
dependency.version,
4,
);
} else if (dependency.overwrite) {
// Package found, update version if overwrite.
const { end, start } = depNode;
recorder.remove(start.offset, end.offset - start.offset);
recorder.insertRight(start.offset, JSON.stringify(dependency.version));
}
}
tree.commitUpdate(recorder);
}
export function getPackageJsonDependency(tree: Tree, name: string): NodeDependency | null {
const packageJson = _readPackageJson(tree);
let dep: NodeDependency | null = null;
[
NodeDependencyType.Default,
NodeDependencyType.Dev,
NodeDependencyType.Optional,
NodeDependencyType.Peer,
].forEach(depType => {
if (dep !== null) {
return;
}
const depsNode = findPropertyInAstObject(packageJson, depType);
if (depsNode !== null && depsNode.kind === 'object') {
const depNode = findPropertyInAstObject(depsNode, name);
if (depNode !== null && depNode.kind === 'string') {
const version = depNode.value;
dep = {
type: depType,
name: name,
version: version,
};
}
}
});
return dep;
}
function _readPackageJson(tree: Tree): JsonAstObject {
const buffer = tree.read(pkgJsonPath);
if (buffer === null) {
throw new SchematicsException('Could not read package.json.');
}
const content = buffer.toString();
const packageJson = parseJsonAst(content, JsonParseMode.Strict);
if (packageJson.kind != 'object') {
throw new SchematicsException('Invalid package.json. Was expecting an object');
}
return packageJson;
}

View File

@ -0,0 +1,91 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { EmptyTree } from '@angular-devkit/schematics';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import {
NodeDependency,
NodeDependencyType,
addPackageJsonDependency,
getPackageJsonDependency,
} from './dependencies';
describe('dependencies', () => {
describe('addDependency', () => {
let tree: UnitTestTree;
const pkgJsonPath = '/package.json';
let dependency: NodeDependency;
beforeEach(() => {
tree = new UnitTestTree(new EmptyTree());
tree.create(pkgJsonPath, '{}');
dependency = {
type: NodeDependencyType.Default,
name: 'my-pkg',
version: '1.2.3',
};
});
[
{ type: NodeDependencyType.Default, key: 'dependencies' },
{ type: NodeDependencyType.Dev, key: 'devDependencies' },
{ type: NodeDependencyType.Optional, key: 'optionalDependencies' },
{ type: NodeDependencyType.Peer, key: 'peerDependencies' },
].forEach(type => {
describe(`Type: ${type.toString()}`, () => {
beforeEach(() => {
dependency.type = type.type;
});
it('should add a dependency', () => {
addPackageJsonDependency(tree, dependency);
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
expect(pkgJson[type.key][dependency.name]).toEqual(dependency.version);
});
it('should handle an existing dependency (update version)', () => {
addPackageJsonDependency(tree, {...dependency, version: '0.0.0'});
addPackageJsonDependency(tree, {...dependency, overwrite: true});
const pkgJson = JSON.parse(tree.readContent(pkgJsonPath));
expect(pkgJson[type.key][dependency.name]).toEqual(dependency.version);
});
});
});
it('should throw when missing package.json', () => {
expect((() => addPackageJsonDependency(new EmptyTree(), dependency))).toThrow();
});
});
describe('getDependency', () => {
let tree: UnitTestTree;
beforeEach(() => {
const pkgJsonPath = '/package.json';
const pkgJsonContent = JSON.stringify({
dependencies: {
'my-pkg': '1.2.3',
},
}, null, 2);
tree = new UnitTestTree(new EmptyTree());
tree.create(pkgJsonPath, pkgJsonContent);
});
it('should get a dependency', () => {
const dep = getPackageJsonDependency(tree, 'my-pkg') as NodeDependency;
expect(dep.type).toEqual(NodeDependencyType.Default);
expect(dep.name).toEqual('my-pkg');
expect(dep.version).toEqual('1.2.3');
});
it('should return null if dependency does not exist', () => {
const dep = getPackageJsonDependency(tree, 'missing-pkg') as NodeDependency;
expect(dep).toBeNull();
});
});
});

View File

@ -5,7 +5,13 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { JsonAstArray, JsonAstNode, JsonAstObject, JsonValue } from '@angular-devkit/core';
import {
JsonAstArray,
JsonAstKeyValue,
JsonAstNode,
JsonAstObject,
JsonValue,
} from '@angular-devkit/core';
import { UpdateRecorder } from '@angular-devkit/schematics';
export function appendPropertyInAstObject(
@ -13,9 +19,9 @@ export function appendPropertyInAstObject(
node: JsonAstObject,
propertyName: string,
value: JsonValue,
indent = 4,
indent: number,
) {
const indentStr = '\n' + new Array(indent + 1).join(' ');
const indentStr = _buildIndent(indent);
if (node.properties.length > 0) {
// Insert comma.
@ -31,6 +37,59 @@ export function appendPropertyInAstObject(
);
}
export function insertPropertyInAstObjectInOrder(
recorder: UpdateRecorder,
node: JsonAstObject,
propertyName: string,
value: JsonValue,
indent: number,
) {
if (node.properties.length === 0) {
appendPropertyInAstObject(recorder, node, propertyName, value, indent);
return;
}
// Find insertion info.
let insertAfterProp: JsonAstKeyValue | null = null;
let prev: JsonAstKeyValue | null = null;
let isLastProp = false;
const last = node.properties[node.properties.length - 1];
for (const prop of node.properties) {
if (prop.key.value > propertyName) {
if (prev) {
insertAfterProp = prev;
}
break;
}
if (prop === last) {
isLastProp = true;
insertAfterProp = last;
}
prev = prop;
}
if (isLastProp) {
appendPropertyInAstObject(recorder, node, propertyName, value, indent);
return;
}
const indentStr = _buildIndent(indent);
const insertIndex = insertAfterProp === null
? node.start.offset + 1
: insertAfterProp.end.offset + 1;
recorder.insertRight(
insertIndex,
`${indentStr}`
+ `"${propertyName}": ${JSON.stringify(value, null, 2).replace(/\n/g, indentStr)}`
+ ',',
);
}
export function appendValueInAstArray(
recorder: UpdateRecorder,
@ -38,7 +97,7 @@ export function appendValueInAstArray(
value: JsonValue,
indent = 4,
) {
const indentStr = '\n' + new Array(indent + 1).join(' ');
const indentStr = _buildIndent(indent);
if (node.elements.length > 0) {
// Insert comma.
@ -68,3 +127,7 @@ export function findPropertyInAstObject(
return maybeNode;
}
function _buildIndent(count: number): string {
return '\n' + new Array(count + 1).join(' ');
}

View File

@ -0,0 +1,81 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { parseJsonAst } from '@angular-devkit/core';
import { HostTree } from '@angular-devkit/schematics';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { insertPropertyInAstObjectInOrder } from './json-utils';
type Pojso = {
[key: string]: string;
};
describe('json-utils', () => {
const filePath = '/temp';
let tree: UnitTestTree;
beforeEach(() => {
tree = new UnitTestTree(new HostTree());
});
describe('insertPropertyInAstObjectInOrder', () => {
function runTest(obj: Pojso, prop: string, val: string): Pojso {
const content = JSON.stringify(obj, null, 2);
tree.create(filePath, content);
const ast = parseJsonAst(content);
const rec = tree.beginUpdate(filePath);
if (ast.kind === 'object') {
insertPropertyInAstObjectInOrder(rec, ast, prop, val, 2);
}
tree.commitUpdate(rec);
const result = JSON.parse(tree.readContent(filePath));
// Clean up the tree by deleting the file.
tree.delete(filePath);
return result;
}
it('should insert a first prop', () => {
const obj = {
m: 'm',
z: 'z',
};
const result = runTest(obj, 'a', 'val');
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
});
it('should insert a middle prop', () => {
const obj = {
a: 'a',
z: 'z',
};
const result = runTest(obj, 'm', 'val');
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
});
it('should insert a last prop', () => {
const obj = {
a: 'a',
m: 'm',
};
const result = runTest(obj, 'z', 'val');
expect(Object.keys(result)).toEqual(['a', 'm', 'z']);
});
it('should insert multiple props', () => {
let obj = {};
obj = runTest(obj, 'z', 'val');
expect(Object.keys(obj)).toEqual(['z']);
obj = runTest(obj, 'm', 'val');
expect(Object.keys(obj)).toEqual(['m', 'z']);
obj = runTest(obj, 'a', 'val');
expect(Object.keys(obj)).toEqual(['a', 'm', 'z']);
obj = runTest(obj, 'b', 'val');
expect(Object.keys(obj)).toEqual(['a', 'b', 'm', 'z']);
});
});
});