Skip to content

Commit 77f6a41

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/schematics): directly use magic-string in update recorder
The schematics `UpdateRecorder` now uses the `magic-string` library directly instead of delegating to another class (`UpdateBuffer`) which now effectively only wraps the `magic-string`. This also allows for improved BOM handling and repeat conversion of strings to and from `Buffer` instances.
1 parent 5a2a963 commit 77f6a41

File tree

2 files changed

+47
-40
lines changed

2 files changed

+47
-40
lines changed

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

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,34 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import MagicString from 'magic-string';
910
import { ContentHasMutatedException } from '../exception/exception';
10-
import { UpdateBufferBase } from '../utility/update-buffer';
11+
import { IndexOutOfBoundException } from '../utility/update-buffer';
1112
import { FileEntry, UpdateRecorder } from './interface';
1213

1314
export class UpdateRecorderBase implements UpdateRecorder {
1415
protected _path: string;
15-
protected _original: Buffer;
16-
protected _content: UpdateBufferBase;
16+
protected content: MagicString;
17+
18+
constructor(
19+
private readonly data: Uint8Array,
20+
path: string,
21+
encoding = 'utf-8',
22+
private readonly bom = false,
23+
) {
24+
let text;
25+
try {
26+
text = new TextDecoder(encoding, { fatal: true, ignoreBOM: false }).decode(data);
27+
} catch (e) {
28+
if (e instanceof TypeError) {
29+
throw new Error(`Failed to decode "${path}" as ${encoding} text.`);
30+
}
31+
32+
throw e;
33+
}
1734

18-
constructor(entry: FileEntry) {
19-
this._original = Buffer.from(entry.content);
20-
this._content = UpdateBufferBase.create(entry.path, entry.content);
21-
this._path = entry.path;
35+
this._path = path;
36+
this.content = new MagicString(text);
2237
}
2338

2439
static createFromFileEntry(entry: FileEntry): UpdateRecorderBase {
@@ -28,62 +43,56 @@ export class UpdateRecorderBase implements UpdateRecorder {
2843

2944
// Check if we're BOM.
3045
if (c0 == 0xef && c1 == 0xbb && c2 == 0xbf) {
31-
return new UpdateRecorderBom(entry);
46+
return new UpdateRecorderBase(entry.content, entry.path, 'utf-8', true);
3247
} else if (c0 === 0xff && c1 == 0xfe) {
33-
return new UpdateRecorderBom(entry);
48+
return new UpdateRecorderBase(entry.content, entry.path, 'utf-16le', true);
3449
} else if (c0 === 0xfe && c1 == 0xff) {
35-
return new UpdateRecorderBom(entry);
50+
return new UpdateRecorderBase(entry.content, entry.path, 'utf-16be', true);
3651
}
3752

38-
return new UpdateRecorderBase(entry);
53+
return new UpdateRecorderBase(entry.content, entry.path);
3954
}
4055

4156
get path() {
4257
return this._path;
4358
}
4459

60+
protected _assertIndex(index: number) {
61+
if (index < 0 || index > this.content.original.length) {
62+
throw new IndexOutOfBoundException(index, 0, this.content.original.length);
63+
}
64+
}
65+
4566
// These just record changes.
4667
insertLeft(index: number, content: Buffer | string): UpdateRecorder {
47-
this._content.insertLeft(index, typeof content == 'string' ? Buffer.from(content) : content);
68+
this._assertIndex(index);
69+
this.content.appendLeft(index, content.toString());
4870

4971
return this;
5072
}
5173

5274
insertRight(index: number, content: Buffer | string): UpdateRecorder {
53-
this._content.insertRight(index, typeof content == 'string' ? Buffer.from(content) : content);
75+
this._assertIndex(index);
76+
this.content.appendRight(index, content.toString());
5477

5578
return this;
5679
}
5780

5881
remove(index: number, length: number): UpdateRecorder {
59-
this._content.remove(index, length);
82+
this._assertIndex(index);
83+
this.content.remove(index, index + length);
6084

6185
return this;
6286
}
6387

6488
apply(content: Buffer): Buffer {
65-
if (!content.equals(this._content.original)) {
89+
if (!content.equals(this.data)) {
6690
throw new ContentHasMutatedException(this.path);
6791
}
6892

69-
return this._content.generate();
70-
}
71-
}
72-
73-
export class UpdateRecorderBom extends UpdateRecorderBase {
74-
constructor(entry: FileEntry, private _delta = 1) {
75-
super(entry);
76-
}
77-
78-
override insertLeft(index: number, content: Buffer | string) {
79-
return super.insertLeft(index + this._delta, content);
80-
}
81-
82-
override insertRight(index: number, content: Buffer | string) {
83-
return super.insertRight(index + this._delta, content);
84-
}
93+
// Schematics only support writing UTF-8 text
94+
const result = Buffer.from((this.bom ? '\uFEFF' : '') + this.content.toString(), 'utf-8');
8595

86-
override remove(index: number, length: number) {
87-
return super.remove(index + this._delta, length);
96+
return result;
8897
}
8998
}

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88

99
import { normalize } from '@angular-devkit/core';
1010
import { SimpleFileEntry } from './entry';
11-
import { UpdateRecorderBase, UpdateRecorderBom } from './recorder';
11+
import { UpdateRecorderBase } from './recorder';
1212

1313
describe('UpdateRecorderBase', () => {
1414
it('works for simple files', () => {
1515
const buffer = Buffer.from('Hello World');
1616
const entry = new SimpleFileEntry(normalize('/some/path'), buffer);
1717

18-
const recorder = new UpdateRecorderBase(entry);
18+
const recorder = UpdateRecorderBase.createFromFileEntry(entry);
1919
recorder.insertLeft(5, ' beautiful');
2020
const result = recorder.apply(buffer);
2121
expect(result.toString()).toBe('Hello beautiful World');
@@ -25,7 +25,7 @@ describe('UpdateRecorderBase', () => {
2525
const buffer = Buffer.from('Hello World');
2626
const entry = new SimpleFileEntry(normalize('/some/path'), buffer);
2727

28-
const recorder = new UpdateRecorderBase(entry);
28+
const recorder = UpdateRecorderBase.createFromFileEntry(entry);
2929
recorder.insertRight(5, ' beautiful');
3030
const result = recorder.apply(buffer);
3131
expect(result.toString()).toBe('Hello beautiful World');
@@ -35,7 +35,7 @@ describe('UpdateRecorderBase', () => {
3535
const buffer = Buffer.from('Hello beautiful World');
3636
const entry = new SimpleFileEntry(normalize('/some/path'), buffer);
3737

38-
const recorder = new UpdateRecorderBase(entry);
38+
const recorder = UpdateRecorderBase.createFromFileEntry(entry);
3939
recorder.remove(6, 9);
4040
recorder.insertRight(6, 'amazing');
4141
recorder.insertRight(15, ' and fantastic');
@@ -46,13 +46,11 @@ describe('UpdateRecorderBase', () => {
4646
it('can create the proper recorder', () => {
4747
const e = new SimpleFileEntry(normalize('/some/path'), Buffer.from('hello'));
4848
expect(UpdateRecorderBase.createFromFileEntry(e) instanceof UpdateRecorderBase).toBe(true);
49-
expect(UpdateRecorderBase.createFromFileEntry(e) instanceof UpdateRecorderBom).toBe(false);
5049
});
5150

5251
it('can create the proper recorder (bom)', () => {
5352
const eBom = new SimpleFileEntry(normalize('/some/path'), Buffer.from('\uFEFFhello'));
5453
expect(UpdateRecorderBase.createFromFileEntry(eBom) instanceof UpdateRecorderBase).toBe(true);
55-
expect(UpdateRecorderBase.createFromFileEntry(eBom) instanceof UpdateRecorderBom).toBe(true);
5654
});
5755

5856
it('supports empty files', () => {
@@ -71,7 +69,7 @@ describe('UpdateRecorderBom', () => {
7169
const buffer = Buffer.from('\uFEFFHello World');
7270
const entry = new SimpleFileEntry(normalize('/some/path'), buffer);
7371

74-
const recorder = new UpdateRecorderBom(entry);
72+
const recorder = UpdateRecorderBase.createFromFileEntry(entry);
7573
recorder.insertLeft(5, ' beautiful');
7674
const result = recorder.apply(buffer);
7775
expect(result.toString()).toBe('\uFEFFHello beautiful World');

0 commit comments

Comments
 (0)