From 04695d25ff760d68741a5c72bd936fd5c26d1620 Mon Sep 17 00:00:00 2001 From: Mike Brocchi Date: Wed, 6 Jun 2018 14:03:35 -0400 Subject: [PATCH] refactor: Consolidate adding dependencies --- .../schematics/angular/application/index.ts | 41 +++---- packages/schematics/angular/library/index.ts | 86 +++++++------- .../angular/migrations/update-6/index.ts | 60 +++------- .../angular/service-worker/index.ts | 21 ++-- .../schematics/angular/universal/index.ts | 20 ++-- .../angular/utility/dependencies.ts | 107 ++++++++++++++++++ .../angular/utility/dependencies_spec.ts | 91 +++++++++++++++ .../update-6 => utility}/json-utils.ts | 71 +++++++++++- .../angular/utility/json-utils_spec.ts | 81 +++++++++++++ 9 files changed, 438 insertions(+), 140 deletions(-) create mode 100644 packages/schematics/angular/utility/dependencies.ts create mode 100644 packages/schematics/angular/utility/dependencies_spec.ts rename packages/schematics/angular/{migrations/update-6 => utility}/json-utils.ts (51%) create mode 100644 packages/schematics/angular/utility/json-utils_spec.ts diff --git a/packages/schematics/angular/application/index.ts b/packages/schematics/angular/application/index.ts index e0f98ff01d2a..8909b5c00a04 100644 --- a/packages/schematics/angular/application/index.ts +++ b/packages/schematics/angular/application/index.ts @@ -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; }; diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 5de82e61e09d..c008ad5f70a0 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -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 { (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; } - - 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'] = {}; - } + [ + { + 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)); - 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; }; } diff --git a/packages/schematics/angular/migrations/update-6/index.ts b/packages/schematics/angular/migrations/update-6/index.ts index f9243e5eca01..a6cf7c0c32a5 100644 --- a/packages/schematics/angular/migrations/update-6/index.ts +++ b/packages/schematics/angular/migrations/update-6/index.ts @@ -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, diff --git a/packages/schematics/angular/service-worker/index.ts b/packages/schematics/angular/service-worker/index.ts index c30fcc980c4e..77c6902576f3 100644 --- a/packages/schematics/angular/service-worker/index.ts +++ b/packages/schematics/angular/service-worker/index.ts @@ -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; }; diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts index 245a60559391..2f02522ce844 100644 --- a/packages/schematics/angular/universal/index.ts +++ b/packages/schematics/angular/universal/index.ts @@ -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; }; diff --git a/packages/schematics/angular/utility/dependencies.ts b/packages/schematics/angular/utility/dependencies.ts new file mode 100644 index 000000000000..892dece42926 --- /dev/null +++ b/packages/schematics/angular/utility/dependencies.ts @@ -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; +} diff --git a/packages/schematics/angular/utility/dependencies_spec.ts b/packages/schematics/angular/utility/dependencies_spec.ts new file mode 100644 index 000000000000..61a95a765fc3 --- /dev/null +++ b/packages/schematics/angular/utility/dependencies_spec.ts @@ -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(); + }); + }); +}); diff --git a/packages/schematics/angular/migrations/update-6/json-utils.ts b/packages/schematics/angular/utility/json-utils.ts similarity index 51% rename from packages/schematics/angular/migrations/update-6/json-utils.ts rename to packages/schematics/angular/utility/json-utils.ts index 6397fa4b6161..2894b01c2f07 100644 --- a/packages/schematics/angular/migrations/update-6/json-utils.ts +++ b/packages/schematics/angular/utility/json-utils.ts @@ -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(' '); +} diff --git a/packages/schematics/angular/utility/json-utils_spec.ts b/packages/schematics/angular/utility/json-utils_spec.ts new file mode 100644 index 000000000000..98ab24c1bc90 --- /dev/null +++ b/packages/schematics/angular/utility/json-utils_spec.ts @@ -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']); + }); + }); +});