Skip to content

Commit b9f77dc

Browse files
committed
feat: add utilities for typescript ast
'newroute-utility.ts' provides typescript utility functions to be used in the new generate router command
1 parent 1690a82 commit b9f77dc

File tree

6 files changed

+656
-1
lines changed

6 files changed

+656
-1
lines changed

addon/ng2/utilities/change.ts

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

addon/ng2/utilities/dynamic-path-parser.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ module.exports = function dynamicPathParser(project, entityName) {
5555
parsedPath.appRoot = appRoot
5656

5757
return parsedPath;
58-
};
58+
};
+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
import * as edit from './change';
4+
5+
export function removeRouteFromParent(){
6+
7+
}
8+
9+
export function findParentRouteFile(){
10+
11+
}
12+
13+
export function addRoutesToParent(){
14+
15+
}
16+
17+
/**
18+
* Add Import `import { symbolName } from fileName` if the import doesn't exit
19+
* already. Assumes fileToEdit can be resolved and accessed.
20+
* @param fileToEdit (file we want to add import to)
21+
* @param symbolName (item to import)
22+
* @param fileName (path to the file)
23+
*/
24+
25+
export function insertImport(fileToEdit: string, symbolName: string, fileName: string): Promise<void> {
26+
let rootNode = ts.createSourceFile(fileToEdit, fs.readFileSync(fileToEdit).toString(),
27+
ts.ScriptTarget.ES6, true);
28+
let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
29+
30+
// get nodes that map to import statements from the file fileName
31+
let relevantImports = allImports.filter(node => {
32+
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
33+
let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral)
34+
.map(n => (<ts.StringLiteralTypeNode>n).text);
35+
return importFiles.filter(file => file === fileName).length === 1;
36+
});
37+
38+
if (relevantImports.length > 0) {
39+
40+
var importsAsterisk: boolean = false;
41+
// imports from import file
42+
let imports: ts.Node[] = [];
43+
relevantImports.forEach(n => {
44+
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
45+
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
46+
importsAsterisk = true;
47+
}
48+
});
49+
50+
// if imports * from fileName, don't add symbolName
51+
if (importsAsterisk) {
52+
return Promise.resolve();
53+
}
54+
55+
let importTextNodes = imports.filter(n => (<ts.Identifier>n).text === symbolName);
56+
57+
// insert import if it's not there
58+
if (importTextNodes.length === 0) {
59+
let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
60+
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
61+
return insertAfterLastOccurence(imports, ', ', symbolName, fileToEdit, fallbackPos);
62+
}
63+
return Promise.resolve();
64+
}
65+
// no such import declaration exists
66+
let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(n => n.text === 'use strict');
67+
let fallbackPos: number = 0;
68+
if(useStrict.length > 0){
69+
fallbackPos = useStrict[0].end;
70+
}
71+
return insertAfterLastOccurence(allImports, ';\n', 'import { ' + symbolName + ' } from \'' + fileName + '\'',
72+
fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral)
73+
};
74+
75+
/**
76+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
77+
* @param node
78+
* @param kind (a valid index of ts.SyntaxKind enum, eg ts.SyntaxKind.ImportDeclaration)
79+
* @return all nodes of kind kind, or [] if none is found
80+
*/
81+
export function findNodes (node: ts.Node, kind: number, arr: ts.Node[] = []): ts.Node[]{
82+
if (node) {
83+
if(node.kind === kind){
84+
arr.push(node);
85+
}
86+
node.getChildren().forEach(child => findNodes(child, kind, arr));
87+
}
88+
return arr;
89+
}
90+
91+
/**
92+
* @param nodes (nodes to sort)
93+
* @return (nodes sorted by their position from the source file
94+
* or [] if nodes is empty)
95+
*/
96+
export function sortNodesByPosition(nodes: ts.Node[]): ts.Node[]{
97+
if (nodes) {
98+
return nodes.sort((first, second) => {return first.pos - second.pos});
99+
}
100+
return [];
101+
}
102+
103+
/**
104+
*
105+
* Insert toInsert after the last occurence of ts.SyntaxKind[nodes[i].kind]
106+
* or after the last of occurence of syntaxKind if the last occurence is a sub child
107+
* of ts.SyntaxKind[nodes[i].kind]
108+
* @param nodes (insert after the last occurence of nodes)
109+
* @param toInsert (string to insert)
110+
* @param separator (separator between existing text that comes before
111+
* the new text and toInsert)
112+
* @param file (file to write the changes to)
113+
* @param fallbackPos (position to insert if toInsert happens to be the first occurence)
114+
* @param syntaxKind (the ts.SyntaxKind of the last subchild of the last
115+
* occurence of nodes, after which we want to insert)
116+
* @throw Error if toInsert is first occurence but fall back is not set
117+
*/
118+
export function insertAfterLastOccurence(nodes: ts.Node[], separator: string, toInsert:string,
119+
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Promise<void> {
120+
var lastItem = sortNodesByPosition(nodes).pop();
121+
122+
if (syntaxKind) {
123+
lastItem = sortNodesByPosition(findNodes(lastItem, syntaxKind)).pop(;
124+
}
125+
if (!lastItem && fallbackPos === undefined) {
126+
return Promise.reject(new Error(`tried to insert ${toInsert} as first occurence with no fallback position`));
127+
}
128+
let lastItemPosition: number = lastItem? lastItem.end : fallbackPos;
129+
130+
let editFile = new edit.InsertChange(file, lastItemPosition, separator + toInsert);
131+
return editFile.apply();
132+
}
133+

tests/acceptance/change.spec.js

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict';
2+
3+
const expect = require('chai').expect;
4+
let path = require('path');
5+
let mockFs = require('mock-fs');
6+
let change = require('../../addon/ng2/utilities/change');
7+
let fs = require('fs');
8+
let Promise = require('ember-cli/lib/ext/promise');
9+
10+
11+
12+
describe('Change: Testing operations', () => {
13+
const readFile = Promise.denodeify(fs.readFile);
14+
let sourcePath = 'src/app/my-component';
15+
16+
beforeEach(() => {
17+
let mockDrive = {
18+
'src/app/my-component': {
19+
'add-file.txt': 'hello',
20+
'remove-replace-file.txt': 'import * as foo from "./bar"',
21+
'replace-file.txt': 'import { FooComponent } from "./baz"'
22+
}
23+
}
24+
mockFs(mockDrive);
25+
});
26+
27+
afterEach(() => {
28+
mockFs.restore();
29+
});
30+
31+
describe('InsertChange: add string to the source code', () => {
32+
33+
it('should add text to the source code', () => {
34+
let sourceFile = path.join(sourcePath, 'add-file.txt');
35+
expect(fs.existsSync(sourceFile)).to.equal(true);
36+
let changeInstance = new change.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+
45+
it('should append string before any strings in the source code, and hence unexpected output', () => {
46+
let sourceFile = path.join(sourcePath, 'add-file.txt');
47+
expect(fs.existsSync(sourceFile)).to.equal(true);
48+
let changeInstance = new change.InsertChange(sourceFile, -6, ' world!');
49+
return changeInstance
50+
.apply()
51+
.then(() => readFile(sourceFile))
52+
.then(contents => {
53+
expect(contents).to.not.equal('hello world!');
54+
});
55+
})
56+
57+
it('should add nothing in the source code if empty string is inserted', () => {
58+
let sourceFile = path.join(sourcePath, 'add-file.txt');
59+
expect(fs.existsSync(sourceFile)).to.equal(true);
60+
let changeInstance = new change.InsertChange(sourceFile, 6, '');
61+
return changeInstance
62+
.apply()
63+
.then(() => readFile(sourceFile, 'utf8'))
64+
.then(contents => {
65+
expect(contents).to.equal('hello');
66+
});
67+
})
68+
});
69+
70+
describe('RemoveChange: remove string from the source code', () => {
71+
72+
it('should remove given text from the source code', () => {
73+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
74+
expect(fs.existsSync(sourceFile)).to.equal(true);
75+
let changeInstance = new change.RemoveChange(sourceFile, 9, 'as foo');
76+
return changeInstance
77+
.apply()
78+
.then(() => readFile(sourceFile, 'utf8'))
79+
.then(contents => {
80+
expect(contents).to.equal('import * from "./bar"');
81+
});
82+
})
83+
84+
it('should remove nothing in the source code if empty string is to be removed', () => {
85+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
86+
expect(fs.existsSync(sourceFile)).to.equal(true);
87+
let changeInstance = new change.RemoveChange(sourceFile, 9, '');
88+
return changeInstance
89+
.apply()
90+
.then(() => readFile(sourceFile, 'utf8'))
91+
.then(contents => {
92+
expect(contents).to.equal('import * as foo from "./bar"');
93+
});
94+
})
95+
});
96+
97+
describe('ReplaceChange: replace string in the source code', () => {
98+
99+
it('should replace a given text in the source code', () => {
100+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
101+
expect(fs.existsSync(sourceFile)).to.equal(true);
102+
let changeInstance = new change.ReplaceChange(sourceFile, 7, '* as foo', '{ fooComponent }');
103+
return changeInstance
104+
.apply()
105+
.then(() => readFile(sourceFile, 'utf8'))
106+
.then(contents => {
107+
expect(contents).to.equal('import { fooComponent } from "./bar"');
108+
});
109+
})
110+
111+
it('should add string to the position of an empty string', () => {
112+
let sourceFile = path.join(sourcePath, 'replace-file.txt');
113+
expect(fs.existsSync(sourceFile)).to.equal(true);
114+
let changeInstance = new change.ReplaceChange(sourceFile, 9, '', 'BarComponent, ');
115+
return changeInstance
116+
.apply()
117+
.then(() => readFile(sourceFile, 'utf8'))
118+
.then(contents => {
119+
expect(contents).to.equal('import { BarComponent, FooComponent } from "./baz"');
120+
});
121+
})
122+
123+
it('should removes the string in the given postion and add nothing if empty string is to be replaced', () => {
124+
let sourceFile = path.join(sourcePath, 'remove-replace-file.txt');
125+
expect(fs.existsSync(sourceFile)).to.equal(true);
126+
let changeInstance = new change.ReplaceChange(sourceFile, 9, ' as foo', '');
127+
return changeInstance
128+
.apply()
129+
.then(() => readFile(sourceFile, 'utf8'))
130+
.then(contents => {
131+
expect(contents).to.equal('import * from "./bar"');
132+
});
133+
})
134+
});
135+
});

0 commit comments

Comments
 (0)