Files
angular/packages/compiler-cli/ngcc/src/writing/package_json_updater.ts
George Kalpakas 3d9dd6df0e refactor(ngcc): abstract updating package.json files behind an interface (#32427)
To persist some of its state, `ngcc` needs to update `package.json`
files (both in memory and on disk).

This refactoring abstracts these operations behind the
`PackageJsonUpdater` interface, making it easier to orchestrate them
from different contexts (e.g. when running tasks in parallel on multiple
processes).

Inspired by/Based on @alxhub's prototype: alxhub/angular@cb631bdb1

PR Close #32427
2019-09-09 15:55:13 -04:00

161 lines
5.7 KiB
TypeScript

/**
* @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 {AbsoluteFsPath, FileSystem, dirname} from '../../../src/ngtsc/file_system';
import {JsonObject, JsonValue} from '../packages/entry_point';
export type PackageJsonChange = [string[], JsonValue];
export type WritePackageJsonChangesFn =
(changes: PackageJsonChange[], packageJsonPath: AbsoluteFsPath, parsedJson?: JsonObject) =>
void;
/**
* A utility object that can be used to safely update values in a `package.json` file.
*
* Example usage:
* ```ts
* const updatePackageJson = packageJsonUpdater
* .createUpdate()
* .addChange(['name'], 'package-foo')
* .addChange(['scripts', 'foo'], 'echo FOOOO...')
* .addChange(['dependencies', 'bar'], '1.0.0')
* .writeChanges('/foo/package.json');
* // or
* // .writeChanges('/foo/package.json', inMemoryParsedJson);
* ```
*/
export interface PackageJsonUpdater {
/**
* Create a `PackageJsonUpdate` object, which provides a fluent API for batching updates to a
* `package.json` file. (Batching the updates is useful, because it avoid unnecessary I/O
* operations.)
*/
createUpdate(): PackageJsonUpdate;
/**
* Write a set of changes to the specified `package.json` file and (and optionally a pre-existing,
* in-memory representation of it).
*
* @param changes The set of changes to apply.
* @param packageJsonPath The path to the `package.json` file that needs to be updated.
* @param parsedJson A pre-existing, in-memory representation of the `package.json` file that
* needs to be updated as well.
*/
writeChanges(
changes: PackageJsonChange[], packageJsonPath: AbsoluteFsPath, parsedJson?: JsonObject): void;
}
/**
* A utility class providing a fluent API for recording multiple changes to a `package.json` file
* (and optionally its in-memory parsed representation).
*
* NOTE: This class should generally not be instantiated directly; instances are implicitly created
* via `PackageJsonUpdater#createUpdate()`.
*/
export class PackageJsonUpdate {
private changes: PackageJsonChange[] = [];
private applied = false;
constructor(private writeChangesImpl: WritePackageJsonChangesFn) {}
/**
* Record a change to a `package.json` property. If the ancestor objects do not yet exist in the
* `package.json` file, they will be created.
*
* @param propertyPath The path of a (possibly nested) property to update.
* @param value The new value to set the property to.
*/
addChange(propertyPath: string[], value: JsonValue): this {
this.ensureNotApplied();
this.changes.push([propertyPath, value]);
return this;
}
/**
* Write the recorded changes to the associated `package.json` file (and optionally a
* pre-existing, in-memory representation of it).
*
* @param packageJsonPath The path to the `package.json` file that needs to be updated.
* @param parsedJson A pre-existing, in-memory representation of the `package.json` file that
* needs to be updated as well.
*/
writeChanges(packageJsonPath: AbsoluteFsPath, parsedJson?: JsonObject): void {
this.ensureNotApplied();
this.writeChangesImpl(this.changes, packageJsonPath, parsedJson);
this.applied = true;
}
private ensureNotApplied() {
if (this.applied) {
throw new Error('Trying to apply a `PackageJsonUpdate` that has already been applied.');
}
}
}
/** A `PackageJsonUpdater` that writes directly to the file-system. */
export class DirectPackageJsonUpdater implements PackageJsonUpdater {
constructor(private fs: FileSystem) {}
createUpdate(): PackageJsonUpdate {
return new PackageJsonUpdate((...args) => this.writeChanges(...args));
}
writeChanges(
changes: PackageJsonChange[], packageJsonPath: AbsoluteFsPath,
preExistingParsedJson?: JsonObject): void {
if (changes.length === 0) {
throw new Error(`No changes to write to '${packageJsonPath}'.`);
}
// Read and parse the `package.json` content.
// NOTE: We are not using `preExistingParsedJson` (even if specified) to avoid corrupting the
// content on disk in case `preExistingParsedJson` is outdated.
const parsedJson =
this.fs.exists(packageJsonPath) ? JSON.parse(this.fs.readFile(packageJsonPath)) : {};
// Apply all changes to both the canonical representation (read from disk) and any pre-existing,
// in-memory representation.
for (const [propPath, value] of changes) {
if (propPath.length === 0) {
throw new Error(`Missing property path for writing value to '${packageJsonPath}'.`);
}
applyChange(parsedJson, propPath, value);
if (preExistingParsedJson) {
applyChange(preExistingParsedJson, propPath, value);
}
}
// Ensure the containing directory exists (in case this is a synthesized `package.json` due to a
// custom configuration) and write the updated content to disk.
this.fs.ensureDir(dirname(packageJsonPath));
this.fs.writeFile(packageJsonPath, `${JSON.stringify(parsedJson, null, 2)}\n`);
}
}
// Helpers
export function applyChange(ctx: JsonObject, propPath: string[], value: JsonValue): void {
const lastPropIdx = propPath.length - 1;
const lastProp = propPath[lastPropIdx];
for (let i = 0; i < lastPropIdx; i++) {
const key = propPath[i];
const newCtx = ctx.hasOwnProperty(key) ? ctx[key] : (ctx[key] = {});
if ((typeof newCtx !== 'object') || (newCtx === null) || Array.isArray(newCtx)) {
throw new Error(`Property path '${propPath.join('.')}' does not point to an object.`);
}
ctx = newCtx;
}
ctx[lastProp] = value;
}