Skip to content

Commit 73a278d

Browse files
committed
feat: add starter code for newroutes command
'newroute-utility.ts' provides typescript utility functions to be used in the new generate router command 'blueprints/routes/*' creates a 'routes.ts' file when the newroutes command is run and 'route.ts' doesn't exit 'newroute.ts' is a command that currently only creates 'routes.ts' as explained above.
1 parent 8372755 commit 73a278d

File tree

7 files changed

+721
-1
lines changed

7 files changed

+721
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default [];

addon/ng2/blueprints/routes/index.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const dynamicPathParser = require('../../utilities/dynamic-path-parser');
2+
const Blueprint = require('ember-cli/lib/models/blueprint');
3+
const getFiles = Blueprint.prototype.files;
4+
const path = require('path');
5+
const fs = require('fs');
6+
const configureMain = require('../../utilities/newroute-utilities').configureMain;
7+
8+
module.exports = {
9+
description: 'Generates a route/guard and template',
10+
11+
files: function(){
12+
var fileList = getFiles.call(this);
13+
fileList = fileList.filter(p => p.indexOf('routes') !== -1);
14+
return fileList;
15+
},
16+
17+
fileMapTokens: function(options){
18+
return {
19+
__path__: () => {
20+
return 'src';
21+
}
22+
};
23+
},
24+
25+
afterInstall: function(options){
26+
const mainFile = path.join(this.project.root, 'src/main.ts');
27+
const routes = 'routes';
28+
const routesFile = './routes';
29+
return configureMain(mainFile, routes, routesFile);
30+
}
31+
}

