Skip to content

Commit 01297f4

Browse files
committed
feat(@angular-devkit/schematics): support reading text content directly from a Tree
The schematics `Tree` now contains an additional `readText` method that supports directly reading the content of a file as UTF-8 text. This avoids the need to manually decode a Buffer within a schematic when text content is needed. If a file path does not exist, an exception will be thrown. While this differs from the semantics of `read`, it helps reduce the amount of code needed for common schematic use cases.
1 parent 0441015 commit 01297f4

File tree

7 files changed

+76
-8
lines changed

7 files changed

+76
-8
lines changed

goldens/public-api/angular_devkit/schematics/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ export class DelegateTree implements Tree_2 {
235235
// (undocumented)
236236
read(path: string): Buffer | null;
237237
// (undocumented)
238+
readText(path: string): string;
239+
// (undocumented)
238240
rename(from: string, to: string): void;
239241
// (undocumented)
240242
get root(): DirEntry;
@@ -547,6 +549,8 @@ export class HostTree implements Tree_2 {
547549
// (undocumented)
548550
read(path: string): Buffer | null;
549551
// (undocumented)
552+
readText(path: string): string;
553+
// (undocumented)
550554
rename(from: string, to: string): void;
551555
// (undocumented)
552556
get root(): DirEntry;

packages/angular_devkit/schematics/src/tree/delegate.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export class DelegateTree implements Tree {
3535
read(path: string): Buffer | null {
3636
return this._other.read(path);
3737
}
38+
readText(path: string): string {
39+
return this._other.readText(path);
40+
}
3841
exists(path: string): boolean {
3942
return this._other.exists(path);
4043
}

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

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@angular-devkit/core';
1919
import { EMPTY, Observable } from 'rxjs';
2020
import { concatMap, map, mergeMap } from 'rxjs/operators';
21+
import { TextDecoder } from 'util';
2122
import {
2223
ContentHasMutatedException,
2324
FileAlreadyExistException,
@@ -162,10 +163,10 @@ export class HostTree implements Tree {
162163
return tree._ancestry.has(this._id);
163164
}
164165
if (tree instanceof DelegateTree) {
165-
return this.isAncestorOf(((tree as unknown) as { _other: Tree })._other);
166+
return this.isAncestorOf((tree as unknown as { _other: Tree })._other);
166167
}
167168
if (tree instanceof ScopedTree) {
168-
return this.isAncestorOf(((tree as unknown) as { _base: Tree })._base);
169+
return this.isAncestorOf((tree as unknown as { _base: Tree })._base);
169170
}
170171

171172
return false;
@@ -206,9 +207,9 @@ export class HostTree implements Tree {
206207
throw new MergeConflictException(path);
207208
}
208209

209-
this._record.overwrite(path, (content as {}) as virtualFs.FileBuffer).subscribe();
210+
this._record.overwrite(path, content as {} as virtualFs.FileBuffer).subscribe();
210211
} else {
211-
this._record.create(path, (content as {}) as virtualFs.FileBuffer).subscribe();
212+
this._record.create(path, content as {} as virtualFs.FileBuffer).subscribe();
212213
}
213214

214215
return;
@@ -234,7 +235,7 @@ export class HostTree implements Tree {
234235
}
235236
// We use write here as merge validation has already been done, and we want to let
236237
// the CordHost do its job.
237-
this._record.write(path, (content as {}) as virtualFs.FileBuffer).subscribe();
238+
this._record.write(path, content as {} as virtualFs.FileBuffer).subscribe();
238239

239240
return;
240241
}
@@ -289,6 +290,26 @@ export class HostTree implements Tree {
289290

290291
return entry ? entry.content : null;
291292
}
293+
294+
readText(path: string): string {
295+
const data = this.read(path);
296+
if (data === null) {
297+
throw new FileDoesNotExistException(path);
298+
}
299+
300+
const decoder = new TextDecoder('utf-8', { fatal: true });
301+
302+
try {
303+
// With the `fatal` option enabled, invalid data will throw a TypeError
304+
return decoder.decode(data);
305+
} catch (e) {
306+
if (e instanceof TypeError) {
307+
throw new Error(`Failed to decode "${path}" as UTF-8 text.`);
308+
}
309+
throw e;
310+
}
311+
}
312+
292313
exists(path: string): boolean {
293314
return this._recordSync.isFile(this._normalizePath(path));
294315
}
@@ -337,7 +358,7 @@ export class HostTree implements Tree {
337358
throw new FileDoesNotExistException(p);
338359
}
339360
const c = typeof content == 'string' ? Buffer.from(content) : content;
340-
this._record.overwrite(p, (c as {}) as virtualFs.FileBuffer).subscribe();
361+
this._record.overwrite(p, c as {} as virtualFs.FileBuffer).subscribe();
341362
}
342363
beginUpdate(path: string): UpdateRecorder {
343364
const entry = this.get(path);
@@ -371,7 +392,7 @@ export class HostTree implements Tree {
371392
throw new FileAlreadyExistException(p);
372393
}
373394
const c = typeof content == 'string' ? Buffer.from(content) : content;
374-
this._record.create(p, (c as {}) as virtualFs.FileBuffer).subscribe();
395+
this._record.create(p, c as {} as virtualFs.FileBuffer).subscribe();
375396
}
376397
delete(path: string): void {
377398
this._recordSync.delete(this._normalizePath(path));
@@ -476,7 +497,7 @@ export class FilterHostTree extends HostTree {
476497
return EMPTY;
477498
}
478499

479-
return newBackend.write(path, (content as {}) as virtualFs.FileBuffer);
500+
return newBackend.write(path, content as {} as virtualFs.FileBuffer);
480501
}),
481502
);
482503
};

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ import { FilterHostTree, HostTree } from './host-tree';
1111
import { MergeStrategy } from './interface';
1212

