angular-docs-cn/packages/compiler-cli/ngcc/test/writing/package_json_updater_spec.ts
George Kalpakas 93ffc67bfb fix(ngcc): update package.json deterministically (#34870)
Ngcc adds properties to the `package.json` files of the entry-points it
processes to mark them as processed for a format and point to the
created Ivy entry-points (in case of `--create-ivy-entry-points`). When
running ngcc in parallel mode (which is the default for the standalone
ngcc command), multiple formats can be processed simultaneously for the
same entry-point and the order of completion is not deterministic.

Previously, ngcc would append new properties at the end of the target
object in `package.json` as soon as the format processing was completed.
As a result, the order of properties in the resulting `package.json`
(when processing multiple formats for an entry-point in parallel) was
not deterministic. For tools that use file hashes for caching purposes
(such as Bazel), this lead to a high probability of cache misses.

This commit fixes the problem by ensuring that the position of
properties added to `package.json` files is deterministic and
independent of the order in which each format is processed.

Jira issue: [FW-1801](https://angular-team.atlassian.net/browse/FW-1801)

Fixes #34635

PR Close #34870
2020-01-23 10:16:35 -08:00

289 lines
9.8 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, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers';
import {JsonObject} from '../../src/packages/entry_point';
import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater';
runInEachFileSystem(() => {
describe('DirectPackageJsonUpdater', () => {
let _: typeof absoluteFrom;
let fs: FileSystem;
let updater: PackageJsonUpdater;
// Helpers
const readJson = (path: AbsoluteFsPath) => JSON.parse(fs.readFile(path));
beforeEach(() => {
_ = absoluteFrom;
fs = getFileSystem();
updater = new DirectPackageJsonUpdater(fs);
});
it('should update a `package.json` file on disk', () => {
const jsonPath = _('/foo/package.json');
loadTestFiles([
{name: jsonPath, contents: '{"foo": true, "bar": {"baz": "OK"}}'},
]);
const update = updater.createUpdate().addChange(['foo'], false).addChange(['bar', 'baz'], 42);
// Not updated yet.
expect(readJson(jsonPath)).toEqual({
foo: true,
bar: {baz: 'OK'},
});
update.writeChanges(jsonPath);
// Updated now.
expect(readJson(jsonPath)).toEqual({
foo: false,
bar: {baz: 42},
});
});
it('should update an in-memory representation (if provided)', () => {
const jsonPath = _('/foo/package.json');
loadTestFiles([
{name: jsonPath, contents: '{"foo": true, "bar": {"baz": "OK"}}'},
]);
const pkg = readJson(jsonPath);
const update = updater.createUpdate().addChange(['foo'], false).addChange(['bar', 'baz'], 42);
// Not updated yet.
expect(pkg).toEqual({
foo: true,
bar: {baz: 'OK'},
});
update.writeChanges(jsonPath, pkg);
// Updated now.
expect(pkg).toEqual({
foo: false,
bar: {baz: 42},
});
});
it('should create the `package.json` file, if it does not exist', () => {
const jsonPath = _('/foo/package.json');
expect(fs.exists(jsonPath)).toBe(false);
updater.createUpdate()
.addChange(['foo'], false)
.addChange(['bar', 'baz'], 42)
.writeChanges(jsonPath);
expect(fs.exists(jsonPath)).toBe(true);
expect(readJson(jsonPath)).toEqual({
foo: false,
bar: {baz: 42},
});
});
it('should create any missing ancestor objects', () => {
const jsonPath = _('/foo/package.json');
loadTestFiles([
{name: jsonPath, contents: '{"foo": {}}'},
]);
const pkg = readJson(jsonPath);
updater.createUpdate()
.addChange(['foo', 'bar', 'baz', 'qux'], 'updated')
.writeChanges(jsonPath, pkg);
expect(readJson(jsonPath)).toEqual(pkg);
expect(pkg).toEqual({
foo: {
bar: {
baz: {
qux: 'updated',
},
},
},
});
});
it('should throw, if no changes have been recorded', () => {
const jsonPath = _('/foo/package.json');
expect(() => updater.createUpdate().writeChanges(jsonPath))
.toThrowError(`No changes to write to '${jsonPath}'.`);
});
it('should throw, if a property-path is empty', () => {
const jsonPath = _('/foo/package.json');
expect(() => updater.createUpdate().addChange([], 'missing').writeChanges(jsonPath))
.toThrowError(`Missing property path for writing value to '${jsonPath}'.`);
});
it('should throw, if a property-path points to a non-object intermediate value', () => {
const jsonPath = _('/foo/package.json');
loadTestFiles([
{name: jsonPath, contents: '{"foo": null, "bar": 42, "baz": {"qux": []}}'},
]);
const writeToProp = (propPath: string[]) =>
updater.createUpdate().addChange(propPath, 'updated').writeChanges(jsonPath);
expect(() => writeToProp(['foo', 'child']))
.toThrowError('Property path \'foo.child\' does not point to an object.');
expect(() => writeToProp(['bar', 'child']))
.toThrowError('Property path \'bar.child\' does not point to an object.');
expect(() => writeToProp(['baz', 'qux', 'child']))
.toThrowError('Property path \'baz.qux.child\' does not point to an object.');
});
it('should throw, if trying to re-apply an already applied update', () => {
const update = updater.createUpdate().addChange(['foo'], 'updated');
expect(() => update.writeChanges(_('/foo/package.json'))).not.toThrow();
expect(() => update.writeChanges(_('/foo/package.json')))
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
expect(() => update.writeChanges(_('/bar/package.json')))
.toThrowError('Trying to apply a `PackageJsonUpdate` that has already been applied.');
});
describe('(property positioning)', () => {
// Helpers
const createJsonFile = (jsonObj: JsonObject) => {
const jsonPath = _('/foo/package.json');
loadTestFiles([{name: jsonPath, contents: JSON.stringify(jsonObj)}]);
return jsonPath;
};
const expectJsonEquals = (jsonFilePath: AbsoluteFsPath, jsonObj: JsonObject) =>
expect(fs.readFile(jsonFilePath).trim()).toBe(JSON.stringify(jsonObj, null, 2));
it('should not change property positioning by default', () => {
const jsonPath = createJsonFile({
p2: '2',
p1: {p12: '1.2', p11: '1.1'},
});
updater.createUpdate()
.addChange(['p1', 'p11'], '1.1-updated')
.addChange(['p1', 'p10'], '1.0-added')
.addChange(['p2'], '2-updated')
.addChange(['p0'], '0-added')
.writeChanges(jsonPath);
expectJsonEquals(jsonPath, {
p2: '2-updated',
p1: {p12: '1.2', p11: '1.1-updated', p10: '1.0-added'},
p0: '0-added',
});
});
it('should not change property positioning with `positioning: unimportant`', () => {
const jsonPath = createJsonFile({
p2: '2',
p1: {p12: '1.2', p11: '1.1'},
});
updater.createUpdate()
.addChange(['p1', 'p11'], '1.1-updated', 'unimportant')
.addChange(['p1', 'p10'], '1.0-added', 'unimportant')
.addChange(['p2'], '2-updated', 'unimportant')
.addChange(['p0'], '0-added', 'unimportant')
.writeChanges(jsonPath);
expectJsonEquals(jsonPath, {
p2: '2-updated',
p1: {p12: '1.2', p11: '1.1-updated', p10: '1.0-added'},
p0: '0-added',
});
});
it('should position added/updated properties alphabetically with `positioning: alphabetic`',
() => {
const jsonPath = createJsonFile({
p2: '2',
p1: {p12: '1.2', p11: '1.1'},
});
updater.createUpdate()
.addChange(['p1', 'p11'], '1.1-updated', 'alphabetic')
.addChange(['p1', 'p10'], '1.0-added', 'alphabetic')
.addChange(['p0'], '0-added', 'alphabetic')
.addChange(['p3'], '3-added', 'alphabetic')
.writeChanges(jsonPath);
expectJsonEquals(jsonPath, {
p0: '0-added',
p2: '2',
p1: {p10: '1.0-added', p11: '1.1-updated', p12: '1.2'},
p3: '3-added',
});
});
it('should position added/updated properties correctly with `positioning: {before: ...}`',
() => {
const jsonPath = createJsonFile({
p2: '2',
p1: {p12: '1.2', p11: '1.1'},
});
updater.createUpdate()
.addChange(['p0'], '0-added', {before: 'p1'})
.addChange(['p1', 'p10'], '1.0-added', {before: 'p11'})
.addChange(['p1', 'p12'], '1.2-updated', {before: 'p11'})
.writeChanges(jsonPath);
expectJsonEquals(jsonPath, {
p2: '2',
p0: '0-added',
p1: {p10: '1.0-added', p12: '1.2-updated', p11: '1.1'},
});
// Verify that trying to add before non-existent property, puts updated property at the
// end.
updater.createUpdate()
.addChange(['p3'], '3-added', {before: 'non-existent'})
.addChange(['p1', 'p10'], '1.0-updated', {before: 'non-existent'})
.writeChanges(jsonPath);
expectJsonEquals(jsonPath, {
p2: '2',
p0: '0-added',
p1: {p12: '1.2-updated', p11: '1.1', p10: '1.0-updated'},
p3: '3-added',
});
});
it('should ignore positioning when updating an in-memory representation', () => {
const jsonObj = {
p20: '20',
p10: {p102: '10.2', p101: '10.1'},
};
const jsonPath = createJsonFile(jsonObj);
updater.createUpdate()
.addChange(['p0'], '0-added', 'alphabetic')
.addChange(['p1'], '1-added', 'unimportant')
.addChange(['p2'], '2-added')
.addChange(['p20'], '20-updated', 'alphabetic')
.addChange(['p10', 'p103'], '10.3-added', {before: 'p102'})
.addChange(['p10', 'p102'], '10.2-updated', {before: 'p103'})
.writeChanges(jsonPath, jsonObj);
expect(JSON.stringify(jsonObj)).toBe(JSON.stringify({
p20: '20-updated',
p10: {p102: '10.2-updated', p101: '10.1', p103: '10.3-added'},
p0: '0-added',
p1: '1-added',
p2: '2-added',
}));
});
});
});
});