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
This commit is contained in:

committed by
Matias Niemelä

parent
38359b166e
commit
3d9dd6df0e
@ -14,7 +14,7 @@ import {FileWriter} from './file_writer';
|
||||
|
||||
/**
|
||||
* This FileWriter overwrites the transformed file, in-place, while creating
|
||||
* a back-up of the original file with an extra `.bak` extension.
|
||||
* a back-up of the original file with an extra `.__ivy_ngcc_bak` extension.
|
||||
*/
|
||||
export class InPlaceFileWriter implements FileWriter {
|
||||
constructor(protected fs: FileSystem) {}
|
||||
|
@ -6,13 +6,14 @@
|
||||
* 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, absoluteFromSourceFile, dirname, join, relative} from '../../../src/ngtsc/file_system';
|
||||
import {AbsoluteFsPath, FileSystem, absoluteFromSourceFile, dirname, join, relative} from '../../../src/ngtsc/file_system';
|
||||
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
|
||||
import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
|
||||
import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {FileToWrite} from '../rendering/utils';
|
||||
|
||||
import {InPlaceFileWriter} from './in_place_file_writer';
|
||||
import {PackageJsonUpdater} from './package_json_updater';
|
||||
|
||||
const NGCC_DIRECTORY = '__ivy_ngcc__';
|
||||
|
||||
@ -25,6 +26,8 @@ const NGCC_DIRECTORY = '__ivy_ngcc__';
|
||||
* `InPlaceFileWriter`).
|
||||
*/
|
||||
export class NewEntryPointFileWriter extends InPlaceFileWriter {
|
||||
constructor(fs: FileSystem, private pkgJsonUpdater: PackageJsonUpdater) { super(fs); }
|
||||
|
||||
writeBundle(
|
||||
bundle: EntryPointBundle, transformedFiles: FileToWrite[],
|
||||
formatProperties: EntryPointJsonProperty[]) {
|
||||
@ -65,16 +68,34 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter {
|
||||
protected updatePackageJson(
|
||||
entryPoint: EntryPoint, formatProperties: EntryPointJsonProperty[],
|
||||
ngccFolder: AbsoluteFsPath) {
|
||||
const packageJson = entryPoint.packageJson;
|
||||
|
||||
for (const formatProperty of formatProperties) {
|
||||
const formatPath = join(entryPoint.path, packageJson[formatProperty] !);
|
||||
const newFormatPath = join(ngccFolder, relative(entryPoint.package, formatPath));
|
||||
const newFormatProperty = formatProperty + '_ivy_ngcc';
|
||||
(packageJson as any)[newFormatProperty] = relative(entryPoint.path, newFormatPath);
|
||||
if (formatProperties.length === 0) {
|
||||
// No format properties need updating.
|
||||
return;
|
||||
}
|
||||
|
||||
this.fs.writeFile(
|
||||
join(entryPoint.path, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
const packageJson = entryPoint.packageJson;
|
||||
const packageJsonPath = join(entryPoint.path, 'package.json');
|
||||
|
||||
// All format properties point to the same format-path.
|
||||
const oldFormatProp = formatProperties[0] !;
|
||||
const oldFormatPath = packageJson[oldFormatProp] !;
|
||||
const oldAbsFormatPath = join(entryPoint.path, oldFormatPath);
|
||||
const newAbsFormatPath = join(ngccFolder, relative(entryPoint.package, oldAbsFormatPath));
|
||||
const newFormatPath = relative(entryPoint.path, newAbsFormatPath);
|
||||
|
||||
// Update all properties in `package.json` (both in memory and on disk).
|
||||
const update = this.pkgJsonUpdater.createUpdate();
|
||||
|
||||
for (const formatProperty of formatProperties) {
|
||||
if (packageJson[formatProperty] !== oldFormatPath) {
|
||||
throw new Error(
|
||||
`Unable to update '${packageJsonPath}': Format properties ` +
|
||||
`(${formatProperties.join(', ')}) map to more than one format-path.`);
|
||||
}
|
||||
|
||||
update.addChange([`${formatProperty}_ivy_ngcc`], newFormatPath);
|
||||
}
|
||||
|
||||
update.writeChanges(packageJsonPath, packageJson);
|
||||
}
|
||||
}
|
||||
|
160
packages/compiler-cli/ngcc/src/writing/package_json_updater.ts
Normal file
160
packages/compiler-cli/ngcc/src/writing/package_json_updater.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
Reference in New Issue
Block a user