diff --git a/addon/ng2/utilities/change.ts b/addon/ng2/utilities/change.ts new file mode 100644 index 000000000000..4e6191952875 --- /dev/null +++ b/addon/ng2/utilities/change.ts @@ -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; + + // 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 { + 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 { + 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 { + 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}`); + }); + } +} diff --git a/tests/acceptance/change.spec.ts b/tests/acceptance/change.spec.ts new file mode 100644 index 000000000000..aaed20037394 --- /dev/null +++ b/tests/acceptance/change.spec.ts @@ -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"'); + }); + }); + }); +});