Skip to content

Commit

Permalink
feat: add file system utilities for 'upgrade' process
Browse files Browse the repository at this point in the history
'fs-promise.ts' is a utility file for asynchronous read/write operations
'change.ts' implements some change interfaces as specified by the upgrade design doc linked below:
https://github.com/hansl/angular-cli/blob/7ea3e78ff3d899d5277aac5dfeeece4056d0efe3/docs/design/upgrade.md
  • Loading branch information
ashoktamang committed Jun 22, 2016
1 parent e5180a4 commit 327f649
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
114 changes: 114 additions & 0 deletions addon/ng2/utilities/change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use strict';

import * as Promise from 'ember-cli/lib/ext/promise';
import fs = require('fs');

const readFile = Promise.denodeify(fs.readFile);
const writeFile = Promise.denodeify(fs.writeFile);

export interface Change {

apply(): Promise<void>;

// The file this change should be applied to. Some changes might not apply to
// a file (maybe the config).
path: string | null;

// The order this change should be applied. Normally the position inside the file.
// Changes are applied from the bottom of a file to the top.
order: number;

// The description of this change. This will be outputted in a dry or verbose run.
description: string;
}

/**
* Will add text to the source code.
*/
export class InsertChange implements Change {

const order: number;
const description: string;

constructor(
public path: string,
private pos: number,
private toAdd: string,
) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
this.order = pos;
}

/**
* This method does not insert spaces if there is none in the original string.
*/
apply(): Promise<any> {
return readFile(this.path, 'utf8').then(content => {
let prefix = content.substring(0, this.pos);
let suffix = content.substring(this.pos);
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
});
}
}

/**
* Will remove text from the source code.
*/
export class RemoveChange implements Change {

const order: number;
const description: string;

constructor(
public path: string,
private pos: number,
private toRemove: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
this.order = pos;
}

apply(): Promise<any> {
return readFile(this.path, 'utf8').then(content => {
let prefix = content.substring(0, this.pos);
let suffix = content.substring(this.pos + this.toRemove.length);
// TODO: throw error if toRemove doesn't match removed string.
return writeFile(this.path, `${prefix}${suffix}`);
});
}
}

/**
* Will replace text from the source code.
*/
export class ReplaceChange implements Change {

const order: number;
const description: string;

constructor(
public path: string,
private pos: number,
private oldText: string,
private newText: string) {
if (pos < 0) {
throw new Error('Negative positions are invalid');
}
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
this.order = pos;
}

apply(): Promise<any> {
return readFile(this.path, 'utf8').then(content => {
let prefix = content.substring(0, this.pos);
let suffix = content.substring(this.pos + this.oldText.length);
// TODO: throw error if oldText doesn't match removed string.
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
});
}
}
120 changes: 120 additions & 0 deletions tests/acceptance/change.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use strict';

// This needs to be first so fs module can be mocked correctly.
let mockFs = require('mock-fs');

import {expect} from 'chai';
import {InsertChange, RemoveChange, ReplaceChange} from '../../addon/ng2/utilities/change';
import fs = require('fs');

let path = require('path');
let Promise = require('ember-cli/lib/ext/promise');

const readFile = Promise.denodeify(fs.readFile);

describe('Change', () => {
let sourcePath = 'src/app/my-component';

beforeEach(() => {
let mockDrive = {
'src/app/my-component': {
'add-file.txt': 'hello',
'remove-replace-file.txt': 'import * as foo from "./bar"',
'replace-file.txt': 'import { FooComponent } from "./baz"'
}
};
mockFs(mockDrive);
});
afterEach(() => {
mockFs.restore();
});

describe('InsertChange', () => {
let sourceFile = path.join(sourcePath, 'add-file.txt');

it('adds text to the source code', () => {
let changeInstance = new InsertChange(sourceFile, 6, ' world!');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('hello world!');
});
});
it('fails for negative position', () => {
expect(() => new InsertChange(sourceFile, -6, ' world!')).to.throw(Error);
});
it('adds nothing in the source code if empty string is inserted', () => {
let changeInstance = new InsertChange(sourceFile, 6, '');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('hello');
});
});
});

describe('RemoveChange', () => {
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');

it('removes given text from the source code', () => {
let changeInstance = new RemoveChange(sourceFile, 9, 'as foo');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('import * from "./bar"');
});
});
it('fails for negative position', () => {
expect(() => new RemoveChange(sourceFile, -6, ' world!')).to.throw(Error);
});
it('does not change the file if told to remove empty string', () => {
let changeInstance = new RemoveChange(sourceFile, 9, '');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('import * as foo from "./bar"');
});
});
});

describe('ReplaceChange', () => {
it('replaces the given text in the source code', () => {
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
let changeInstance = new ReplaceChange(sourceFile, 7, '* as foo', '{ fooComponent }');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('import { fooComponent } from "./bar"');
});
});
it('fails for negative position', () => {
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).to.throw(Error);
});
it('adds string to the position of an empty string', () => {
let sourceFile = path.join(sourcePath, 'replace-file.txt');
let changeInstance = new ReplaceChange(sourceFile, 9, '', 'BarComponent, ');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('import { BarComponent, FooComponent } from "./baz"');
});
});
it('removes the given string only if an empty string to add is given', () => {
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
let changeInstance = new ReplaceChange(sourceFile, 9, ' as foo', '');
return changeInstance
.apply()
.then(() => readFile(sourceFile, 'utf8'))
.then(contents => {
expect(contents).to.equal('import * from "./bar"');
});
});
});
});

0 comments on commit 327f649

Please sign in to comment.