addon/ng2/commands/newroute.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
'use strict';
2+
3+
import * as path from 'path';
4+
import * as fs from 'fs';
5+
import * as Promise from 'ember-cli/lib/ext/promise';
6+
import * as Blueprint from 'ember-cli/lib/models/blueprint';
7+
import * as EmberGenerateCommand from 'ember-cli/lib/commands/generate';
8+
import * as SilentError from 'silent-error';
9+
import * as _ from 'lodash';
10+
11+
var EOL = require('os').EOL;
12+
const chalk = require('chalk');
13+
14+
const newRouteCommand = EmberGenerateCommand.extend({
15+
name: 'newroute',
16+
description: 'generates a new router',
17+
works: 'insideProject',
18+
availableOptions : [ ],
19+
20+
beforeRun: function(rawArgs) {
21+
if (!rawArgs.length) {
22+
return;
23+
}
24+
25+
EmberGenerateCommand.prototype.printDetailedHelp = function (options) {
26+
var blueprintList = fs.readdirSync(path.join(__dirname, '..', 'blueprints'));
27+
var blueprints = blueprintList
28+
.filter(bp => bp.indexOf('routes') !== -1)
29+
.map(bp => Blueprint.load(path.join(__dirname, '..', 'blueprints', bp)));
30+
31+
var output = '';
32+
blueprints
33+
.forEach(function (bp) {
34+
output += bp.printBasicHelp(false) + EOL;
35+
});
36+
this.ui.writeLine(chalk.cyan(' Available blueprints'));
37+
this.ui.writeLine(output);
38+
};
39+
return EmberGenerateCommand.prototype.beforeRun.apply(this, arguments);
40+
},
41+
42+
run: function(commandOptions, rawArgs){
43+
var blueprintName = rawArgs[0];
44+
45+
if (!blueprintName) {
46+
return Promise.reject(new SilentError('The `ember generate` command requires a ' +
47+
'blueprint name to be specified. ' +
48+
'For more details, use `ember help`.'));
49+
}
50+
if (blueprintName !== 'routes'){
51+
return Promise.reject(new SilentError(`Unknown blueprint name ${blueprintName} for newroute. ` +
52+
`Try the routes blueprint`))
53+
}
54+
55+
var Task = this.tasks.GenerateFromBlueprint;
56+
var task = new Task({
57+
ui: this.ui,
58+
analytics: this.analytics,
59+
project: this.project,
60+
testing: this.testing,
61+
settings: this.settings
62+
});
63+
64+
var taskArgs = {
65+
args: rawArgs
66+
};
67+
68+
if (this.settings && this.settings.usePods && !commandOptions.classic) {
69+
commandOptions.pod = !commandOptions.pod;
70+
}
71+
72+
var taskOptions = _.merge(taskArgs, commandOptions || {});
73+
74+
if (this.project.initializeAddons) {
75+
this.project.initializeAddons();
76+
}
77+
if (fs.existsSync(path.join(this.project.root, '/src/routes.ts'))){
78+
return Promise.resolve();
79+
}
80+
return task.run(taskOptions);
81+
}
82+
});
83+
84+
module.exports = newRouteCommand;
85+
module.exports.overrideCore = true;

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+
};
+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import * as ts from 'typescript';
2+
import * as fs from 'fs';
3+
import * as edit from './change';
4+
5+
/**
6+
* Adds provideRouter configuration to the main file (import and bootstrap) if
7+
* main file hasn't been already configured, else it has no effect.
8+
*
9+
* @param (mainFile) path to main.ts in ng project
10+
* @param (routesName) exported name for the routes array from routesFile
11+
* @param (routesFile)
12+
*/
13+
export function configureMain(mainFile: string, routesName: string, routesFile: string): Promise<void>{
14+
return insertImport(mainFile, 'provideRouter', '@angular/router')
15+
.then(() => {
16+
return insertImport(mainFile, routesName, routesFile, true);
17+
}).then(() => {
18+
let rootNode = ts.createSourceFile(mainFile, fs.readFileSync(mainFile).toString(),
19+
ts.ScriptTarget.ES6, true);
20+
// get ExpressionStatements from the top level syntaxList of the sourceFile
21+
let bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(node => {
22+
// get bootstrap expressions
23+
return node.kind === ts.SyntaxKind.ExpressionStatement &&
24+
node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap';
25+
});
26+
// printAll(bootstrapNodes[0].getChildAt(0).getChildAt(2).getChildAt(2));
27+
if (bootstrapNodes.length !== 1){
28+
return Promise.reject(new Error(`Did not bootstrap provideRouter in ${mainFile} because of multiple or no bootstrap calls`));
29+
}
30+
let bootstrapNode = bootstrapNodes[0].getChildAt(0);
31+
let isBootstraped = findNodes(bootstrapNode, ts.SyntaxKind.Identifier).map(_ => _.text).indexOf('provideRouter') !== -1;
32+
// console.log(bootstrapNode.getChildAt(2).getChildAt(2).getChildAt(1)
33+
// .getChildren().map(n => ts.SyntaxKind[n.kind]));
34+
if (isBootstraped) {
35+
return Promise.resolve();
36+
}
37+
// if bracket exitst already, add configuration template,
38+
// otherwise, insert into bootstrap parens
39+
var fallBackPos: number, configurePathsTemplate: string, separator: string, syntaxListNodes: any;
40+
let bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers
41+
42+
if ( bootstrapProviders ) {
43+
syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren();
44+
fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral
45+
separator = syntaxListNodes.length === 0 ? '' : ', ';
46+
configurePathsTemplate = `provideRouter(${routesName})`;
47+
} else {
48+
fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral
49+
syntaxListNodes = bootstrapNode.getChildAt(2).getChildren();
50+
configurePathsTemplate = `, [provideRouter(${routesName})]`;
51+
separator = '';
52+
}
53+
54+
return insertAfterLastOccurence(syntaxListNodes, separator, configurePathsTemplate,
55+
mainFile, fallBackPos);
56+
});
57+
}
58+
59+
export function removeRouteFromParent(){
60+
61+
}
62+
63+
export function findParentRouteFile(){
64+
65+
}
66+
67+
export function addRoutesToParent(){
68+
69+
}
70+
71+
/**
72+
* Add Import `import { symbolName } from fileName` if the import doesn't exit
73+
* already. Assumes fileToEdit can be resolved and accessed.
74+
* @param fileToEdit (file we want to add import to)
75+
* @param symbolName (item to import)
76+
* @param fileName (path to the file)
77+
* @param isDefault (if true, import follows style for importing default exports)
78+
*/
79+
80+
export function insertImport(fileToEdit: string, symbolName: string,
81+
fileName: string, isDefault=false): Promise<void> {
82+
let rootNode = ts.createSourceFile(fileToEdit, fs.readFileSync(fileToEdit).toString(),
83+
ts.ScriptTarget.ES6, true);
84+
let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
85+
86+
// get nodes that map to import statements from the file fileName
87+
let relevantImports = allImports.filter(node => {
88+
// StringLiteral of the ImportDeclaration is the import file (fileName in this case).
89+
let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral)
90+
.map(n => (<ts.StringLiteralTypeNode>n).text);
91+
return importFiles.filter(file => file === fileName).length === 1;
92+
});
93+
94+
if (relevantImports.length > 0) {
95+
96+
var importsAsterisk: boolean = false;
97+
// imports from import file
98+
let imports: ts.Node[] = [];
99+
relevantImports.forEach(n => {
100+
Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier));
101+
if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) {
102+
importsAsterisk = true;
103+
}
104+
});
105+
106+
// if imports * from fileName, don't add symbolName
107+
if (importsAsterisk) {
108+
return Promise.resolve();
109+
}
110+
111+
let importTextNodes = imports.filter(n => (<ts.Identifier>n).text === symbolName);
112+
113+
// insert import if it's not there
114+
if (importTextNodes.length === 0) {
115+
let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos ||
116+
findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos;
117+
return insertAfterLastOccurence(imports, ', ', symbolName, fileToEdit, fallbackPos);
118+
}
119+
return Promise.resolve();
120+
}
121+
122+
// no such import declaration exists
123+
let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral).filter(n => n.text === 'use strict');
124+
let fallbackPos: number = 0;
125+
if(useStrict.length > 0){
126+
fallbackPos = useStrict[0].end;
127+
}
128+
let open = isDefault ? '' : '{ ';
129+
let close = isDefault ? '' : ' }';
130+
return insertAfterLastOccurence(allImports, ';\n', `import ${open}${symbolName}${close} from '${fileName}'`,
131+
fileToEdit, fallbackPos, ts.SyntaxKind.StringLiteral);
132+
};
133+
134+
/**
135+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
136+
* @param node
137+
* @param kind (a valid index of ts.SyntaxKind enum, eg ts.SyntaxKind.ImportDeclaration)
138+
* @return all nodes of kind kind, or [] if none is found
139+
*/
140+
export function findNodes (node: ts.Node, kind: number, arr: ts.Node[] = []): ts.Node[]{
141+
if (node) {
142+
if(node.kind === kind){
143+
arr.push(node);
144+
}
145+
node.getChildren().forEach(child => findNodes(child, kind, arr));
146+
}
147+
return arr;
148+
}
149+
150+
/**
151+
* @param nodes (nodes to sort)
152+
* @return (nodes sorted by their position from the source file
153+
* or [] if nodes is empty)
154+
*/
155+
export function sortNodesByPosition(nodes: ts.Node[]): ts.Node[]{
156+
if (nodes) {
157+
return nodes.sort((first, second) => {return first.pos - second.pos});
158+
}
159+
return [];
160+
}
161+
162+
/**
163+
*
164+
* Insert toInsert after the last occurence of ts.SyntaxKind[nodes[i].kind]
165+
* or after the last of occurence of syntaxKind if the last occurence is a sub child
166+
* of ts.SyntaxKind[nodes[i].kind]
167+
* @param nodes (insert after the last occurence of nodes)
168+
* @param toInsert (string to insert)
169+
* @param separator (separator between existing text that comes before
170+
* the new text and toInsert)
171+
* @param file (file to write the changes to)
172+
* @param fallbackPos (position to insert if toInsert happens to be the first occurence)
173+
* @param syntaxKind (the ts.SyntaxKind of the last subchild of the last
174+
* occurence of nodes, after which we want to insert)
175+
* @param shallow (if true, don't look for the deepest occurence. In this case, it may not insert
176+
* after the last occurence)
177+
* @throw Error if toInsert is first occurence but fall back is not set
178+
*/
179+
export function insertAfterLastOccurence(nodes: ts.Node[], separator: string, toInsert:string, file: string,
180+
fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Promise<void> {
181+
var lastItem = sortNodesByPosition(nodes).pop();
182+
183+
if (syntaxKind) {
184+
lastItem = sortNodesByPosition(findNodes(lastItem, syntaxKind)).pop();
185+
}
186+
if (!lastItem && fallbackPos == undefined) {
187+
return Promise.reject(new Error(`tried to insert ${toInsert} as first occurence with no fallback position`));
188+
}
189+
let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos;
190+
let editFile = new edit.InsertChange(file, lastItemPosition, separator + toInsert);
191+
return editFile.apply();
192+
}
193+
194+
function printAll(node, d=0){
195+
let text = node.text ? `-----> ${node.text}` : '';
196+
console.log(new Array(d).join('####'), ts.SyntaxKind[node.kind], text);
197+
d++;
198+
node.getChildren().forEach(_ => printAll(_, d));
199+
}

0 commit comments

Comments
 (0)