1313
describe('HostTree', () => {
14+
describe('readText', () => {
15+
it('returns text when reading a file that exists', () => {
16+
const tree = new HostTree();
17+
tree.create('/textfile1', 'abc');
18+
tree.create('/textfile2', '123');
19+
expect(tree.readText('/textfile1')).toEqual('abc');
20+
expect(tree.readText('/textfile2')).toEqual('123');
21+
});
22+
23+
it('throws an error when a file does not exist', () => {
24+
const tree = new HostTree();
25+
const path = '/textfile1';
26+
expect(() => tree.readText(path)).toThrowError(`Path "${path}" does not exist.`);
27+
});
28+
29+
it('throws an error when invalid UTF-8 characters are present', () => {
30+
const tree = new HostTree();
31+
const path = '/textfile1';
32+
tree.create(path, Buffer.from([0xff, 0xff, 0xff, 0xff]));
33+
expect(() => tree.readText(path)).toThrowError(`Failed to decode "${path}" as UTF-8 text.`);
34+
});
35+
});
36+
1437
describe('merge', () => {
1538
it('should create files from each tree', () => {
1639
const tree = new HostTree();

packages/angular_devkit/schematics/src/tree/interface.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ export interface Tree {
8383

8484
// Readonly.
8585
read(path: string): Buffer | null;
86+
87+
/**
88+
* Reads a file from the Tree as a UTF-8 encoded text file.
89+
*
90+
* @param path The path of the file to read.
91+
* @returns A string containing the contents of the file.
92+
* @throws {@link FileDoesNotExistException} if the file is not found.
93+
* @throws An error if the file contains invalid UTF-8 characters.
94+
*/
95+
readText(path: string): string;
96+
8697
exists(path: string): boolean;
8798
get(path: string): FileEntry | null;
8899
getDir(path: string): DirEntry;

packages/angular_devkit/schematics/src/tree/null.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export class NullTree implements Tree {
5757
read(_path: string) {
5858
return null;
5959
}
60+
readText(path: string): string {
61+
throw new FileDoesNotExistException(path);
62+
}
6063
get(_path: string) {
6164
return null;
6265
}

packages/angular_devkit/schematics/src/tree/scoped.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export class ScopedTree implements Tree {
113113
read(path: string): Buffer | null {
114114
return this._base.read(this._fullPath(path));
115115
}
116+
readText(path: string): string {
117+
return this._base.readText(this._fullPath(path));
118+
}
116119
exists(path: string): boolean {
117120
return this._base.exists(this._fullPath(path));
118121
}

0 commit comments

Comments
 (0)