Skip to content

Commit

Permalink
feat(@angular-devkit/schematics): add UpdateBuffer2 based on magic-st…
Browse files Browse the repository at this point in the history
…ring

This PR adds UpdateBuffer2 which should eventually
replace UpdateBuffer. UpdateBuffer2 internally uses
the magic-string library.
UpdateBuffer and related symbols have been marked
as deprecated.

Closes #21110
  • Loading branch information
kyubisation authored and filipesilva committed Aug 20, 2021
1 parent 8a984f7 commit 0565ed6
Show file tree
Hide file tree
Showing 9 changed files with 313 additions and 14 deletions.
4 changes: 2 additions & 2 deletions goldens/public-api/angular_devkit/schematics/src/index.md
Expand Up @@ -488,13 +488,13 @@ export class HostSink extends SimpleSinkBase {
// (undocumented)
_done(): Observable<void>;
// (undocumented)
protected _filesToCreate: Map<Path, UpdateBuffer>;
protected _filesToCreate: Map<Path, UpdateBufferBase>;
// (undocumented)
protected _filesToDelete: Set<Path>;
// (undocumented)
protected _filesToRename: Set<[Path, Path]>;
// (undocumented)
protected _filesToUpdate: Map<Path, UpdateBuffer>;
protected _filesToUpdate: Map<Path, UpdateBufferBase>;
// (undocumented)
protected _force: boolean;
// (undocumented)
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/schematics/BUILD.bazel
Expand Up @@ -41,6 +41,7 @@ ts_library(
"//packages/angular_devkit/core",
"//packages/angular_devkit/core/node", # TODO: get rid of this for 6.0
"@npm//@types/node",
"@npm//magic-string",
"@npm//rxjs",
],
)
Expand Down
1 change: 1 addition & 0 deletions packages/angular_devkit/schematics/package.json
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@angular-devkit/core": "0.0.0",
"jsonc-parser": "3.0.0",
"magic-string": "0.25.7",
"ora": "5.4.1",
"rxjs": "6.6.7"
}
Expand Down
10 changes: 5 additions & 5 deletions packages/angular_devkit/schematics/src/sink/host.ts
Expand Up @@ -16,14 +16,14 @@ import {
} from 'rxjs';
import { concatMap, reduce } from 'rxjs/operators';
import { CreateFileAction } from '../tree/action';
import { UpdateBuffer } from '../utility/update-buffer';
import { UpdateBufferBase } from '../utility/update-buffer';
import { SimpleSinkBase } from './sink';

