Skip to content

Commit 2cf8406

Browse files
ashoktamangEmmanuelAzuh
authored andcommitted
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
1 parent 58c9b1a commit 2cf8406

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

addon/ng2/utilities/change.ts

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
3+
import * as Promise from 'ember-cli/lib/ext/promise';
4+
import fs = require('fs');
5+
6+
const readFile = Promise.denodeify(fs.readFile);
7+
const writeFile = Promise.denodeify(fs.writeFile);
8+
9+
export interface Change {
10+
11+
apply(): Promise<void>;
12+
13+
// The file this change should be applied to. Some changes might not apply to
14+
// a file (maybe the config).
15+
path: string | null;
16+
17+
// The order this change should be applied. Normally the position inside the file.
18+
// Changes are applied from the bottom of a file to the top.
19+
order: number;
20+
21+
// The description of this change. This will be outputted in a dry or verbose run.
22+
description: string;
23+
}
24+
25+
/**
26+
* Will add text to the source code.
27+
*/
28+
export class InsertChange implements Change {
29+
30+
const order: number;
31+
const description: string;
32+
33+
constructor(
34+
public path: string,
35+
private pos: number,
36+
private toAdd: string,
37+
) {
38+
if (pos < 0) {
39+
throw new Error('Negative positions are invalid');
40+
}
41+
this.description = `Inserted ${toAdd} into position ${pos} of ${path}`;
42+
this.order = pos;
43+
}
44+
45+
/**
46+
* This method does not insert spaces if there is none in the original string.
47+
*/
48+
apply(): Promise<any> {
49+
return readFile(this.path, 'utf8').then(content => {
50+
let prefix = content.substring(0, this.pos);
51+
let suffix = content.substring(this.pos);
52+
return writeFile(this.path, `${prefix}${this.toAdd}${suffix}`);
53+
});
54+
}
55+
}
56+
57+
/**
58+
* Will remove text from the source code.
59+
*/
60+
export class RemoveChange implements Change {
61+
62+
const order: number;
63+
const description: string;
64+
65+
constructor(
66+
public path: string,
67+
private pos: number,
68+
private toRemove: string) {
69+
if (pos < 0) {
70+
throw new Error('Negative positions are invalid');
71+
}
72+
this.description = `Removed ${toRemove} into position ${pos} of ${path}`;
73+
this.order = pos;
74+
}
75+
76+
apply(): Promise<any> {
77+
return readFile(this.path, 'utf8').then(content => {
78+
let prefix = content.substring(0, this.pos);
79+
let suffix = content.substring(this.pos + this.toRemove.length);
80+
// TODO: throw error if toRemove doesn't match removed string.
81+
return writeFile(this.path, `${prefix}${suffix}`);
82+
});
83+
}
84+
}
85+
86+
/**
87+
* Will replace text from the source code.
88+
*/
89+
export class ReplaceChange implements Change {
90+
91+
const order: number;
92+
const description: string;
93+
94+
constructor(
95+
public path: string,
96+
private pos: number,
97+
private oldText: string,
98+
private newText: string) {
99+
if (pos < 0) {
100+
throw new Error('Negative positions are invalid');
101+
}
102+
this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`;
103+
this.order = pos;
104+
}
105+
106+
apply(): Promise<any> {
107+
return readFile(this.path, 'utf8').then(content => {
108+
let prefix = content.substring(0, this.pos);
109+
let suffix = content.substring(this.pos + this.oldText.length);
110+
// TODO: throw error if oldText doesn't match removed string.
111+
return writeFile(this.path, `${prefix}${this.newText}${suffix}`);
112+
});
113+
}
114+
}

tests/acceptance/change.spec.ts

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict';
2+
3+
// This needs to be first so fs module can be mocked correctly.
4+
let mockFs = require('mock-fs');
5+
6+
import {expect} from 'chai';
7+
import {InsertChange, RemoveChange, ReplaceChange} from '../../addon/ng2/utilities/change';
8+
import fs = require('fs');
9+
10+
let path = require('path');
11+
let Promise = require('ember-cli/lib/ext/promise');
12+
13+
const readFile = Promise.denodeify(fs.readFile);
14+
15+
describe('Change', () => {
16+
let sourcePath = 'src/app/my-component';
17+
18+
beforeEach(() => {
19+
let mockDrive = {
20+
'src/app/my-component': {
21+
'add-file.txt': 'hello',
22+
'remove-replace-file.txt': 'import * as foo from "./bar"',
23+
'replace-file.txt': 'import { FooComponent } from "./baz"'
24+
}
25+
};
26+
mockFs(mockDrive);
27+
});
28+
afterEach(() => {
29+
mockFs.restore();
30+
});
31+
32+
describe('InsertChange', () => {
33+
let sourceFile = path.join(sourcePath, 'add-file.txt');
34+
35+
it('adds text to the source code', () => {
36+
let changeInstance = new InsertChange(sourceFile, 6, ' world!');
37+
return changeInstance
38+
.apply()
39+
.then(() => readFile(sourceFile, 'utf8'))
40+
.then(contents => {
41+
expect(contents).to.equal('hello world!');
42+
});
43+
});
44+
it('fails for negative position', () => {
45+
expect(() => new InsertChange(sourceFile, -6, ' world!')).to.throw(Error);
46+
});
47+
it('adds nothing in the source code if empty string is inserted', () => {
48+
let changeInstance = new InsertChange(sourceFile, 6, '');
49+
return changeInstance
50+
.apply()
51+
.then(() => readFile(sourceFile, 'utf8'))
52+
.then(contents => {
53+
expect(contents).to.equal('hello');
54+
});
55+
});
56+
});
57+
58+
describe('RemoveChange', () => {
59+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
60+
61+
it('removes given text from the source code', () => {
62+
let changeInstance = new RemoveChange(sourceFile, 9, 'as foo');
63+
return changeInstance
64+
.apply()
65+
.then(() => readFile(sourceFile, 'utf8'))
66+
.then(contents => {
67+
expect(contents).to.equal('import * from "./bar"');
68+
});
69+
});
70+
it('fails for negative position', () => {
71+
expect(() => new RemoveChange(sourceFile, -6, ' world!')).to.throw(Error);
72+
});
73+
it('does not change the file if told to remove empty string', () => {
74+
let changeInstance = new RemoveChange(sourceFile, 9, '');
75+
return changeInstance
76+
.apply()
77+
.then(() => readFile(sourceFile, 'utf8'))
78+
.then(contents => {
79+
expect(contents).to.equal('import * as foo from "./bar"');
80+
});
81+
});
82+
});
83+
84+
describe('ReplaceChange', () => {
85+
it('replaces the given text in the source code', () => {
86+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
87+
let changeInstance = new ReplaceChange(sourceFile, 7, '* as foo', '{ fooComponent }');
88+
return changeInstance
89+
.apply()
90+
.then(() => readFile(sourceFile, 'utf8'))
91+
.then(contents => {
92+
expect(contents).to.equal('import { fooComponent } from "./bar"');
93+
});
94+
});
95+
it('fails for negative position', () => {
96+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
97+
expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).to.throw(Error);
98+
});
99+
it('adds string to the position of an empty string', () => {
100+
let sourceFile = path.join(sourcePath, 'replace-file.txt');
101+
let changeInstance = new ReplaceChange(sourceFile, 9, '', 'BarComponent, ');
102+
return changeInstance
103+
.apply()
104+
.then(() => readFile(sourceFile, 'utf8'))
105+
.then(contents => {
106+
expect(contents).to.equal('import { BarComponent, FooComponent } from "./baz"');
107+
});
108+
});
109+
it('removes the given string only if an empty string to add is given', () => {
110+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
111+
let changeInstance = new ReplaceChange(sourceFile, 9, ' as foo', '');
112+
return changeInstance
113+
.apply()
114+
.then(() => readFile(sourceFile, 'utf8'))
115+
.then(contents => {
116+
expect(contents).to.equal('import * from "./bar"');
117+
});
118+
});
119+
});
120+
});

0 commit comments

Comments
 (0)