Skip to content

Commit 53256fc

Browse files
committed
fix(@angular-devkit/schematics): support VirtualTree/HostTree interop
1 parent 4f3021f commit 53256fc

File tree

6 files changed

+141
-104
lines changed

6 files changed

+141
-104
lines changed

packages/angular_devkit/schematics/src/sink/dryrun_spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
// tslint:disable:no-implicit-dependencies
9-
import { normalize, virtualFs } from '@angular-devkit/core';
9+
import { Path, normalize, virtualFs } from '@angular-devkit/core';
1010
import { toArray } from 'rxjs/operators';
11-
import { FileSystemCreateTree, FileSystemTree } from '../tree/filesystem';
11+
import { HostCreateTree, HostTree } from '../tree/host-tree';
1212
import { optimize } from '../tree/static';
1313
import { DryRunSink } from './dryrun';
1414

@@ -22,7 +22,7 @@ const host = new virtualFs.test.TestHost({
2222

2323
describe('DryRunSink', () => {
2424
it('works when creating everything', done => {
25-
const tree = new FileSystemCreateTree(host);
25+
const tree = new HostCreateTree(host);
2626

2727
tree.create('/test', 'testing 1 2');
2828
const recorder = tree.beginUpdate('/test');
@@ -31,7 +31,9 @@ describe('DryRunSink', () => {
3131
tree.overwrite('/hello', 'world');
3232

3333
const files = ['/hello', '/sub/directory/file2', '/sub/file1', '/test'];
34-
expect(tree.files.sort()).toEqual(files.map(normalize));
34+
const treeFiles: Path[] = [];
35+
tree.visit(path => treeFiles.push(path));
36+
expect(treeFiles.sort()).toEqual(files.map(normalize));
3537

3638
const sink = new DryRunSink(new virtualFs.SimpleMemoryHost());
3739
sink.reporter.pipe(toArray())
@@ -49,7 +51,7 @@ describe('DryRunSink', () => {
4951
});
5052

5153
it('works with root', done => {
52-
const tree = new FileSystemTree(host);
54+
const tree = new HostTree(host);
5355

5456
tree.create('/test', 'testing 1 2');
5557
const recorder = tree.beginUpdate('/test');
@@ -58,7 +60,9 @@ describe('DryRunSink', () => {
5860
tree.overwrite('/hello', 'world');
5961

6062
const files = ['/hello', '/sub/directory/file2', '/sub/file1', '/test'];
61-
expect(tree.files.sort()).toEqual(files.map(normalize));
63+
const treeFiles: Path[] = [];
64+
tree.visit(path => treeFiles.push(path));
65+
expect(treeFiles.sort()).toEqual(files.map(normalize));
6266

6367
// Need to create this file on the filesystem, otherwise the commit phase will fail.
6468
const outputHost = new virtualFs.SimpleMemoryHost();

packages/angular_devkit/schematics/src/sink/host_spec.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
*/
88
// tslint:disable:no-implicit-dependencies
99
import { normalize, virtualFs } from '@angular-devkit/core';
10-
import { FileSystemTree, HostSink } from '@angular-devkit/schematics';
10+
import { HostSink } from '@angular-devkit/schematics';
1111
import { fileBufferToString } from '../../../core/src/virtual-fs/host';
12-
import { FileSystemCreateTree } from '../tree/filesystem';
12+
import { HostCreateTree, HostTree } from '../tree/host-tree';
1313
import { optimize } from '../tree/static';
1414

1515

@@ -20,22 +20,24 @@ describe('FileSystemSink', () => {
2020
'/sub/directory/file2': '',
2121
'/sub/file1': '',
2222
});
23-
const tree = new FileSystemCreateTree(host);
23+
const tree = new HostCreateTree(host);
2424

2525
tree.create('/test', 'testing 1 2');
2626
const recorder = tree.beginUpdate('/test');
2727
recorder.insertLeft(8, 'testing ');
2828
tree.commitUpdate(recorder);
2929

3030
const files = ['/hello', '/sub/directory/file2', '/sub/file1', '/test'];
31-
expect(tree.files).toEqual(files.map(normalize));
31+
const treeFiles: string[] = [];
32+
tree.visit(path => treeFiles.push(path));
33+
expect(treeFiles.sort()).toEqual(files);
3234

3335
const outputHost = new virtualFs.test.TestHost();
3436
const sink = new HostSink(outputHost);
3537
sink.commit(optimize(tree))
3638
.toPromise()
3739
.then(() => {
38-
const tmpFiles = outputHost.files;
40+
const tmpFiles = outputHost.files.sort();
3941
expect(tmpFiles as string[]).toEqual(files);
4042
expect(outputHost.sync.read(normalize('/test')).toString())
4143
.toBe('testing testing 1 2');
@@ -51,7 +53,7 @@ describe('FileSystemSink', () => {
5153
'/sub/directory/file2': '/sub/directory/file2',
5254
'/sub/file1': '/sub/file1',
5355
});
54-
const tree = new FileSystemCreateTree(host);
56+
const tree = new HostCreateTree(host);
5557

5658
const outputHost = new virtualFs.test.TestHost();
5759
const sink = new HostSink(outputHost);
@@ -64,7 +66,7 @@ describe('FileSystemSink', () => {
6466
const host = new virtualFs.test.TestHost({
6567
'/file0': '/file0',
6668
});
67-
const tree = new FileSystemTree(host);
69+
const tree = new HostTree(host);
6870
tree.rename('/file0', '/file1');
6971

7072
const sink = new HostSink(host);
@@ -82,7 +84,7 @@ describe('FileSystemSink', () => {
8284
const host = new virtualFs.test.TestHost({
8385
'/sub/directory/file2': '',
8486
});
85-
const tree = new FileSystemTree(host);
87+
const tree = new HostTree(host);
8688
tree.rename('/sub/directory/file2', '/another-directory/file2');
8789

8890
const sink = new HostSink(host);
@@ -99,7 +101,7 @@ describe('FileSystemSink', () => {
99101
const host = new virtualFs.test.TestHost({
100102
'/file0': 'world',
101103
});
102-
const tree = new FileSystemTree(host);
104+
const tree = new HostTree(host);
103105
tree.delete('/file0');
104106
tree.create('/file0', 'hello');
105107

@@ -116,9 +118,14 @@ describe('FileSystemSink', () => {
116118
const host = new virtualFs.test.TestHost({
117119
'/file0': 'world',
118120
});
119-
const tree = new FileSystemTree(host);
121+
const tree = new HostTree(host);
122+
120123
tree.rename('/file0', '/file1');
124+
expect(tree.exists('/file0')).toBeFalsy();
125+
expect(tree.exists('/file1')).toBeTruthy();
126+
121127
tree.create('/file0', 'hello');
128+
expect(tree.exists('/file0')).toBeTruthy();
122129

123130
const sink = new HostSink(host);
124131
sink.commit(optimize(tree))

packages/angular_devkit/schematics/src/tree/host-tree.ts

Lines changed: 72 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import {
1616
normalize,
1717
virtualFs,
1818
} from '@angular-devkit/core';
19-
import { ReadonlyHost } from '../../../core/src/virtual-fs/host';
20-
import { CordHostRecord } from '../../../core/src/virtual-fs/host/record';
2119
import {
2220
ContentHasMutatedException,
2321
FileAlreadyExistException,
@@ -95,9 +93,10 @@ export class HostDirEntry implements DirEntry {
9593

9694

9795
export class HostTree implements Tree {
98-
private _id = _uniqueId++;
96+
private readonly _id = --_uniqueId;
9997
private _record: virtualFs.CordHost;
10098
private _recordSync: virtualFs.SyncDelegateHost;
99+
private _ancestry = new Set<number>();
101100

102101
private _dirCache = new Map<Path, HostDirEntry>();
103102

@@ -116,79 +115,28 @@ export class HostTree implements Tree {
116115
}
117116

118117
protected _willCreate(path: Path) {
119-
let current: ReadonlyHost = this._record;
120-
while (current && current != this._backend) {
121-
if (!(current instanceof virtualFs.CordHost)) {
122-
break;
123-
}
124-
125-
if (current.willCreate(path)) {
126-
return true;
127-
}
128-
129-
current = current.backend;
130-
}
131-
132-
return false;
118+
return this._record.willCreate(path);
133119
}
134-
protected _willOverwrite(path: Path) {
135-
let current: ReadonlyHost = this._record;
136-
while (current && current != this._backend) {
137-
if (!(current instanceof virtualFs.CordHost)) {
138-
break;
139-
}
140120

141-
if (current.willOverwrite(path)) {
142-
return true;
143-
}
144-
145-
current = current.backend;
146-
}
147-
148-
return false;
121+
protected _willOverwrite(path: Path) {
122+
return this._record.willOverwrite(path);
149123
}
150-
protected _willDelete(path: Path) {
151-
let current: ReadonlyHost = this._record;
152-
while (current && current != this._backend) {
153-
if (!(current instanceof virtualFs.CordHost)) {
154-
break;
155-
}
156124

157-
if (current.willDelete(path)) {
158-
return true;
159-
}
160-
161-
current = current.backend;
162-
}
163-
164-
return false;
125+
protected _willDelete(path: Path) {
126+
return this._record.willDelete(path);
165127
}
166-
protected _willRename(path: Path) {
167-
let current: ReadonlyHost = this._record;
168-
while (current && current != this._backend) {
169-
if (!(current instanceof virtualFs.CordHost)) {
170-
break;
171-
}
172-
173-
if (current.willRename(path)) {
174-
return true;
175-
}
176128

177-
current = current.backend;
178-
}
179-
180-
return false;
129+
protected _willRename(path: Path) {
130+
return this._record.willRename(path);
181131
}
182132

183-
184133
branch(): Tree {
185-
// Freeze our own records, and swap. This is so the branch and this Tree don't share the same
186-
// history anymore.
187-
const record = this._record;
188-
this._record = new virtualFs.CordHost(record);
189-
this._recordSync = new virtualFs.SyncDelegateHost(this._record);
134+
const branchedTree = new HostTree(this._backend);
135+
branchedTree._record = this._record.clone();
136+
branchedTree._recordSync = new virtualFs.SyncDelegateHost(branchedTree._record);
137+
branchedTree._ancestry = new Set(this._ancestry).add(this._id);
190138

191-
return new HostTree(record);
139+
return branchedTree;
192140
}
193141

194142
merge(other: Tree, strategy: MergeStrategy = MergeStrategy.Default): void {
@@ -197,6 +145,12 @@ export class HostTree implements Tree {
197145
return;
198146
}
199147

148+
if (other instanceof HostTree && other._ancestry.has(this._id)) {
149+
// Workaround for merging a branch back into one of its ancestors
150+
// More complete branch point tracking is required to avoid
151+
strategy |= MergeStrategy.Overwrite;
152+
}
153+
200154
const creationConflictAllowed =
201155
(strategy & MergeStrategy.AllowCreationConflict) == MergeStrategy.AllowCreationConflict;
202156
const overwriteConflictAllowed =
@@ -205,15 +159,17 @@ export class HostTree implements Tree {
205159
(strategy & MergeStrategy.AllowOverwriteConflict) == MergeStrategy.AllowDeleteConflict;
206160

207161
other.actions.forEach(action => {
208-
if (action.id === this._id) {
209-
return;
210-
}
211-
212162
switch (action.kind) {
213163
case 'c': {
214164
const { path, content } = action;
215165

216166
if ((this._willCreate(path) || this._willOverwrite(path))) {
167+
const existingContent = this.read(path);
168+
if (existingContent && content.equals(existingContent)) {
169+
// Identical outcome; no action required
170+
return;
171+
}
172+
217173
if (!creationConflictAllowed) {
218174
throw new MergeConflictException(path);
219175
}
@@ -228,21 +184,41 @@ export class HostTree implements Tree {
228184

229185
case 'o': {
230186
const { path, content } = action;
187+
if (this._willDelete(path) && !overwriteConflictAllowed) {
188+
throw new MergeConflictException(path);
189+
}
231190

232191
// Ignore if content is the same (considered the same change).
233-
if (this._willOverwrite(path) && !overwriteConflictAllowed) {
234-
throw new MergeConflictException(path);
192+
if (this._willOverwrite(path)) {
193+
const existingContent = this.read(path);
194+
if (existingContent && content.equals(existingContent)) {
195+
// Identical outcome; no action required
196+
return;
197+
}
198+
199+
if (!overwriteConflictAllowed) {
200+
throw new MergeConflictException(path);
201+
}
235202
}
236203
// We use write here as merge validation has already been done, and we want to let
237204
// the CordHost do its job.
238-
this._record.overwrite(path, content as {} as virtualFs.FileBuffer).subscribe();
205+
this._record.write(path, content as {} as virtualFs.FileBuffer).subscribe();
239206

240207
return;
241208
}
242209

243210
case 'r': {
244211
const { path, to } = action;
212+
if (this._willDelete(path)) {
213+
throw new MergeConflictException(path);
214+
}
215+
245216
if (this._willRename(path)) {
217+
if (this._record.willRenameTo(path, to)) {
218+
// Identical outcome; no action required
219+
return;
220+
}
221+
246222
// No override possible for renaming.
247223
throw new MergeConflictException(path);
248224
}
@@ -253,9 +229,16 @@ export class HostTree implements Tree {
253229

254230
case 'd': {
255231
const { path } = action;
256-
if (this._willDelete(path) && !deleteConflictAllowed) {
232+
if (this._willDelete(path)) {
233+
// TODO: This should technically check the content (e.g., hash on delete)
234+
// Identical outcome; no action required
235+
return;
236+
}
237+
238+
if (!this.exists(path) && !deleteConflictAllowed) {
257239
throw new MergeConflictException(path);
258240
}
241+
259242
this._recordSync.delete(path);
260243

261244
return;
@@ -372,16 +355,7 @@ export class HostTree implements Tree {
372355
get actions(): Action[] {
373356
// Create a list of all records until we hit our original backend. This is to support branches
374357
// that diverge from each others.
375-
const allRecords: CordHostRecord[] = [...this._record.records()];
376-
let current = this._record.backend;
377-
while (current != this._backend) {
378-
if (!(current instanceof virtualFs.CordHost)) {
379-
break;
380-
}
381-
382-
allRecords.unshift(...current.records());
383-
current = current.backend;
384-
}
358+
const allRecords = [...this._record.records()];
385359

386360
return clean(
387361
allRecords
@@ -426,3 +400,17 @@ export class HostTree implements Tree {
426400
);
427401
}
428402
}
403+
404+
export class HostCreateTree extends HostTree {
405+
constructor(host: virtualFs.ReadonlyHost) {
406+
super();
407+
408+
const tempHost = new HostTree(host);
409+
tempHost.visit(path => {
410+
const content = tempHost.read(path);
411+
if (content) {
412+
this.create(path, content);
413+
}
414+
});
415+
}
416+
}

0 commit comments

Comments
 (0)