-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add file system utilities for 'upgrade' process
'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
1 parent
e5180a4
commit 327f649
Showing
2 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"'); | ||
}); | ||
}); | ||
}); | ||
}); |