export class HostSink extends SimpleSinkBase {
protected _filesToDelete = new Set<Path>();
protected _filesToRename = new Set<[Path, Path]>();
protected _filesToCreate = new Map<Path, UpdateBuffer>();
protected _filesToUpdate = new Map<Path, UpdateBuffer>();
protected _filesToCreate = new Map<Path, UpdateBufferBase>();
protected _filesToUpdate = new Map<Path, UpdateBufferBase>();

constructor(protected _host: virtualFs.Host, protected _force = false) {
super();
Expand Down Expand Up @@ -55,12 +55,12 @@ export class HostSink extends SimpleSinkBase {
}

protected _overwriteFile(path: Path, content: Buffer): Observable<void> {
this._filesToUpdate.set(path, new UpdateBuffer(content));
this._filesToUpdate.set(path, UpdateBufferBase.create(content));

return EMPTY;
}
protected _createFile(path: Path, content: Buffer): Observable<void> {
this._filesToCreate.set(path, new UpdateBuffer(content));
this._filesToCreate.set(path, UpdateBufferBase.create(content));

return EMPTY;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/angular_devkit/schematics/src/tree/recorder.ts
Expand Up @@ -7,17 +7,17 @@
*/

import { ContentHasMutatedException } from '../exception/exception';
import { UpdateBuffer } from '../utility/update-buffer';
import { UpdateBufferBase } from '../utility/update-buffer';
import { FileEntry, UpdateRecorder } from './interface';

export class UpdateRecorderBase implements UpdateRecorder {
protected _path: string;
protected _original: Buffer;
protected _content: UpdateBuffer;
protected _content: UpdateBufferBase;

constructor(entry: FileEntry) {
this._original = Buffer.from(entry.content);
this._content = new UpdateBuffer(entry.content);
this._content = UpdateBufferBase.create(entry.content);
this._path = entry.path;
}

Expand Down
18 changes: 18 additions & 0 deletions packages/angular_devkit/schematics/src/tree/recorder_spec.ts
Expand Up @@ -7,6 +7,7 @@
*/

import { normalize } from '@angular-devkit/core';
import { UpdateBuffer2, UpdateBufferBase } from '../utility/update-buffer';
import { SimpleFileEntry } from './entry';
import { UpdateRecorderBase, UpdateRecorderBom } from './recorder';

Expand All @@ -31,6 +32,23 @@ describe('UpdateRecorderBase', () => {
expect(result.toString()).toBe('Hello beautiful World');
});

it('works with multiple adjacent inserts', () => {
const buffer = Buffer.from('Hello beautiful World');
const entry = new SimpleFileEntry(normalize('/some/path'), buffer);

// TODO: Remove once UpdateBufferBase.create defaults to UpdateBuffer2
spyOn(UpdateBufferBase, 'create').and.callFake(
(originalContent) => new UpdateBuffer2(originalContent),
);

const recorder = new UpdateRecorderBase(entry);
recorder.remove(6, 9);
recorder.insertRight(6, 'amazing');
recorder.insertRight(15, ' and fantastic');
const result = recorder.apply(buffer);
expect(result.toString()).toBe('Hello amazing and fantastic World');
});

it('can create the proper recorder', () => {
const e = new SimpleFileEntry(normalize('/some/path'), Buffer.from('hello'));
expect(UpdateRecorderBase.createFromFileEntry(e) instanceof UpdateRecorderBase).toBe(true);
Expand Down
@@ -0,0 +1,20 @@
/**
* @license
* Copyright Google LLC 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
*/

function isEnabled(variable: string): boolean {
return variable === '1' || variable.toLowerCase() === 'true';
}

function isPresent(variable: string | undefined): variable is string {
return typeof variable === 'string' && variable !== '';
}

// Use UpdateBuffer2, which uses magic-string internally.
// TODO: Switch this for the next major release to use UpdateBuffer2 by default.
const updateBufferV2 = process.env['NG_UPDATE_BUFFER_V2'];
export const updateBufferV2Enabled = isPresent(updateBufferV2) && isEnabled(updateBufferV2);
88 changes: 85 additions & 3 deletions packages/angular_devkit/schematics/src/utility/update-buffer.ts
Expand Up @@ -7,13 +7,16 @@
*/

import { BaseException } from '@angular-devkit/core';
import MagicString from 'magic-string';
import { updateBufferV2Enabled } from './environment-options';
import { LinkedList } from './linked-list';

export class IndexOutOfBoundException extends BaseException {
constructor(index: number, min: number, max = Infinity) {
super(`Index ${index} outside of range [${min}, ${max}].`);
}
}
/** @deprecated Since v13.0 */
export class ContentCannotBeRemovedException extends BaseException {
constructor() {
super(`User tried to remove content that was marked essential.`);
Expand All @@ -26,6 +29,7 @@ export class ContentCannotBeRemovedException extends BaseException {
* it means the content itself was deleted.
*
* @see UpdateBuffer
* @deprecated Since v13.0
*/
export class Chunk {
private _content: Buffer | null;
Expand Down Expand Up @@ -176,6 +180,37 @@ export class Chunk {
}
}

/**
* Base class for an update buffer implementation that allows buffers to be inserted to the _right
* or _left, or deleted, while keeping indices to the original buffer.
*/
export abstract class UpdateBufferBase {
constructor(protected _originalContent: Buffer) {}
abstract get length(): number;
abstract get original(): Buffer;
abstract toString(encoding?: string): string;
abstract generate(): Buffer;
abstract insertLeft(index: number, content: Buffer, assert?: boolean): void;
abstract insertRight(index: number, content: Buffer, assert?: boolean): void;
abstract remove(index: number, length: number): void;

/**
* Creates an UpdateBufferBase instance. Depending on the NG_UPDATE_BUFFER_V2
* environment variable, will either create an UpdateBuffer or an UpdateBuffer2
* instance.
*
* See: https://github.com/angular/angular-cli/issues/21110
*
* @param originalContent The original content of the update buffer instance.
* @returns An UpdateBufferBase instance.
*/
static create(originalContent: Buffer): UpdateBufferBase {
return updateBufferV2Enabled
? new UpdateBuffer2(originalContent)
: new UpdateBuffer(originalContent);
}
}

/**
* An utility class that allows buffers to be inserted to the _right or _left, or deleted, while
* keeping indices to the original buffer.
Expand All @@ -185,12 +220,15 @@ export class Chunk {
*
* Since the Node Buffer structure is non-destructive when slicing, we try to use slicing to create
* new chunks, and always keep chunks pointing to the original content.
*
* @deprecated Since v13.0
*/
export class UpdateBuffer {
export class UpdateBuffer extends UpdateBufferBase {
protected _linkedList: LinkedList<Chunk>;

constructor(protected _originalContent: Buffer) {
this._linkedList = new LinkedList(new Chunk(0, _originalContent.length, _originalContent));
constructor(originalContent: Buffer) {
super(originalContent);
this._linkedList = new LinkedList(new Chunk(0, originalContent.length, originalContent));
}

protected _assertIndex(index: number) {
Expand Down Expand Up @@ -274,3 +312,47 @@ export class UpdateBuffer {
}
}
}

/**
* An utility class that allows buffers to be inserted to the _right or _left, or deleted, while
* keeping indices to the original buffer.
*/
export class UpdateBuffer2 extends UpdateBufferBase {
protected _mutatableContent: MagicString = new MagicString(this._originalContent.toString());

protected _assertIndex(index: number) {
if (index < 0 || index > this._originalContent.length) {
throw new IndexOutOfBoundException(index, 0, this._originalContent.length);
}
}

get length(): number {
return this._mutatableContent.length();
}
get original(): Buffer {
return this._originalContent;
}

toString(): string {
return this._mutatableContent.toString();
}

generate(): Buffer {
return Buffer.from(this.toString());
}

insertLeft(index: number, content: Buffer): void {
this._assertIndex(index);
this._mutatableContent.appendLeft(index, content.toString());
}

insertRight(index: number, content: Buffer): void {
this._assertIndex(index);
this._mutatableContent.appendRight(index, content.toString());
}

remove(index: number, length: number) {
this._assertIndex(index);
this._mutatableContent.remove(index, index + length);
}
}

0 comments on commit 0565ed6

Please sign in to comment.