├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── angular │ └── get-template.js ├── config.js ├── index.js ├── lib │ ├── build-specs.js │ ├── camel-to-dash.js │ ├── components │ │ ├── add-emit-to-outputs.js │ │ ├── component-class.js │ │ ├── component-constructor.js │ │ ├── component-decorator.js │ │ ├── component-enums.js │ │ ├── component-imports.js │ │ ├── component-inputs-and-outputs.js │ │ ├── component-interfaces.js │ │ ├── component-lifecycle-events.js │ │ ├── component-methods.js │ │ ├── component-properties.js │ │ ├── get-bindings.js │ │ ├── get-component-class-name.js │ │ ├── get-controller-class.js │ │ ├── get-input-and-outputs-from-angular-4-component.js │ │ ├── get-lifecycle-events.js │ │ ├── get-template-url.js │ │ ├── get-template.js │ │ └── replace-controller-static-references.js │ ├── get-class.js │ ├── get-constructor.js │ ├── get-enums.js │ ├── get-extended-classes.js │ ├── get-imports.js │ ├── get-interfaces.js │ ├── get-methods.js │ ├── get-mocks.js │ ├── get-named-imports.js │ ├── get-output-file-path.js │ ├── get-properties.js │ ├── get-related-spec-file-path.js │ ├── get-source-file.js │ ├── get-specs.js │ ├── get-unit-tests.js │ ├── make-directories-in-path.js │ ├── parser.js │ ├── pretty-print.js │ ├── process-string-replacements.js │ ├── process-template.js │ ├── read-file.js │ ├── remove-$injects.js │ ├── remove-from-constructor.js │ ├── remove-imports.js │ ├── render-source-file.js │ ├── render-template.js │ ├── services │ │ ├── service-class.js │ │ ├── service-constructor.js │ │ ├── service-http.js │ │ ├── service-imports.js │ │ ├── service-interfaces.js │ │ ├── service-methods.js │ │ └── service-properties.js │ ├── update-references.js │ ├── variables.js │ └── write-file.js ├── stubs │ └── index.js ├── templates │ ├── component-test.hbs │ └── service-test.hbs └── tools │ ├── process-components.js │ ├── process-files.js │ ├── process-service-specs.js │ ├── process-services.js │ └── process-templates.js └── tests ├── add-emit-to-outputs.test.js ├── component-constructor.test.js ├── component-decorator.test.js ├── component-imports.test.js ├── component-inputs-and-outputs.test.js ├── component-interfaces.test.js ├── component-methods.test.js ├── component-properties.test.js ├── data ├── account.component.ts ├── app.component.ts ├── events-manager.utility.ts ├── reddit.service.ts └── search-input.component.ts ├── get-bindings.test.js ├── get-constructor.test.js ├── get-named-imports.test.js ├── get-template-url.test.js ├── process-string-replacements.test.js ├── process-template.test.js ├── service-http.test.js ├── service-imports.test.js ├── utils └── get-ast.js └── variables.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module" 8 | }, 9 | "extends": "eslint:recommended", 10 | "ecmaFeatures": { 11 | "jsx": true, 12 | "experimentalObjectRestSpread": true, 13 | "modules": true 14 | }, 15 | "rules": { 16 | 17 | "no-alert": 2, 18 | "no-caller": 2, 19 | "no-bitwise": 0, 20 | "no-console": 0, 21 | "no-debugger": 2, 22 | "no-empty": 2, 23 | "no-eval": 1, 24 | "no-ex-assign": 2, 25 | "no-extra-boolean-cast": 0, 26 | "no-irregular-whitespace":0, 27 | "no-floating-decimal": 0, 28 | "no-implied-eval": 2, 29 | "no-with": 2, 30 | "no-fallthrough": 2, 31 | "no-unreachable": 2, 32 | "no-undef-init": 2, 33 | "no-octal": 2, 34 | "no-obj-calls": 2, 35 | "no-new-wrappers": 2, 36 | "no-new": 2, 37 | "no-new-func": 2, 38 | "no-native-reassign": 2, 39 | "no-plusplus": 0, 40 | "no-delete-var": 2, 41 | "no-return-assign": 2, 42 | "no-new-object": 2, 43 | "no-label-var": 2, 44 | "no-ternary": 0, 45 | "no-self-compare": 0, 46 | "no-undef":0, 47 | "smarter-eqeqeq": 0, 48 | "brace-style": 0, 49 | "camelcase": 0, 50 | "curly": 2, 51 | "dot-notation": 0, 52 | "eqeqeq": 1, 53 | "new-parens": 2, 54 | "guard-for-in": 0, 55 | "radix": 0, 56 | "new-cap": 2, 57 | "quote-props": 0, 58 | "semi": 2, 59 | "use-isnan": 2, 60 | "quotes": [2, "single"], 61 | "max-params": [0, 3], 62 | "max-statements": [0, 10], 63 | "complexity": [0, 11], 64 | "wrap-iife": 0, 65 | "no-multi-str": 2 66 | } 67 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | results.ts 5 | .tmp 6 | upgrade/ 7 | 8 | .vscode 9 | .dev 10 | ro -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eric Tsai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angularjs-to-angular 2 | CLI that takes in angularjs files and outputs angular. 3 | 4 | Originally written by Grubhub engineers to convert the consumer web application from AngularJS to Angular. This will not work for you out of the box because every AngularJS application is unique. We've provided the tools necessary so you can put some sample files and unit test them. 5 | 6 | You are also encouraged to hack at the conversion script to get the results you need. 7 | 8 | ## Getting started ## 9 | * git clone git@github.com:erictsai6/angularjs-to-angular.git 10 | * cd angularjs-to-angular 11 | * npm install 12 | * npm run components 13 | * This will convert the test component files under tests/data/ and output to upgrade/ 14 | * npm run services 15 | * This will convert the test service files under tests/data/ and output to upgrade/ 16 | * npm run serviceSpecs 17 | * This will convert the test service spec files under tests/data/ and output to upgrade/ 18 | * npm run templates 19 | * This will convert the test template files under tests/data/ and output to upgrade/ 20 | * npm test 21 | * Executes the unit tests using AvaJS 22 | 23 | ## How to develop with this script ## 24 | The entry point of the script is under src/index.js where it will take commandline arguments that will accept blobs for component, service, service spec and template conversions. Each specific tool can be found under src/tools/ file. 25 | 26 | ### Components ### 27 | src/tools/process-components.js 28 | #### Places to hack #### 29 | * imports.get() - reads the AST and based on the code determines what should be imported at the top of the new file 30 | * decorator.get() - reads the AST and based on how the template is defined will set up the component decorator with template or templateUrl. 31 | * removeFromConstructor - takes in a list of arguments that you want to remove from the constructor because they no longer exist in Angular 32 | * processStringReplacements - Has a large 2 x N array with the first column being a REGEX match and the second column representing what you want to replace it with. 33 | * updateReferences - Useful for updating the import references to maintain consistent import paths. 34 | 35 | ### Services ### 36 | src/tools/process-services.js 37 | #### Places to hack #### 38 | * imports.get() - reads the AST and based on the code determines what should be imported at the top of the new file 39 | * removeFromConstructor - takes in a list of arguments that you want to remove from the constructor because they no longer exist in Angular 40 | * processStringReplacements - Has a large 2 x N array with the first column being a REGEX match and the second column representing what you want to replace it with. 41 | * updateReferences - Useful for updating the import references to maintain consistent import paths. 42 | 43 | 44 | ### Templates ### 45 | src/lib/process-template.js 46 | #### Places to hack #### 47 | * The first section revolves around using cheerio (similar to JQuery) to search for specific elements and add or remove attributes 48 | * The second section is a series of REGEX replace statements 49 | 50 | ## Running this on your source code ## 51 | * cd angularjs-to-angular 52 | * npm link 53 | * cd 54 | * npm link angularjs-to-angular 55 | * git clone https://github.com/gdi2290/angular-starter upgrade 56 | * This will create a upgrade directory with an angular starter project 57 | * ./node_modules/.bin/angularjs-to-angular -c="src/**/*.component.ts" 58 | * Your new source code will be under the upgrade directory. 59 | * Good luck! 60 | 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angularjs-to-angular", 3 | "version": "1.0.0", 4 | "description": "cli that takes in angularjs files and outputs angular", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "components": "node src/index.js -c 'tests/data/**/*component.{js,ts}'", 8 | "templates": "node src/index.js -t 'tests/data/**/*.tpl.html'", 9 | "services": "node src/index.js -s 'tests/data/**/!(*routes|*module|*component|*spec).{js,ts}'", 10 | "serviceSpecs": "node src/index.js --serviceSpecs 'tests/data/**/*{service,model}.{js,ts}'", 11 | "test": "ava" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/erictsai6/angularjs-to-angular.git" 16 | }, 17 | "keywords": [ 18 | "angularjs", 19 | "angular", 20 | "converter" 21 | ], 22 | "author": "Team Umami", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/erictsai6/angularjs-to-angular/issues" 26 | }, 27 | "homepage": "https://github.com/erictsai6/angularjs-to-angular#readme", 28 | "dependencies": { 29 | "cheerio": "^1.0.0-rc.1", 30 | "glob": "^7.1.2", 31 | "handlebars": "^4.0.10", 32 | "lodash.sortby": "^4.7.0", 33 | "lodash.uniq": "^4.5.0", 34 | "lodash.uniqueid": "^4.0.1", 35 | "node-glob": "^1.2.0", 36 | "prettier": "^1.4.4", 37 | "rimraf": "^2.6.1", 38 | "typescript": "^2.5.0-dev.20170616", 39 | "typescript-formatter": "^5.2.0", 40 | "yargs": "^8.0.1" 41 | }, 42 | "devDependencies": { 43 | "ava": "^0.19.1", 44 | "eslint": "^3.19.0", 45 | "js-beautify": "^1.6.14", 46 | "rimraf": "^2.6.1" 47 | }, 48 | "bin": "src/index.js" 49 | } 50 | -------------------------------------------------------------------------------- /src/angular/get-template.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, existsSync } = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function (file) { 5 | const text = readFileSync(file, 'UTF-8'); 6 | 7 | if (/templateUrl:/.test(text)) { 8 | const templateFile = /templateUrl: '(.*)'/.exec(text)[1]; 9 | const templatePath = path.join(process.cwd(), path.dirname(file), templateFile); 10 | if (existsSync(templatePath)) { 11 | return readFileSync(templatePath, 'UTF-8'); 12 | } 13 | } 14 | else if (/template:/.test(text)) { 15 | return /template: `(.|\n)*`/m.exec(text); 16 | } 17 | return ''; 18 | }; -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | 2 | const outputRoot = './upgrade'; 3 | 4 | module.exports = { 5 | outputRoot 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const glob = require('glob'); 4 | let converted = false; 5 | 6 | const argv = require('yargs') // Reference -> https://github.com/yargs/yargs 7 | .option('components', { 8 | alias: 'c', 9 | describe: 'Processes the components. Provide a glob in quotes.', 10 | }) 11 | .option('services', { 12 | alias: 's', 13 | describe: 'Processes the services. Provide a glob in quotes.', 14 | }) 15 | .option('serviceSpecs', { 16 | describe: 'Processes the non component spec files', 17 | }) 18 | .option('templates', { 19 | alias: 't', 20 | describe: 'Processes the templates. Provide a glob in quote.', 21 | }) 22 | .option('copy', { 23 | alias: 'y', 24 | describe: 'Copies over the targeted file. Provide a glob in quotes.', 25 | }) 26 | .help('h').argv; 27 | 28 | /* Process Components */ 29 | if (argv.components) { 30 | const globPath = path.join(process.cwd(), argv.components); 31 | processComponents(globPath); 32 | converted = true; 33 | } 34 | 35 | /* Process Services */ 36 | if (argv.services) { 37 | const globPath = path.join(process.cwd(), argv.services); 38 | processServices(globPath); 39 | converted = true; 40 | } 41 | 42 | /* Process Non Component Spec Files */ 43 | if (argv.serviceSpecs) { 44 | const globPath = path.join(process.cwd(), argv.serviceSpecs); 45 | processServiceSpecs(globPath); 46 | converted = true; 47 | } 48 | 49 | /* Process Templates */ 50 | if (argv.templates) { 51 | const globPath = path.join(process.cwd(), argv.templates); 52 | const componentsGlobPath = argv.components 53 | ? path.join(process.cwd(), argv.components) 54 | : null; 55 | processTemplates(globPath, componentsGlobPath); 56 | converted = true; 57 | } 58 | 59 | /* Process files that you want to copy */ 60 | if (argv.copy) { 61 | const globPath = path.join(process.cwd(), argv.copy); 62 | processFiles(globPath); 63 | converted = true; 64 | } 65 | 66 | if (converted) { 67 | console.log('Conversion completed'); 68 | process.exit(0); 69 | } else { 70 | console.log('No arguments provided, nothing converted'); 71 | process.exit(1); 72 | } 73 | 74 | function processComponents(globPath) { 75 | const tool = require('./tools/process-components'); 76 | const files = glob.sync(globPath); 77 | tool(files); 78 | } 79 | 80 | function processServices(globPath) { 81 | const tool = require('./tools/process-services'); 82 | const files = glob.sync(globPath); 83 | tool(files); 84 | } 85 | 86 | function processServiceSpecs(globPath) { 87 | const tool = require('./tools/process-service-specs'); 88 | const files = glob.sync(globPath); 89 | tool(files); 90 | } 91 | 92 | function processTemplates(globPath, componentGlobPath) { 93 | const tool = require('./tools/process-templates'); 94 | const files = glob.sync(globPath); 95 | const componentFiles = glob.sync(componentGlobPath); 96 | tool(files, componentFiles); 97 | } 98 | 99 | function processFiles(globPath) { 100 | const tool = require('./tools/process-files'); 101 | const files = glob.sync(globPath); 102 | tool(files); 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/build-specs.js: -------------------------------------------------------------------------------- 1 | const getSpecs = require('./get-specs'); 2 | 3 | module.exports = function (file) { 4 | let results = ''; 5 | const specs = getSpecs(file); 6 | visit(specs[0]); 7 | 8 | function visit(node) { 9 | let addClose = false; 10 | if (!node) { 11 | return; 12 | } 13 | 14 | if (node.type === 'describe' && !//.test(node.suite)) { 15 | results += `\n${getIndent(node)}xdescribe('${escapeColons(node.suite)}', () => {\n`; 16 | addClose = true; 17 | } 18 | 19 | if (node.type === 'it') { 20 | results += `\n${getIndent(node)}xit('${escapeColons(node.test)}', () => {`; 21 | results += commentOutCode(node.code); 22 | addClose = true; 23 | } 24 | 25 | if (node.type === 'beforeEach') { 26 | results += `\n${getIndent(node)}beforeEach(() => {`; 27 | results += commentOutCode(node.code); 28 | addClose = true; 29 | } 30 | 31 | if (node.type === 'afterEach') { 32 | results += `\n${getIndent(node)}afterEach(() => {`; 33 | results += commentOutCode(node.code); 34 | addClose = true; 35 | } 36 | 37 | node.children.forEach(n => visit(n)); 38 | 39 | if (addClose) { 40 | results += `\n${getIndent(node)}});\n`; 41 | } 42 | } 43 | 44 | return results; 45 | 46 | }; 47 | 48 | function escapeColons(s) { 49 | if (!s) { 50 | return ''; 51 | } 52 | 53 | // Hack because of prettier choking 54 | s = s.replace(/constructor/g, 'constructxr'); 55 | 56 | return s.replace(/'/g, '\\\''); 57 | } 58 | 59 | function getIndent(node) { 60 | let d = node.depth - 4; 61 | if (d < 0) { 62 | d = 0; 63 | } 64 | 65 | return new Array(d).join(' '); 66 | } 67 | 68 | function commentOutCode(code) { 69 | if (!code) { 70 | return ''; 71 | } 72 | 73 | let lines = code 74 | .replace(/\/*.*\*\//g, '\n') 75 | .replace(/\n+/, '') 76 | .split('\n'); 77 | 78 | let firstIndentLine = lines.find(x => /^ +/.test(x)); 79 | let firstIndent = /^( +)\w/.exec(firstIndentLine); 80 | firstIndent = firstIndent ? firstIndent[1] : ''; 81 | 82 | return ` 83 | ${firstIndent} /* 84 | ${lines.map(x => firstIndent + x).join('\n')} 85 | ${firstIndent} */ 86 | 87 | `; 88 | 89 | } -------------------------------------------------------------------------------- /src/lib/camel-to-dash.js: -------------------------------------------------------------------------------- 1 | module.exports = function(str) { 2 | return str.replace(/([A-Z])/g, g => `-${g[0].toLowerCase()}`); 3 | }; 4 | -------------------------------------------------------------------------------- /src/lib/components/add-emit-to-outputs.js: -------------------------------------------------------------------------------- 1 | const getBindings = require('./get-bindings'); 2 | 3 | module.exports = function (ast, code) { 4 | 5 | const bindings = getBindings(ast); 6 | const outputBindings = bindings.filter(x => x.type === 'output' || x.type === 'two-way'); 7 | 8 | for (b of outputBindings) { 9 | code = code.replace(new RegExp(b.name + '\\(', 'g'), `${b.name}.emit(`); 10 | //&& isFunction(this.onFilter)) 11 | code = code.replace(new RegExp(`isFunction\\(this\.${b.name}\\)`, 'g'), `isFunction(this.${b.name}.emit)`); 12 | } 13 | 14 | return code; 15 | }; -------------------------------------------------------------------------------- /src/lib/components/component-class.js: -------------------------------------------------------------------------------- 1 | const getLifecycleEvents = require('./get-lifecycle-events'); 2 | const inputsAndOutputs = require('./component-inputs-and-outputs'); 3 | const constructor = require('./component-constructor'); 4 | const lifecycleEvents = require('./component-lifecycle-events'); 5 | const properties = require('./component-properties'); 6 | const methods = require('./component-methods'); 7 | const getControllerClass = require('./get-controller-class'); 8 | const getComponentClassName = require('./get-component-class-name'); 9 | const getExtendedClasses = require('../get-extended-classes'); 10 | 11 | module.exports.get = function (ast) { 12 | const name = getComponentClassName(ast); 13 | 14 | let extendedClasses = []; 15 | const controllerClass = getControllerClass(ast); 16 | if(controllerClass) { 17 | const extended = getExtendedClasses(controllerClass); 18 | extended.forEach(e => { 19 | if(!/I.*Controller|Bindings/.test(e)) { 20 | extendedClasses.push(e); 21 | } 22 | }); 23 | } 24 | const extendsText = extendedClasses.length > 0 ? ` extends ${extendedClasses.join(', ')}` : ''; 25 | 26 | const interfacesImplemented = getLifecycleEvents(ast); 27 | const implementsText = interfacesImplemented.length > 0 ? ` implements ${interfacesImplemented.join(', ')}` : ''; 28 | 29 | 30 | let result = `export class ${name}${extendsText}${implementsText} {${inputsAndOutputs.get(ast)}${properties.get(ast)}${constructor.get(ast)}${lifecycleEvents.get(ast)}${methods.get(ast)}\n}`; 31 | return result; 32 | }; -------------------------------------------------------------------------------- /src/lib/components/component-constructor.js: -------------------------------------------------------------------------------- 1 | const getContructor = require('../get-constructor'); 2 | const getControllerClass = require('./get-controller-class'); 3 | 4 | module.exports.get = function (ast) { 5 | let result = ''; 6 | 7 | const controllerClass = getControllerClass(ast); 8 | if (!controllerClass) { 9 | return result; 10 | } 11 | 12 | const constructor = getContructor(controllerClass); 13 | 14 | if (!constructor) { 15 | return result; 16 | } 17 | 18 | result = ast.text.slice(constructor.pos, constructor.end); 19 | 20 | 21 | // Need to remove lifecycle events from constructor 22 | const onInit = constructor.body.statements.find(s => { 23 | return /\$onInit/.test(ast.text.slice(s.pos, s.end)); 24 | }); 25 | if (onInit) { 26 | result = result.replace(ast.text.slice(onInit.pos, onInit.end), ''); 27 | } 28 | 29 | const onChanges = constructor.body.statements.find(s => { 30 | return /\$onChanges/.test(ast.text.slice(s.pos, s.end)); 31 | }); 32 | if (onChanges) { 33 | result = result.replace(ast.text.slice(onChanges.pos, onChanges.end), ''); 34 | } 35 | 36 | const onDestroy = constructor.body.statements.find(s => { 37 | return /\$onDestroy/.test(ast.text.slice(s.pos, s.end)); 38 | }); 39 | if (onDestroy) { 40 | result = result.replace(ast.text.slice(onDestroy.pos, onDestroy.end), ''); 41 | } 42 | 43 | result = result.replace(/(private|protected) activatedRouter: ActivatedRouter,/, '$1 activatedRoute: ActivatedRoute,'); 44 | 45 | return result; 46 | }; -------------------------------------------------------------------------------- /src/lib/components/component-decorator.js: -------------------------------------------------------------------------------- 1 | const getTemplate = require('./get-template'); 2 | const getTemplateUrl = require('./get-template-url'); 3 | const processTemplate = require('../process-template'); 4 | const path = require('path'); 5 | 6 | module.exports.get = function (file, ast) { 7 | const results = []; 8 | const template = getTemplate(ast); 9 | const templateUrl = getTemplateUrl(ast); 10 | const componentName = path.basename(file).replace(/[\.-]component\.ts/, ''); 11 | 12 | results.push(` selector: '${componentName}'`); 13 | if (template) { 14 | results.push(` template: \`${processTemplate(template)}\``); 15 | } 16 | else if(/process\.env/.test(templateUrl)) { 17 | results.push(' templateUrl'); 18 | } 19 | else if (templateUrl) { 20 | results.push(` templateUrl: ${templateUrl}`); 21 | } 22 | 23 | return `${/process\.env/.test(templateUrl) ? 'const templateUrl = ' + templateUrl + ';\n' : ''} 24 | @Component({ 25 | ${results.join(',\n')} 26 | })`; 27 | 28 | }; -------------------------------------------------------------------------------- /src/lib/components/component-enums.js: -------------------------------------------------------------------------------- 1 | const getEnums = require('../get-enums'); 2 | 3 | module.exports.get = function (ast) { 4 | const enums = getEnums(ast); 5 | 6 | const results = enums.map(e => { 7 | return ast.text.slice(e.pos, e.end); 8 | }); 9 | 10 | return results.join(''); 11 | }; -------------------------------------------------------------------------------- /src/lib/components/component-imports.js: -------------------------------------------------------------------------------- 1 | const getImports = require('../get-imports'); 2 | const getLifecycleEvents = require('./get-lifecycle-events'); 3 | const getBindings = require('./get-bindings'); 4 | const unique = require('lodash.uniq'); 5 | const sortBy = require('lodash.sortby'); 6 | const getTemplate = require('./get-template'); 7 | const getTemplateUrl = require('./get-template-url'); 8 | 9 | module.exports.get = function (ast) { 10 | let results = getImports(ast); 11 | const lifecycleEvents = getLifecycleEvents(ast); 12 | const imports = [ 13 | 'Component', 14 | ...lifecycleEvents 15 | ]; 16 | 17 | const bindings = getBindings(ast); 18 | 19 | if (bindings.some(x => x.type === 'input' || x.type === 'two-way')) { 20 | imports.push('Input'); 21 | } 22 | 23 | if (bindings.some(x => x.type === 'output' || x.type === 'two-way')) { 24 | imports.push('Output'); 25 | imports.push('EventEmitter'); 26 | } 27 | 28 | if (lifecycleEvents.some(x => x === 'OnChanges')) { 29 | imports.push('SimpleChanges'); 30 | } 31 | 32 | if (/\$window|this\.configuration/.test(ast.text)) { 33 | imports.push('Inject'); 34 | } 35 | 36 | if (/\$element/.test(ast.text)) { 37 | imports.push('ElementRef'); 38 | } 39 | 40 | results.push(`import { ${imports.join(', ')} } from '@angular/core';`); 41 | 42 | if ((/\$http\:/.test(ast.text))) { 43 | results.push('import { HttpClient } from \'@angular/common/http\';'); 44 | } 45 | 46 | if ((/activatedRouter\:/.test(ast.text))) { 47 | results.push('import { ActivatedRoute } from \'@angular/router\';'); 48 | } 49 | 50 | if ((/\$location\:/.test(ast.text))) { 51 | results.push('import { Location } from \'@angular/common\';'); 52 | } 53 | 54 | // (TODO) - New developers please add additional statements if you have custom utilities that you want to import. 55 | // we've provided a commented out code snippet below 56 | // if (/\$ngRedux/.test(ast.text) && !/NgRedux/.test(ast.text)) { 57 | // results.push('import { NgRedux } from \'@angular-redux/store\';'); 58 | // if (!/import { IAppState }/.test(ast.text)) { 59 | // results.push('import { IAppState } from \'app/shared/state/app-state\';'); 60 | // } 61 | // } 62 | 63 | // We need to remove the template import that is no longer being used 64 | const template = getTemplate(ast); 65 | if (!template) { 66 | const templateUrl = getTemplateUrl(ast); 67 | if (templateUrl) { 68 | results = results.filter(x => !(new RegExp(templateUrl)).test(x)); 69 | } 70 | } 71 | 72 | results = sortBy(unique(results.filter(r => !!r))); 73 | results = results.join('\n'); 74 | return results; 75 | }; 76 | -------------------------------------------------------------------------------- /src/lib/components/component-inputs-and-outputs.js: -------------------------------------------------------------------------------- 1 | const getBindings = require('./get-bindings'); 2 | const sortBy = require('lodash.sortby'); 3 | 4 | module.exports.get = function (ast) { 5 | const inputs = []; 6 | const outputs = []; 7 | 8 | const bindings = getBindings(ast); 9 | // Returns an array of {name: String, type: String, optional: Boolean } where type can be input, output or two-way 10 | 11 | for (b of bindings) { 12 | if (b.type === 'input' || b.type === 'two-way') { 13 | inputs.push(` @Input() public ${b.name}${b.optional ? '?' : ''};`); 14 | } 15 | 16 | if (b.type === 'output') { 17 | outputs.push(` @Output() public ${b.name}${b.optional ? '?' : ''}: EventEmitter = new EventEmitter();`); 18 | } 19 | 20 | if (b.type === 'two-way') { 21 | outputs.push(` @Output() public ${b.name}Change${b.optional ? '?' : ''}: EventEmitter = new EventEmitter();`); 22 | } 23 | } 24 | 25 | let result = [ 26 | ...sortBy(inputs), 27 | ...sortBy(outputs) 28 | ].join('\n'); 29 | 30 | if (result) { 31 | result = `\n${result}\n`; 32 | } 33 | 34 | return result; 35 | }; -------------------------------------------------------------------------------- /src/lib/components/component-interfaces.js: -------------------------------------------------------------------------------- 1 | const getInterfaces = require('../get-interfaces'); 2 | 3 | module.exports.get = function (ast) { 4 | const interfaces = getInterfaces(ast); 5 | 6 | const results = interfaces.map(i => { 7 | 8 | // If the interface name ends with "Bindings" or "Controller" we don't want it 9 | if (/(Bindings|Controller)$/.test(i.name.text)) { 10 | return; 11 | } 12 | 13 | return ast.text.slice(i.pos, i.end); 14 | }).filter(x=>!!x); 15 | 16 | return results.join(''); 17 | }; -------------------------------------------------------------------------------- /src/lib/components/component-lifecycle-events.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports.get = function(ast) { 5 | return `\n${this.getOnInit(ast)}${this.getOnChanges(ast)}${this.getOnDestroy(ast)}\n`; 6 | }; 7 | 8 | module.exports.getOnInit = function (ast) { 9 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 10 | const controllerClass = classes[1]; 11 | 12 | if(!controllerClass) { 13 | return ''; 14 | } 15 | 16 | let onInit = controllerClass.members.find(x => x.name && x.name.text === '$onInit' && (x.initializer || x.body)); 17 | 18 | if (onInit) { 19 | onInit = ast.text.slice(onInit.pos, onInit.end); 20 | onInit = onInit.replace(/(public|private) ?\$onInit.*/, 'public ngOnInit(): void {'); 21 | } 22 | else { 23 | const constructor = controllerClass.members.find(x => x.kind === kind.Constructor); 24 | if(constructor) { 25 | onInit = constructor.body.statements.find(s => { 26 | return /\$onInit/.test(ast.text.slice(s.pos, s.end)); 27 | }); 28 | if (onInit) { 29 | onInit = ast.text.slice(onInit.pos, onInit.end); 30 | onInit = onInit.replace(/this\.\$onInit.*{/, 'public ngOnInit(): void {'); 31 | onInit = onInit.replace(/ {4}/g, ' '); 32 | onInit = onInit.replace(/ {6}/g, ' '); 33 | onInit = onInit.slice(0, onInit.length - 1); 34 | } 35 | } 36 | } 37 | 38 | return onInit || ''; 39 | }; 40 | 41 | module.exports.getOnChanges = function (ast) { 42 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 43 | const controllerClass = classes[1]; 44 | 45 | if(!controllerClass) { 46 | return ''; 47 | } 48 | 49 | let onChanges = controllerClass.members.find(x => x.name && x.name.text === '$onChanges' && (x.initializer || x.body)); 50 | 51 | if (onChanges) { 52 | onChanges = ast.text.slice(onChanges.pos, onChanges.end); 53 | onChanges = onChanges.replace(/(private|public) ?\$onChanges.*{/, 'public ngOnChanges(changes: SimpleChanges): void {'); 54 | } 55 | else { 56 | const constructor = controllerClass.members.find(x => x.kind === kind.Constructor); 57 | if(constructor) { 58 | onChanges = constructor.body.statements.find(s => { 59 | return /\$onChanges/.test(ast.text.slice(s.pos, s.end)); 60 | }); 61 | if (onChanges) { 62 | onChanges = ast.text.slice(onChanges.pos, onChanges.end); 63 | onChanges = onChanges.replace(/this\.\$onChanges.*/, 'public ngOnChanges(changes: SimpleChanges): void {'); 64 | onChanges = onChanges.replace(/ {4}/g, ' '); 65 | onChanges = onChanges.replace(/ {6}/g, ' '); 66 | onChanges = onChanges.slice(0, onChanges.length - 1); 67 | } 68 | } 69 | } 70 | 71 | if(onChanges) { 72 | onChanges = onChanges.replace(/bindings/g, 'changes'); 73 | } 74 | 75 | return onChanges || ''; 76 | }; 77 | 78 | module.exports.getOnDestroy = function (ast) { 79 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 80 | const controllerClass = classes[1]; 81 | 82 | if(!controllerClass) { 83 | return ''; 84 | } 85 | 86 | let onDestroy = controllerClass.members.find(x => x.name && x.name.text === '$onDestroy' && (x.initializer || x.body)); 87 | 88 | if (onDestroy) { 89 | onDestroy = ast.text.slice(onDestroy.pos, onDestroy.end); 90 | onDestroy = onDestroy.replace(/(private|public) ?\$onDestroy.*/, 'public ngOnDestroy(): void {'); 91 | } 92 | else { 93 | const constructor = controllerClass.members.find(x => x.kind === kind.Constructor); 94 | if(constructor) { 95 | onDestroy = constructor.body.statements.find(s => { 96 | return /\$onDestroy/.test(ast.text.slice(s.pos, s.end)); 97 | }); 98 | if (onDestroy) { 99 | onDestroy = ast.text.slice(onDestroy.pos, onDestroy.end); 100 | onDestroy = onDestroy.replace(/this\.\$onDestroy.*{/, 'public ngOnDestroy(): void {'); 101 | onDestroy = onDestroy.replace(/ {4}/g, ' '); 102 | onDestroy = onDestroy.replace(/ {6}/g, ' '); 103 | onDestroy = onDestroy.slice(0, onDestroy.length - 1); 104 | } 105 | } 106 | } 107 | 108 | return onDestroy || ''; 109 | }; 110 | -------------------------------------------------------------------------------- /src/lib/components/component-methods.js: -------------------------------------------------------------------------------- 1 | const getMethods = require('../get-methods'); 2 | const getControllerClass = require('./get-controller-class'); 3 | 4 | module.exports.get = function (ast) { 5 | const controllerClass = getControllerClass(ast); 6 | const methods = getMethods.all(controllerClass); 7 | 8 | const results = methods.map(m => { 9 | return ast.text.slice(m.pos, m.end); 10 | }); 11 | 12 | return results.join(''); 13 | }; -------------------------------------------------------------------------------- /src/lib/components/component-properties.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | const getBindings = require('./get-bindings'); 4 | const getProperties = require('../get-properties'); 5 | 6 | module.exports.get = function (ast) { 7 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 8 | const controllerClass = classes[1]; 9 | 10 | const result = []; 11 | 12 | if(controllerClass) { 13 | 14 | const properties = getProperties(controllerClass); 15 | 16 | properties.forEach((p) => { 17 | 18 | // We only want the one line property declarations 19 | if (!/;$/.test(ast.text.slice(p.pos, p.end))) { 20 | return; 21 | } 22 | 23 | // We don't want $onInit, $onChanges or $onDestroy property or method declarations 24 | // as they are handled separately 25 | if (/\$(onInit|onChanges|onDestroy)/.test(p.name.text)) { 26 | return; 27 | } 28 | 29 | // We don't want any properties that should become input or output bindings 30 | const bindings = getBindings(ast); 31 | if (bindings.some(x => x.name === p.name.text)) { 32 | return; 33 | } 34 | 35 | result.push(ast.text.slice(p.pos, p.end)); 36 | 37 | }); 38 | } 39 | 40 | return result.join(''); 41 | }; -------------------------------------------------------------------------------- /src/lib/components/get-bindings.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function (ast) { 5 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 6 | const componentClass = classes[0]; 7 | let bindingsProperty; 8 | 9 | const constructor = componentClass.members.find(x => x.kind === kind.Constructor); 10 | if (constructor) { 11 | bindingsProperty = constructor.body.statements.find(x => x.expression && x.expression.left && x.expression.left.name.text === 'bindings'); 12 | } 13 | 14 | if (!bindingsProperty) { 15 | bindingsProperty = componentClass.members.find(x => x.kind === kind.PropertyDeclaration && x.name.text === 'bindings'); 16 | } 17 | 18 | if (bindingsProperty) { 19 | const text = ast.text.slice(bindingsProperty.pos, bindingsProperty.end); 20 | const results = []; 21 | 22 | // Bindings look like 23 | // this.bindings = { 24 | // compactCount: ' x.kind === kind.ClassDeclaration); 6 | const componentClass = classes[0]; 7 | 8 | let name = componentClass.name.text; 9 | if (name.indexOf('Component') === -1) { 10 | name += 'Component'; 11 | } 12 | 13 | return name; 14 | }; -------------------------------------------------------------------------------- /src/lib/components/get-controller-class.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | const classes = ast.statements.filter(x=>x.kind === kind.ClassDeclaration); 6 | if(classes && classes.length > 1) { 7 | return classes[1]; 8 | } 9 | return undefined; 10 | }; -------------------------------------------------------------------------------- /src/lib/components/get-input-and-outputs-from-angular-4-component.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | 3 | module.exports = function(ast) { 4 | const results = []; 5 | const klass = ast.statements.find(x=>x.kind === ts.SyntaxKind.ClassDeclaration); 6 | 7 | const inputsAndOutputs = klass.members.filter( 8 | x => x.kind === ts.SyntaxKind.PropertyDeclaration 9 | && x.decorators && x.decorators.length > 0); 10 | 11 | inputsAndOutputs.forEach(i => { 12 | results.push({ 13 | type: i.decorators[0].expression.expression.text, 14 | name: i.name.text 15 | }); 16 | }); 17 | 18 | return results; 19 | }; -------------------------------------------------------------------------------- /src/lib/components/get-lifecycle-events.js: -------------------------------------------------------------------------------- 1 | module.exports = function (ast) { 2 | const events = []; 3 | 4 | if (/\$onInit/.test(ast.text)) { 5 | events.push('OnInit'); 6 | } 7 | 8 | if (/\$onChanges/.test(ast.text)) { 9 | events.push('OnChanges'); 10 | } 11 | 12 | if (/\$onDestroy/.test(ast.text)) { 13 | events.push('OnDestroy'); 14 | } 15 | 16 | return events; 17 | }; -------------------------------------------------------------------------------- /src/lib/components/get-template-url.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function (ast) { 5 | let templateUrl = ''; 6 | 7 | if (/require\((`\.\/\${process.*)\)/.test(ast.text)) { 8 | const url = /require\((`\.\/\${process.*)\)/.exec(ast.text)[1]; 9 | return url.replace('?test', ''); 10 | } 11 | 12 | 13 | const templateImport = ast.statements.find(x => x.kind === kind.ImportDeclaration && x.moduleSpecifier && /\.html/.test(x.moduleSpecifier.text)); 14 | if (templateImport) { 15 | templateUrl = `'${templateImport.moduleSpecifier.text}'`; 16 | templateUrl = templateUrl.replace('?test', ''); 17 | } 18 | 19 | return templateUrl; 20 | }; -------------------------------------------------------------------------------- /src/lib/components/get-template.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function (ast) { 5 | let result = ''; 6 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 7 | const componentClass = classes[0]; 8 | let templateProperty; 9 | 10 | const constructor = componentClass.members.find(x => x.kind === kind.Constructor); 11 | if (constructor) { 12 | templateProperty = constructor.body.statements.find(x => x.expression && x.expression.left && x.expression.left.name && x.expression.left.name.text === 'template'); 13 | if(templateProperty) { 14 | result = templateProperty.expression.right.text; 15 | } 16 | } 17 | else if(/public template(: string)? = `/.test(ast.text)) { 18 | return /public template(: string)? = `((.|\n)*)`;/g.exec(ast.text)[2]; 19 | } 20 | 21 | if(result === 'template') { 22 | return ''; 23 | } 24 | 25 | return result; 26 | }; -------------------------------------------------------------------------------- /src/lib/components/replace-controller-static-references.js: -------------------------------------------------------------------------------- 1 | const getComponentClassName = require('./get-component-class-name'); 2 | const getControllerClass = require('./get-controller-class'); 3 | 4 | module.exports = function (ast, code) { 5 | 6 | const componentClassName = getComponentClassName(ast); 7 | const controllerClass = getControllerClass(ast); 8 | 9 | if (controllerClass) { 10 | const controllerName = controllerClass.name.text; 11 | 12 | code = code.replace(new RegExp(`${controllerName}\\.`, 'g'), `${componentClassName}.`); 13 | } 14 | 15 | return code; 16 | }; -------------------------------------------------------------------------------- /src/lib/get-class.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | const classes = ast.statements.filter(x=>x.kind === kind.ClassDeclaration 6 | && x.modifiers && x.modifiers.find(y=>y.kind === kind.ExportKeyword)); 7 | 8 | if(classes && classes.length > 0) { 9 | return classes[0]; 10 | } 11 | return undefined; 12 | }; -------------------------------------------------------------------------------- /src/lib/get-constructor.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(theClass) { 5 | if(!theClass) { 6 | return; 7 | } 8 | return theClass.members.find(x => x.kind === kind.Constructor); 9 | }; -------------------------------------------------------------------------------- /src/lib/get-enums.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | return ast.statements.filter(x => x.kind === kind.EnumDeclaration); 6 | }; -------------------------------------------------------------------------------- /src/lib/get-extended-classes.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(theClass) { 3 | let results = []; 4 | 5 | if(theClass && theClass.heritageClauses && theClass.heritageClauses.length > 0) { 6 | theClass.heritageClauses.forEach((h) => { 7 | h.types.forEach(t => { 8 | results.push(t.expression.text); 9 | }); 10 | }); 11 | } 12 | 13 | return results; 14 | }; -------------------------------------------------------------------------------- /src/lib/get-imports.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | module.exports = function (ast) { 4 | let results = []; 5 | 6 | ast.statements.filter(x => x.kind === kind.ImportDeclaration).forEach(i => { 7 | let importText = ast.text.slice(i.pos, i.end); 8 | 9 | // There is a new line that needs to be trimmed off 10 | results.push(importText.trim()); 11 | }); 12 | 13 | return results; 14 | }; -------------------------------------------------------------------------------- /src/lib/get-interfaces.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | return ast.statements.filter(x => x.kind === kind.InterfaceDeclaration); 6 | }; -------------------------------------------------------------------------------- /src/lib/get-methods.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports.all = function getMethods(theClass) { 5 | 6 | const results = []; 7 | 8 | if(!theClass) { 9 | return results; 10 | } 11 | 12 | theClass.members.forEach(m => { 13 | // We don't want public static $inject properties 14 | if(m.name && m.name.text === '$inject') { 15 | return; 16 | } 17 | 18 | // We don't want the constructor as it is handled separately 19 | if(m.kind === kind.Constructor) { 20 | return; 21 | } 22 | 23 | // We don't want the one line property declarations 24 | if(m.kind === kind.PropertyDeclaration && (!m.initializer || m.initializer && m.initializer.kind !== kind.ArrowFunction)) { 25 | return; 26 | } 27 | 28 | // We don't want semi-colon classes 29 | if(m.kind === kind.SemicolonClassElement) { 30 | return; 31 | } 32 | 33 | //We don't want $onInit, $onChanges or $onDestroy property or method declarations as they are handled separately 34 | if((m.kind === kind.PropertyDeclaration || m.kind === kind.MethodDeclaration) && /(onInit|onChanges|onDestroy)/.test(m.name.text)) { 35 | return; 36 | } 37 | 38 | results.push(m); 39 | 40 | }); 41 | 42 | return results; 43 | }; 44 | 45 | module.exports.public = function(theClass) { 46 | const methods = this.all(theClass); 47 | return methods.filter(m => m.modifiers && m.modifiers.find(x=> x.kind === kind.PublicKeyword)); 48 | }; -------------------------------------------------------------------------------- /src/lib/get-mocks.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const getConstructor = require('./get-constructor'); 3 | const getImports = require('./get-imports'); 4 | 5 | module.exports = function (ast) { 6 | let mocks = []; 7 | const theClass = ast.statements.find(x => x.kind === ts.SyntaxKind.ClassDeclaration); 8 | const constructor = getConstructor(theClass); 9 | 10 | function getType(parameter) { 11 | let type = 'any'; 12 | if (parameter && parameter.type && parameter.type.typeName) { 13 | type = parameter.type.typeName.text; 14 | } 15 | return type; 16 | } 17 | 18 | function getProvider(mock) { 19 | let result; 20 | if (mock.type === 'NgRedux') { 21 | result = '{ provide: NgRedux, useValue: MockNgRedux.getInstance() }'; 22 | } 23 | else if (mock.useReal) { 24 | result = `{ provide: ${mock.type}, useClass: ${mock.type} }`; 25 | } 26 | else if (mock.useObject) { 27 | result = `{ provide: '${mock.name}', useValue: ${mock.mockName} }`; 28 | } 29 | else { 30 | result = `{ provide: ${mock.type}, useValue: instance(${mock.mockName}) }`; 31 | } 32 | 33 | return result; 34 | } 35 | 36 | function getVariable(mock) { 37 | let result; 38 | if (mock.type === 'NgRedux') { 39 | return result; 40 | } 41 | result = `let ${mock.mockName}${mock.type ? ': ' + mock.type : ''};`; 42 | return result; 43 | } 44 | 45 | function getAssignment(mock) { 46 | let result; 47 | if (mock.useReal || mock.type === 'NgRedux') { 48 | return result; 49 | } 50 | 51 | if (mock.useObject) { 52 | result = `${mock.mockName} = {};`; 53 | } 54 | else if (mock.useMock) { 55 | result = `${mock.mockName} = mock(${mock.type})`; 56 | } 57 | return result; 58 | } 59 | 60 | 61 | const imports = getImports(ast); 62 | 63 | if (constructor && constructor.parameters) { 64 | constructor.parameters.forEach(p => { 65 | if (/\$q|\$timeout|\$interval|\$element/.test(p.name.text)) { 66 | return; 67 | } 68 | 69 | const type = getType(p); 70 | if (!type) { 71 | return; 72 | } 73 | 74 | let theImport = null; 75 | if (p.type && p.type.typeName && p.type.typeName.text) { 76 | theImport = imports.find(x => (new RegExp(' ' + p.type.typeName.text + '[, ]')).test(x)); 77 | 78 | if (/NgRedux/.test(theImport)) { 79 | theImport += '\nimport { MockNgRedux } from \'@angular-redux/store/testing\';'; 80 | } 81 | } 82 | 83 | const useReal = /EventsManager/.test(type); 84 | const useObject = type === 'any'; 85 | let mock = { 86 | name: p.name.text, 87 | mockName: `${p.name.text}Mock`, 88 | type: type, 89 | import: theImport, 90 | useReal, 91 | useObject, 92 | useMock: !useReal && !useObject 93 | }; 94 | 95 | mock.provide = getProvider(mock); 96 | mock.variable = getVariable(mock); 97 | mock.assignment = getAssignment(mock); 98 | 99 | mocks.push(mock); 100 | }); 101 | } 102 | 103 | return mocks; 104 | }; -------------------------------------------------------------------------------- /src/lib/get-named-imports.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | const sortBy = require('lodash.sortby'); 4 | 5 | module.exports = function(ast) { 6 | const results = []; 7 | const imports = ast.statements.filter(x=>x.kind === kind.ImportDeclaration); 8 | 9 | imports.forEach(i => { 10 | if(i.importClause && i.importClause.namedBindings && i.importClause && i.importClause.namedBindings.elements) { 11 | i.importClause.namedBindings.elements.forEach(e => { 12 | results.push({ 13 | name: e.name.text, 14 | path: i.moduleSpecifier.text 15 | }); 16 | }); 17 | } 18 | }); 19 | 20 | return sortBy(results, (r) => { 21 | return r.name; 22 | }); 23 | }; -------------------------------------------------------------------------------- /src/lib/get-output-file-path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('../config'); 3 | 4 | module.exports = function (filePath) { 5 | let outputFileName = path.join(config.outputRoot, filePath.replace(process.cwd(), '')); 6 | if (/src\/(data|img)/.test(outputFileName)) { 7 | outputFileName = outputFileName.replace('src', 'src/assets'); 8 | } 9 | return outputFileName; 10 | }; -------------------------------------------------------------------------------- /src/lib/get-properties.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(theClass) { 5 | const results = theClass.members.map(p => { 6 | if (p.kind !== kind.PropertyDeclaration) { 7 | return; 8 | } 9 | 10 | if (p.name && p.name.text === '$inject') { 11 | return; 12 | } 13 | 14 | return p; 15 | }).filter(x=>!!x); 16 | 17 | return results; 18 | }; -------------------------------------------------------------------------------- /src/lib/get-related-spec-file-path.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = function (file) { 4 | let specFile; 5 | if (fs.existsSync(file.replace('.ts', '.spec.ts'))) { 6 | specFile = file.replace('.ts', '.spec.ts'); 7 | } 8 | else if (fs.existsSync(file.replace('.ts', '-spec.ts'))) { 9 | specFile = file.replace('.ts', '-spec.ts'); 10 | } 11 | return specFile; 12 | }; -------------------------------------------------------------------------------- /src/lib/get-source-file.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('fs'); 2 | const ts = require('typescript'); 3 | 4 | module.exports = function (file) { 5 | const sourceFile = ts.createSourceFile(file, readFileSync(file).toString(), ts.ScriptTarget.TS, true); 6 | return sourceFile; 7 | }; -------------------------------------------------------------------------------- /src/lib/get-specs.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const getSourceFile = require('../lib/get-source-file'); 3 | const uniqueId = require('lodash.uniqueid'); 4 | 5 | module.exports = function (path) { 6 | const source = getSourceFile(path); 7 | const nodes = []; 8 | let id; 9 | const parentForDepth = {}; 10 | 11 | // Walk the AST tree and pick out the describes and the its and put them in a flat array 12 | walk(source, 0); 13 | 14 | function walk(node, depth = 0) { 15 | 16 | if (node.kind === ts.SyntaxKind.CallExpression && node.expression.text === 'describe') { 17 | const newId = +uniqueId(); 18 | 19 | nodes.push({ 20 | depth: depth, 21 | type: 'describe', 22 | suite: node.arguments[0].text, 23 | id: newId, 24 | parentId: parentForDepth[`d_${depth - 6}`] 25 | }); 26 | 27 | if (!parentForDepth[`d_${depth}`]) { 28 | parentForDepth[`d_${depth}`] = newId; 29 | } 30 | id = newId; 31 | } 32 | 33 | if (node.kind === ts.SyntaxKind.CallExpression && node.expression.text === 'it') { 34 | nodes.push({ 35 | depth: depth, 36 | type: 'it', 37 | test: node.arguments[0].text, 38 | parentId: id, 39 | code: source.text.slice(node.pos, node.end) 40 | }); 41 | } 42 | 43 | if (node.kind === ts.SyntaxKind.CallExpression && node.expression.text === 'beforeEach') { 44 | 45 | nodes.push({ 46 | depth: depth, 47 | type: 'beforeEach', 48 | parentId: id, 49 | code: source.text.slice(node.pos, node.end) 50 | }); 51 | } 52 | 53 | if (node.kind === ts.SyntaxKind.CallExpression && node.expression.text === 'afterEach') { 54 | 55 | nodes.push({ 56 | depth: depth, 57 | type: 'afterEach', 58 | parentId: id, 59 | code: source.text.slice(node.pos, node.end) 60 | }); 61 | } 62 | 63 | depth++; 64 | node.getChildren().forEach(c => walk(c, depth)); 65 | } 66 | 67 | // Now convert that flat array back to a tree 68 | let map = {}; 69 | let node; 70 | let roots = []; 71 | for (let i = 0; i < nodes.length; i += 1) { 72 | node = nodes[i]; 73 | node.children = []; 74 | map[node.id] = i; 75 | if (node.parentId) { 76 | nodes[map[node.parentId]].children.push(node); 77 | } else { 78 | roots.push(node); 79 | } 80 | } 81 | 82 | return roots; 83 | 84 | }; -------------------------------------------------------------------------------- /src/lib/get-unit-tests.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = function (file) { 4 | const contents = fs.readFileSync(file, 'UTF-8'); 5 | const lines = contents.split('\n'); 6 | const results = []; 7 | 8 | let result; 9 | 10 | lines.forEach(l => { 11 | const describe = /describe\('(.*)'/.exec(l); 12 | if (describe) { 13 | 14 | //If we already have a describe, its time to push the result and start again 15 | if (result) { 16 | results.push(result); 17 | result = undefined; 18 | } 19 | 20 | // We don't want the top level '' 21 | if (/unit test/i.test(describe[1])) { 22 | return; 23 | } 24 | 25 | result = { group: describe[1], tests: [] }; 26 | return; 27 | } 28 | 29 | const it = /it\('(.*)'/.exec(l); 30 | if (it) { 31 | if (!result) { 32 | result = { group: '', tests: [] }; 33 | } 34 | result.tests.push(it[1]); 35 | } 36 | }); 37 | 38 | return results; 39 | }; -------------------------------------------------------------------------------- /src/lib/make-directories-in-path.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function make(filePath) { 5 | const dirname = path.dirname(filePath); 6 | if (fs.existsSync(dirname)) { 7 | return true; 8 | } 9 | make(dirname); 10 | fs.mkdirSync(dirname); 11 | }; -------------------------------------------------------------------------------- /src/lib/parser.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | 3 | const defaultOptions = { 4 | experimentalDecorators: true, 5 | experimentalAsyncFunctions: true, 6 | jsx: false, 7 | }; 8 | 9 | module.exports = { 10 | parse(code, options) { 11 | options = Object.assign({}, defaultOptions, options); 12 | 13 | const compilerHost = { 14 | fileExists: () => true, 15 | getCanonicalFileName: filename => filename, 16 | getCurrentDirectory: () => '', 17 | getDefaultLibFileName: () => 'lib.d.ts', 18 | getNewLine: () => '\n', 19 | getSourceFile: filename => { 20 | return ts.createSourceFile(filename, code, ts.ScriptTarget.Latest, true); 21 | }, 22 | readFile: () => null, 23 | useCaseSensitiveFileNames: () => true, 24 | writeFile: () => null, 25 | }; 26 | 27 | const filename = 'file.ts'; 28 | 29 | const program = ts.createProgram([filename], { 30 | noResolve: true, 31 | target: ts.ScriptTarget.Latest, 32 | experimentalDecorators: options.experimentalDecorators, 33 | experimentalAsyncFunctions: options.experimentalAsyncFunctions, 34 | jsx: options.jsx ? 'preserve' : undefined, 35 | }, compilerHost); 36 | 37 | const sourceFile = program.getSourceFile(filename); 38 | 39 | return sourceFile; 40 | } 41 | }; -------------------------------------------------------------------------------- /src/lib/pretty-print.js: -------------------------------------------------------------------------------- 1 | const renderSourceFile = require('./render-source-file'); 2 | const prettier = require('prettier'); 3 | 4 | module.exports = { 5 | printAst, 6 | printString 7 | }; 8 | 9 | function printAst(ast) { 10 | let code = renderSourceFile(ast); 11 | return printString(code); 12 | } 13 | 14 | function printString(code) { 15 | 16 | // The AST printed by typescript doesn't have the same formatting as the orignal file. 17 | // We start by adding a bunch of line breaks 18 | code = code.replace(/(\s{4,}};?\s)/g, '$1\n'); 19 | code = code.replace(/(\s{4,}if \(.*)/g, '\n$1'); 20 | code = code.replace(/(\s{4,}return.*)/g, '\n$1'); 21 | code = code.replace(/^(( *)?(export|constructor|interface|class).*)/gm, '\n$1'); 22 | code = code.replace(/(@Injectable().*)/g, '\n$1'); 23 | code = code.replace(/(@Component().*)/g, '\n$1'); 24 | 25 | // Now we run the code through prettier 26 | code = prettier.format(code, { 27 | parser: 'typescript', 28 | printWidth: 140, 29 | tabWidth: 4, 30 | singleQuote: true 31 | }); 32 | 33 | code = code.replace(/(@(Output|Input).*;\n)( +)(public)/g, '$1\n$3$4'); 34 | 35 | return code; 36 | } -------------------------------------------------------------------------------- /src/lib/process-string-replacements.js: -------------------------------------------------------------------------------- 1 | const replacements = [ 2 | // $q 3 | [/(ng\.)?IPromise<(.*)>/g, 'Promise<$2>'], 4 | [/ng\.IHttpPromise<(.*)>/g, 'Promise<$1>'], 5 | [/(ng\.)?IPromise/g, 'Promise'], 6 | [/(this|self)\.\$q\.when\(\)/g, 'Promise.resolve()'], 7 | [/(this|self)\.\$q\.all/g, 'Promise.all'], 8 | [/( *)(let|const)?\s*defer(red)?:?.*? = (this|self)\.\$q\.defer\(\)/g, '$1let deferredResolve;\n$1let deferredReject;\n$1let deferred: Promise = new Promise((resolve, reject) => {\n$1 deferredResolve = resolve;\n$1 deferredReject = reject;\n$1})'], 9 | [/defer(red)?\.resolve/g, 'deferredResolve'], 10 | [/defer(red)?\.reject/g, 'deferredReject'], 11 | [/defer(red)?\.promise/g, 'deferred'], 12 | [/(this|self)\.\$q\.when\((.*)\)/g, 'Promise.resolve($2)'], 13 | [/(this|self)\.\$q\.resolve\((.*)\)/g, 'Promise.resolve($2)'], 14 | [/(this|self)\.\$q\.reject\((.*)\)/g, 'Promise.reject($2)'], 15 | [/, this\.\$q/g, ''], 16 | [/Promise\.reject\(\);/g, 'Promise.reject(new Error(\'An error occurred.\'));'], 17 | [/\(\$q, /g, '('], 18 | 19 | // $window 20 | [/private \$window(: (any|ng\.IWindowService))?/g, '@Inject(\'$window\') private $window: any'], 21 | 22 | // configuration 23 | [/private configuration(: any)?/g, '@Inject(\'configuration\') private configuration: any'], 24 | 25 | // redux 26 | [/private \$ngRedux(: any)?/g, 'private $ngRedux: NgRedux'], 27 | 28 | // $timeout 29 | [/(this|self)\.\$timeout\.cancel\((.*)\)/g, 'clearTimeout($2)'], 30 | [/(this|self)\.\$timeout/g, 'setTimeout'], 31 | 32 | // $interval 33 | [/(this|self)\.\$interval\.cancel\((.*)\)/g, 'clearInterval($2)'], 34 | [/(this|self)\.\$interval/g, 'setInterval'], 35 | 36 | [/( *)\.finally\((.*)/g, '$1.then($2\n$1 // Native promise does not have finally. This then will execute last.'], 37 | 38 | // One off issues with PromotionalControllerBase 39 | [/public abstract \$onInit: \(\) => void;/g, 'public abstract ngOnInit(): void;'], 40 | 41 | [/super\(\$q, \$routeParams, window,/g, 'super($routeParams,'], 42 | 43 | // Modal one off issues 44 | [/ \({ \$value: any }\?\) => void;/g, ' EventEmitter;'], 45 | [/this\.dismiss\(\);/, 'this.dismiss.emit();'], 46 | 47 | [/\$http:\s*ng\.IHttpService/g, '$http: HttpClient'], 48 | [/\$location:\s*ng\.ILocationService/g, '$location: Location'], 49 | [/\$location\.path/g, '$location.go'], 50 | [/this\.activatedRouter\.hash\(\)/g, 'this.activatedRoute.snapshot.fragment'], 51 | [/(this\.\$http\.get\(.*\))/g, '$1.toPromise()'], 52 | [/params: (.*),/g, 'params: new HttpParams({fromObject: ($1) }),'], 53 | [/headers: {\n(.*)\n.*}\n/g, 'headers: new HttpHeaders({ $1 })'], 54 | [/response\.data/g, 'response'], 55 | [/return this\.\$location\.hash\(\)/g, '//return this.$location.hash()'], 56 | 57 | [/angular\.ui\.bootstrap\.\w+/g, 'any'], 58 | 59 | [/ ng\.[^,|\n|;|)| ]*/g, ' any'], 60 | 61 | [/const .* require\(`\.\/\${process.*\);\n/, ''], 62 | [/\$element: any(,|\))/g, '$element: ElementRef$1'], 63 | 64 | [/Array<(\w+)>/g, '$1[]'], 65 | ]; 66 | 67 | module.exports = function(results) { 68 | for(r of replacements) { 69 | results = results.replace(r[0], r[1]); 70 | } 71 | return results; 72 | }; -------------------------------------------------------------------------------- /src/lib/process-template.js: -------------------------------------------------------------------------------- 1 | const cheerio = require('cheerio'); 2 | const pretty = require('js-beautify'); 3 | const camelToDash = require('./camel-to-dash'); 4 | 5 | // Reference for changes in templates https://angular.io/guide/ajs-quick-reference 6 | 7 | module.exports = function(template, componentDict) { 8 | let result = template; 9 | const $ = cheerio.load(result, { 10 | normalizeWhitespace: false, 11 | decodeEntities: false, 12 | }); 13 | 14 | /*********************************** Processing template with cheerio *********************************/ 15 | 16 | // Replace ng-bind 17 | const mdButton = $('md-button'); 18 | mdButton.each(function() { 19 | // Do not use ()=> 20 | $(this).attr('mat-button', 'mat-button'); 21 | }); 22 | 23 | // Replace ng-bind 24 | const ngBind = $('[ng-bind]'); 25 | ngBind.each(function() { 26 | // Do not use ()=> 27 | const val = $(this).attr('ng-bind'); 28 | $(this).removeAttr('ng-bind'); 29 | $(this).text(`{{${val}}}`); 30 | }); 31 | ngBind.removeAttr('ng-bind'); 32 | 33 | // Replace ng-show 34 | const ngShow = $('[ng-show]'); 35 | ngShow.each(function() { 36 | // Do not use ()=> 37 | const val = $(this).attr('ng-show'); 38 | $(this).removeAttr('ng-show'); 39 | $(this).attr('[hidden]', `!${val}`); 40 | }); 41 | 42 | // in Angular2, structural directives cannot live on the same element, so *ngIf and *ngRepeat can't be on the same element. 43 | // solution is to move ngIf to a wrapper like ng-container, and not introduce new html elements. 44 | // https://angular.io/guide/structural-directives#group-sibling-elements-with-ng-container 45 | const ngIfngRepeats = $('[ng-if][ng-repeat]'); 46 | ngIfngRepeats.each(function() { 47 | const $el = $(this); 48 | const ifVal = $el.attr('ng-if'); 49 | 50 | $el.removeAttr('ng-if'); 51 | $el.wrap($(``)); 52 | }); 53 | 54 | // Remove ng-model-options 55 | const ngModelOptions = $('[ng-model-options]'); 56 | ngModelOptions.each(function() { 57 | $(this).removeAttr('ng-model-options'); 58 | }); 59 | 60 | const allInputs = $('input, select'); 61 | allInputs.each(function() { 62 | const name = $(this).attr('name'); 63 | if (name) { 64 | $(this).attr(`#${name}`, 'ngModel'); 65 | } 66 | }); 67 | 68 | for (let componentKey in componentDict) { 69 | const components = $(componentKey); 70 | components.each(function() { 71 | const componentBindings = componentDict[componentKey]; 72 | for (let newInputBinding of componentBindings.input) { 73 | const oldInputBinding = camelToDash(newInputBinding); 74 | const attrValue = $(this).attr(oldInputBinding); 75 | $(this).removeAttr(oldInputBinding); 76 | $(this).attr(`[${newInputBinding}]`, attrValue); 77 | } 78 | for (let newOutputBinding of componentBindings.output) { 79 | const oldOutputBinding = camelToDash(newOutputBinding); 80 | const attrValue = $(this).attr(oldOutputBinding); 81 | $(this).removeAttr(oldOutputBinding); 82 | $(this).attr(`(${newOutputBinding})`, attrValue); 83 | } 84 | for (let newBinding of componentBindings['two-way']) { 85 | const oldBinding = camelToDash(newBinding); 86 | const attrValue = $(this).attr(oldBinding); 87 | $(this).removeAttr(oldBinding); 88 | $(this).attr(`[(${newBinding})]`, attrValue); 89 | } 90 | }); 91 | } 92 | 93 | result = $('body').html(); 94 | 95 | /*********************************** Processing template with regex replace ***************************/ 96 | 97 | // Cheerio adds ="" to empty attributes 98 | result = result.replace(/=""/g, ''); 99 | 100 | // Replace ng-if with *ngIf 101 | result = result.replace(/ng-if/g, '*ngIf'); 102 | 103 | // Replace one time bindings 104 | result = result.replace(/"::/g, '"'); 105 | result = result.replace(/{::/g, '{'); 106 | 107 | // Remove $ctrl 108 | result = result.replace(/\::\$ctrl\./g, ''); 109 | result = result.replace(/\$ctrl\./g, ''); 110 | 111 | // Replace ng-click with (click) 112 | result = result.replace(/([^\w])ng-click([^\w])/g, '$1(click)$2'); 113 | 114 | // Replace ng-dblclick with (dblclick) 115 | result = result.replace(/([^\w])ng-dblclick([^\w])/g, '$1(dblclick)$2'); 116 | 117 | // Replace ng-blur with (blur) 118 | result = result.replace(/([^\w])ng-blur([^\w])/g, '$1(blur)$2'); 119 | 120 | // Replace ng-focus with (focus) 121 | result = result.replace(/([^\w])ng-focus([^\w])/g, '$1(focus)$2'); 122 | 123 | // Replace ng-mouseenter with (mouseenter) 124 | result = result.replace(/([^\w])ng-mouseenter([^\w])/g, '$1(mouseenter)$2'); 125 | 126 | // Replace ng-mouseleave with (mouseleave) 127 | result = result.replace(/([^\w])ng-mouseleave([^\w])/g, '$1(mouseleave)$2'); 128 | 129 | // Replace ng-mouseover with (mouseover) 130 | result = result.replace(/([^\w])ng-mouseover([^\w])/g, '$1(mouseover)$2'); 131 | 132 | // Replace ng-keypress with (keypress) 133 | result = result.replace(/([^\w])ng-keypress([^\w])/g, '$1(keypress)$2'); 134 | 135 | // Replace ng-keydown with (keydown) 136 | result = result.replace(/([^\w])ng-keydown([^\w])/g, '$1(keydown)$2'); 137 | 138 | // Replace ng-change with (change) 139 | result = result.replace(/([^\w])ng-change([^\w])/g, '$1(change)$2'); 140 | 141 | // Replace ng-change with (change) 142 | result = result.replace(/([^\w])ng-submit([^\w])/g, '$1(ngSubmit)$2'); 143 | 144 | // Replace track by with trackBy 145 | result = result.replace(/([^\w])track by (\w+)(\..*)"/g, ';trackBy:$2?$3"'); 146 | 147 | // Replace ng-href with [href] 148 | result = result.replace(/([^\w])ng-href([^\w])/g, '$1[href]$2'); 149 | result = result.replace(/\[href\]="((\w|\/)+)"/g, 'href="$1"'); 150 | result = result.replace( 151 | /\[href\]="(.+){{(.*)}}(.*)"/g, 152 | '[href]="`$1${$2}$3`"' 153 | ); 154 | result = result.replace(/\[href\]="{{(.*)}}"/g, '[href]="$1"'); 155 | result = result.replace( 156 | /\[href\]="`(.*)\${(.*)}(.*)`"/g, 157 | "[href]=\"'$1' + $2 + '$3'\"" 158 | ); 159 | result = result.replace(/\[href\]="(.*) \+ ''"/g, '[href]="$1"'); 160 | 161 | // Replace ng-class with [ngClass] 162 | result = result.replace(/([^\w])ng-class([^\w])/g, '$1[ngClass]$2'); 163 | 164 | // Replace ng-style with [ngStyle] 165 | result = result.replace(/([^\w])ng-style([^\w])/g, '$1[ngStyle]$2'); 166 | 167 | // Replace ng-src with [src] 168 | result = result.replace(/([^\w])ng-src([^\w])/g, '$1[src]$2'); 169 | 170 | // Replace ng-bind-html with [innerHtml] 171 | // https://stackoverflow.com/questions/34585453/how-to-bind-raw-html-in-angular2 172 | result = result.replace(/([^\w])ng-bind-html([^\w])/g, '$1[innerHtml]$2'); 173 | 174 | // Replace ng-hide with [hidden] 175 | result = result.replace(/([^\w])ng-hide([^\w])/g, '$1[hidden]$2'); 176 | 177 | // Replace ng-disabled with [disabled] 178 | result = result.replace(/(ng-| )disabled="(.*)"/g, '[disabled]="$2"'); 179 | 180 | // Replace ng-checked with [checked] 181 | // https://stackoverflow.com/questions/40214655/angular-2-checkbox-two-way-data-binding 182 | result = result.replace(/(ng-| )checked="(.*)"/g, '[checked]="$2"'); 183 | 184 | // Replace ng-model with [(ngModel)] 185 | result = result.replace(/([^\w])ng-model([^\w])/g, '$1[(ngModel)]$2'); 186 | 187 | // Replace ng-value with [value] 188 | result = result.replace(/([^\w])ng-value([^\w])/g, '$1[value]$2'); 189 | 190 | // Replace ng-required with [required] 191 | result = result.replace(/(ng-| )required="(.*)"/g, '[required]="$2"'); 192 | 193 | // Replace ng-pattern with [pattern] 194 | result = result.replace(/(ng-| )pattern="(.*)"/g, '[pattern]="$2"'); 195 | 196 | // Replace ng-repeat="x in ..." with *ngFor="let x of" 197 | result = result.replace( 198 | /([^\w])ng-repeat="(\w+)\sin\s([^"]*)"/g, 199 | '$1*ngFor="let $2 of $3"' 200 | ); 201 | 202 | // Replace track by with ; trackBy 203 | result = result.replace(/track by/g, ';trackBy:'); 204 | 205 | // Replace limitTo pipe in ngRepeats with slice pipe 206 | result = result.replace(/limitTo\s?:\s?([^"]*)/g, 'slice:0:$1'); 207 | 208 | // Replace ng-switch-when with *ngSwitchCase 209 | result = result.replace( 210 | /([^\w])ng-switch-when([^\w])/g, 211 | '$1*ngSwitchCase$2' 212 | ); 213 | 214 | // Replace ng-switch-default with *ngSwitchDefault 215 | result = result.replace( 216 | /([^\w])ng-switch-default([^\w])/g, 217 | '$1*ngSwitchDefault$2' 218 | ); 219 | 220 | // Replace ng-switch with [ngSwitch] 221 | result = result.replace(/([^\w])ng-switch([^\w])/g, '$1[ngSwitch]$2'); 222 | 223 | // Replace itemtype with [attr.itemtype] 224 | result = result.replace(/itemtype="{{(.*)}}"/, '[attr.itemtype]="$1"'); 225 | 226 | // Replace ng-view with router-outlet 227 | result = result.replace(/ng-view/g, 'router-outlet'); 228 | 229 | // Replace ng-view with router-outlet 230 | result = result.replace(/ng-view/g, 'router-outlet'); 231 | 232 | result = pretty.html(result, { 233 | unformatted: ['code', 'pre', 'em', 'strong'], 234 | indent_inner_html: true, 235 | indent_char: ' ', 236 | indent_size: 4, 237 | sep: '\n', 238 | wrap_attributes: 'force-aligned', 239 | }); 240 | return result; 241 | }; 242 | -------------------------------------------------------------------------------- /src/lib/read-file.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, existsSync } = require('fs'); 2 | 3 | module.exports = function (file) { 4 | if (!existsSync(file)) { 5 | throw new Error(`${file} does not exist.`); 6 | } 7 | return readFileSync(file, 'UTF-8'); 8 | }; -------------------------------------------------------------------------------- /src/lib/remove-$injects.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | ast.statements.filter(s => s.kind === kind.ClassDeclaration).forEach(c => { 6 | c.members = c.members.filter(m => { 7 | if (!m.name) { 8 | return true; 9 | } 10 | return m.name.text !== '$inject'; 11 | }); 12 | }); 13 | 14 | return ast; 15 | }; -------------------------------------------------------------------------------- /src/lib/remove-from-constructor.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function (ast, parameterNames) { 5 | 6 | ast.statements.filter(s => s.kind === kind.ClassDeclaration).forEach(c => { 7 | const constructor = c.members.find(x => x.kind === kind.Constructor); 8 | if (constructor) { 9 | constructor.parameters = constructor.parameters.filter(p => { 10 | return parameterNames.indexOf(p.name.text) === -1; 11 | }); 12 | 13 | if (constructor.parameters.find(x => x.name.text === '$locationUtils')) { 14 | constructor.parameters = constructor.parameters.filter(p => p.name.text !== '$routeParams'); 15 | } 16 | } 17 | }); 18 | 19 | return ast; 20 | }; -------------------------------------------------------------------------------- /src/lib/remove-imports.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports = function(ast) { 5 | ast.statements = ast.statements.filter(x => x.kind !== kind.ImportDeclaration); 6 | return ast; 7 | }; -------------------------------------------------------------------------------- /src/lib/render-source-file.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | 3 | module.exports = function(sourceFile) { 4 | const printer = ts.createPrinter({ 5 | newLine: ts.NewLineKind.LineFeed 6 | }); 7 | 8 | return printer.printFile(sourceFile); 9 | }; -------------------------------------------------------------------------------- /src/lib/render-template.js: -------------------------------------------------------------------------------- 1 | const handlebars = require('handlebars'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const encoding = 'UTF-8'; 5 | 6 | module.exports = function (templateName, data) { 7 | if (!templateName.endsWith('.hbs')) { 8 | templateName += '.hbs'; 9 | } 10 | const templateFile = fs.readFileSync(path.join(__dirname, '../templates', templateName), encoding); 11 | const template = handlebars.compile(templateFile, { encoding: 'UTF-8' }); 12 | 13 | return template(data); 14 | }; -------------------------------------------------------------------------------- /src/lib/services/service-class.js: -------------------------------------------------------------------------------- 1 | const getClass = require('../get-class'); 2 | const constructor = require('./service-constructor'); 3 | const properties = require('./service-properties'); 4 | const methods = require('./service-methods'); 5 | const getExtendedClasses = require('../get-extended-classes'); 6 | 7 | module.exports.get = function (ast) { 8 | const theClass = getClass(ast); 9 | const extendedClasses = getExtendedClasses(theClass); 10 | let extendsText = ''; 11 | 12 | if(extendedClasses && extendedClasses.length > 0) { 13 | extendsText = ` extends ${extendedClasses.join(', ')}`; 14 | } 15 | 16 | let result = `\n\n@Injectable()\nexport class ${theClass.name.text}${extendsText} {${properties.get(ast)}${constructor.get(ast)}${methods.get(ast)}\n}`; 17 | return result; 18 | }; -------------------------------------------------------------------------------- /src/lib/services/service-constructor.js: -------------------------------------------------------------------------------- 1 | const getContructor = require('../get-constructor'); 2 | const getClass = require('../get-class'); 3 | 4 | module.exports.get = function (ast) { 5 | let result = ''; 6 | 7 | const theClass = getClass(ast); 8 | if(!theClass) { 9 | return result; 10 | } 11 | 12 | const constructor = getContructor(theClass); 13 | 14 | if (!constructor) { 15 | return result; 16 | } 17 | 18 | result = ast.text.slice(constructor.pos, constructor.end); 19 | 20 | return result; 21 | }; -------------------------------------------------------------------------------- /src/lib/services/service-http.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | const getProperties = require('../get-properties'); 4 | 5 | module.exports.get = function (ast) { 6 | const methods = ast.statements.filter(x => x.kind === kind.ClassDeclaration); 7 | console.log(methods); 8 | const theClass = classes[0]; 9 | 10 | let results = []; 11 | 12 | return results.join(''); 13 | }; -------------------------------------------------------------------------------- /src/lib/services/service-imports.js: -------------------------------------------------------------------------------- 1 | const getImports = require('../get-imports'); 2 | const unique = require('lodash.uniq'); 3 | const sortBy = require('lodash.sortby'); 4 | 5 | module.exports.get = function (ast) { 6 | let results = getImports(ast); 7 | 8 | const imports = []; 9 | 10 | // Add Injectable import if the code contain an exported class 11 | if (/export (abstract )?class/.test(ast.text)) { 12 | imports.push('Injectable'); 13 | } 14 | 15 | if (/\$window|this\.configuration/.test(ast.text)) { 16 | imports.push('Inject'); 17 | } 18 | 19 | if (/\$element/.test(ast.text)) { 20 | imports.push('ElementRef'); 21 | } 22 | 23 | results.push(`import { ${imports.join(', ')} } from '@angular/core';`); 24 | 25 | if ((/\$http\:/.test(ast.text))) { 26 | results.push('import { HttpClient, HttpHeaders, HttpParams } from \'@angular/common/http\';'); 27 | } 28 | 29 | // (TODO) - New developers please add additional statements if you have custom utilities that you want to import. 30 | // we've provided a commented out code snippet below 31 | // if (/\$ngRedux/.test(ast.text) && !/NgRedux/.test(ast.text)) { 32 | // results.push('import { NgRedux } from \'@angular-redux/store\';'); 33 | // if (!/import { IAppState }/.test(ast.text)) { 34 | // results.push('import { IAppState } from \'app/shared/state/app-state\';'); 35 | // } 36 | // } 37 | 38 | results = sortBy(unique(results)); 39 | results = results.join('\n'); 40 | return results; 41 | }; 42 | -------------------------------------------------------------------------------- /src/lib/services/service-interfaces.js: -------------------------------------------------------------------------------- 1 | const getInterfaces = require('../get-interfaces'); 2 | 3 | module.exports.get = function (ast) { 4 | const interfaces = getInterfaces(ast); 5 | 6 | const results = interfaces.map(i => { 7 | return ast.text.slice(i.pos, i.end); 8 | }); 9 | 10 | return results.join(''); 11 | }; -------------------------------------------------------------------------------- /src/lib/services/service-methods.js: -------------------------------------------------------------------------------- 1 | const getMethods = require('../get-methods'); 2 | const getClass = require('../get-class'); 3 | 4 | module.exports.get = function (ast) { 5 | const theClass = getClass(ast); 6 | const methods = getMethods.all(theClass); 7 | 8 | const results = methods.map(m => { 9 | return ast.text.slice(m.pos, m.end); 10 | }); 11 | 12 | return results.join(''); 13 | }; -------------------------------------------------------------------------------- /src/lib/services/service-properties.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | const getProperties = require('../get-properties'); 4 | 5 | module.exports.get = function (ast) { 6 | 7 | const methods = ast.statements.filter(x => x.kind === kind.MethodDeclaration); 8 | console.log(methods); 9 | const theClass = classes[0]; 10 | 11 | let results = []; 12 | 13 | if(theClass) { 14 | 15 | const properties = getProperties(theClass); 16 | 17 | results = properties.map((p) => { 18 | 19 | // We only want the one line property declarations 20 | if (!/;$/.test(ast.text.slice(p.pos, p.end))) { 21 | return; 22 | } 23 | 24 | return ast.text.slice(p.pos, p.end); 25 | 26 | }).filter(x=>!!x); 27 | } 28 | 29 | return results.join(''); 30 | }; -------------------------------------------------------------------------------- /src/lib/update-references.js: -------------------------------------------------------------------------------- 1 | const replacements = [ 2 | //Update imports from 'shared/... to 'app/shared/... 3 | [/'shared\//g, '\'app/shared/'], 4 | 5 | //Update imports from 'modules/... to 'app/modules/... 6 | [/'modules\//g, '\'app/modules/'], 7 | 8 | //Update path to images 9 | //[/(['`])src\/img\//g, '$1img/'], 10 | 11 | //Update path to images and data 12 | [/(['`])(src\/)?(img|data)\//g, '$1assets/$3/'], 13 | 14 | // Update path to dependencies 15 | [/!shared\/dependencies\//g, '!app/shared/dependencies/'] 16 | ]; 17 | 18 | module.exports = function(results) { 19 | for(r of replacements) { 20 | results = results.replace(r[0], r[1]); 21 | } 22 | return results; 23 | }; -------------------------------------------------------------------------------- /src/lib/variables.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const kind = ts.SyntaxKind; 3 | 4 | module.exports.get = function (ast) { 5 | const variables = ast.statements.filter(x => x.kind === kind.VariableStatement); 6 | 7 | const results = variables.map(v => { 8 | return ast.text.slice(v.pos, v.end); 9 | }); 10 | 11 | return results.join(''); 12 | }; -------------------------------------------------------------------------------- /src/lib/write-file.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | const makeDirectoriesInPath = require('./make-directories-in-path'); 3 | 4 | module.exports = function (path, text) { 5 | makeDirectoriesInPath(path); 6 | writeFileSync(path, text, { encoding: 'UTF-8' }); 7 | }; -------------------------------------------------------------------------------- /src/stubs/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const rimraf = require('rimraf'); 3 | const fs = require('fs'); 4 | const encoding = 'UTF-8'; 5 | const parser = require('../lib/parser'); 6 | const getMethods = require('../lib/get-methods'); 7 | const kind = require('../lib/kind'); 8 | const ensureDirectoryExistence = require('../lib/make-directories-in-path'); 9 | const getNamedImports = require('../lib/get-named-imports'); 10 | 11 | module.exports = function (files) { 12 | const tmpPath = path.join(process.cwd(), '.tmp/stubs'); 13 | const exports = []; 14 | 15 | // Clean the temp directory 16 | rimraf.sync(tmpPath); 17 | 18 | // Process each file from the glob 19 | files.forEach(file => { 20 | console.log(`Processing: ${file}`); 21 | 22 | const code = fs.readFileSync(file, encoding); 23 | const ast = parser.parse(code, {}); 24 | 25 | // Get the exported class 26 | const classes = ast.statements.filter(x => x.kind === kind.ClassDeclaration && x.modifiers && x.modifiers.some(x => x.kind === kind.ExportKeyword)); 27 | if (classes.length === 0) { 28 | return; 29 | } 30 | const theClass = classes[0]; 31 | 32 | // Get the imports for later use 33 | const namedImports = getNamedImports(ast); 34 | 35 | // Check to see if the class extends from any other service 36 | let extendsText = ''; 37 | const imports = []; 38 | if (theClass.heritageClauses && theClass.heritageClauses.length > 0) { 39 | theClass.heritageClauses.forEach((h) => { 40 | h.types.filter(x => x.expression && /service/i.test(x.expression.text)).forEach(t => { 41 | const extendedStub = `${t.expression.text}Stub`; 42 | extendsText += extendedStub; 43 | const namedImport = namedImports.find(x => x.name === t.expression.text); 44 | if(namedImport) { 45 | const importPath = path.basename(namedImport.path) + '.stub'; 46 | imports.push(`import { ${extendedStub} } from './${importPath}'`); 47 | } 48 | }); 49 | }); 50 | 51 | if (extendsText) { 52 | extendsText = ' extends ' + extendsText; 53 | } 54 | } 55 | 56 | // Get all the public method signatures 57 | const methods = getMethods.public(theClass); 58 | const signatures = methods.map(m => { 59 | let sig = ast.text.slice(m.pos, m.body ? m.body.pos : m.initializer.pos); 60 | 61 | // Removes comments from the file 62 | sig = sig.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, ''); 63 | 64 | // Replace ng.IPromise with Promise 65 | sig = sig.replace(/ng\.IPromise(<.*>)?/g, 'Promise'); 66 | 67 | const params = /\((.*)\)/g.exec(sig); 68 | if(params) { 69 | params[1].split(',').forEach(p => { 70 | if(p.split(':').length === 2) { 71 | sig = sig.replace(p.split(':')[1], ' any'); 72 | } 73 | }); 74 | } 75 | 76 | if(m.body) { 77 | return `${sig} { ${/Promise/.test(sig) ? '\n return Promise.resolve();\n }' : '}'}`; 78 | } 79 | else { 80 | return `${sig} [];`; 81 | } 82 | }); 83 | 84 | const className = `${theClass.name.text}Stub`; 85 | const stub = `${imports.join('\n')} 86 | 87 | export class ${className}${extendsText} { 88 | ${signatures.join('')} 89 | } 90 | `; // Break done to add new line to EOF 91 | 92 | // Write it out 93 | const outputFilePath = path.join(tmpPath, path.basename(file).replace('.ts', '.stub.ts')); 94 | ensureDirectoryExistence(outputFilePath); 95 | fs.writeFileSync(outputFilePath, stub, encoding); 96 | const outputFileName = path.basename(outputFilePath); 97 | exports.push(`export * from './${outputFileName.replace(/\.ts$/, '')}';`); 98 | console.log(`Created: ${outputFileName}`); 99 | }); 100 | 101 | const indexContent = `${exports.join('\n')}\n`; 102 | fs.writeFileSync(path.join(tmpPath, 'index.ts'), indexContent, encoding); 103 | 104 | console.log(`Created ${files.length} stubs in ${tmpPath.replace(process.cwd() + '/', '')}.`); 105 | }; 106 | -------------------------------------------------------------------------------- /src/templates/component-test.hbs: -------------------------------------------------------------------------------- 1 | import { NO_ERRORS_SCHEMA, DebugElement, Component } from '@angular/core'; 2 | {{#if importForms}} 3 | import { FormsModule } from '@angular/forms'; 4 | {{/if}} 5 | import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; 6 | import { By } from '@angular/platform-browser'; 7 | {{#if mocks}} 8 | import { mock, instance, when, verify } from 'ts-mockito'; 9 | 10 | {{#each imports}} 11 | {{{this}}} 12 | {{/each}} 13 | {{/if}} 14 | 15 | import { {{componentClass}} } from '{{componentPath}}'; 16 | 17 | @Component({ 18 | template: ` 19 | <{{selector}}{{#unless inputs}}>{{/unless}} 20 | {{#each inputs}} 21 | [{{name}}]="{{name}}"{{#if @last}}> 22 | {{/if}} 23 | {{/each}} 24 | ` 25 | }) 26 | class TestHostComponent { 27 | {{#each inputs}} 28 | public {{name}}; 29 | {{/each}} 30 | {{#if outputs}} 31 | 32 | // https://angular.io/guide/testing#test-a-component-inside-a-test-host-component 33 | {{#each outputs}} 34 | // "public {{name}}" output needs to be bound to the template above and tested 35 | {{/each}} 36 | {{/if}} 37 | } 38 | 39 | describe(' {{testName}}', () => { 40 | let component: {{componentClass}}; 41 | let fixture: ComponentFixture; 42 | let element: DebugElement; 43 | {{#each variables}} 44 | {{{this}}} 45 | {{/each}} 46 | 47 | beforeEach(() => { 48 | {{#each assignments}} 49 | {{{this}}} 50 | {{/each}} 51 | 52 | TestBed.configureTestingModule({ 53 | declarations: [ 54 | TestHostComponent, 55 | {{componentClass}} 56 | ], 57 | {{#if importForms}} 58 | imports: [ 59 | FormsModule 60 | ], 61 | {{/if}} 62 | {{#if providers}} 63 | providers: [ 64 | {{#each providers}} 65 | {{{this}}}{{#unless @last}},{{/unless}} 66 | {{/each}} 67 | ], 68 | {{/if}} 69 | schemas: [ NO_ERRORS_SCHEMA ] 70 | }); 71 | }); 72 | 73 | beforeEach(() => { 74 | fixture = TestBed.createComponent(TestHostComponent); 75 | element = fixture 76 | .debugElement 77 | .query(By.css('{{selector}}')); 78 | component = element.componentInstance; 79 | component.ngOnInit(); 80 | fixture.detectChanges(); 81 | }); 82 | {{#if testBedGets}} 83 | 84 | beforeEach(() => { 85 | {{#each testBedGets}} 86 | {{{this}}} 87 | {{/each}} 88 | }); 89 | 90 | {{/if}} 91 | {{#if specs}} 92 | {{{specs}}} 93 | {{/if}} 94 | {{#unless specs}} 95 | {{#each methods}} 96 | describe('#{{this}}', () => { 97 | // Write tests 98 | }); 99 | 100 | {{/each}} 101 | {{/unless}} 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /src/templates/service-test.hbs: -------------------------------------------------------------------------------- 1 | import { mock, instance, when, verify } from 'ts-mockito'; 2 | 3 | {{#each imports}} 4 | import { {{name}} } from '{{path}}'; 5 | {{/each}} 6 | 7 | {{#if specs}} 8 | {{{specs}}} 9 | {{/if}} 10 | -------------------------------------------------------------------------------- /src/tools/process-components.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const path = require('path'); 3 | const writeFile = require('../lib/write-file'); 4 | const removeFromConstructor = require('../lib/remove-from-constructor'); 5 | const getSourceFile = require('../lib/get-source-file'); 6 | const renderSourceFile = require('../lib/render-source-file'); 7 | const imports = require('../lib/components/component-imports'); 8 | const variables = require('../lib/variables'); 9 | const interfaces = require('../lib/components/component-interfaces'); 10 | const enums = require('../lib/components/component-enums'); 11 | const decorator = require('../lib/components/component-decorator'); 12 | const componentClass = require('../lib/components/component-class'); 13 | const makeDirectoriesInPath = require('../lib/make-directories-in-path'); 14 | const renderTemplate = require('../lib/render-template'); 15 | const methods = require('../lib/get-methods'); 16 | const getLifecycleEvents = require('../lib/components/get-lifecycle-events'); 17 | const sortBy = require('lodash.sortby'); 18 | const processStringReplacements = require('../lib/process-string-replacements'); 19 | const updateReferences = require('../lib/update-references'); 20 | const getMocks = require('../lib/get-mocks'); 21 | const config = require('../config'); 22 | const tmpPath = path.join(process.cwd(), config.outputRoot); 23 | const getRelatedSpecFilePath = require('../lib/get-related-spec-file-path'); 24 | const buildSpecs = require('../lib/build-specs'); 25 | const getOutputFilePath = require('../lib/get-output-file-path'); 26 | const replaceControllerStaticReferences = require('../lib/components/replace-controller-static-references'); 27 | const addEmitToOutputs = require('../lib/components/add-emit-to-outputs.js'); 28 | const pretty = require('../lib/pretty-print'); 29 | const getInputsAndOutputs = require('../lib/components/get-input-and-outputs-from-angular-4-component'); 30 | const getAngularTemplate = require('../angular/get-template'); 31 | 32 | module.exports = function(files) { 33 | sortBy(files).forEach(file => { 34 | // Read and parse the file's code 35 | let ast = getSourceFile(file); 36 | 37 | let component = `${imports.get(ast)}${variables.get( 38 | ast 39 | )}${interfaces.get(ast)}\n${enums.get(ast)}\n${decorator.get( 40 | file, 41 | ast 42 | )}\n${componentClass.get(ast)}`; 43 | 44 | ast = ts.createSourceFile(file, component, ts.ScriptTarget.TS, true); 45 | 46 | // Remove $q, $timeout, and $interval from the constructor 47 | ast = removeFromConstructor(ast, [ 48 | '$q', 49 | '$timeout', 50 | '$interval', 51 | '$routeParams', 52 | ]); 53 | 54 | // Render the ast back to typescript 55 | component = renderSourceFile(ast); 56 | 57 | // Try and clean up the formatting some 58 | component = pretty.printString(component); 59 | 60 | // Replace controller static references 61 | component = replaceControllerStaticReferences( 62 | getSourceFile(file), 63 | component 64 | ); 65 | 66 | // Do a bunch of string replacements 67 | component = processStringReplacements(component); 68 | 69 | // Update the references 70 | component = updateReferences(component); 71 | 72 | // Add emit to output usages within code 73 | component = addEmitToOutputs(getSourceFile(file), component); 74 | 75 | // Write it out 76 | const outputFilePath = getOutputFilePath(file); 77 | makeDirectoriesInPath(outputFilePath); 78 | writeFile(outputFilePath, component); 79 | 80 | // Create a test file 81 | const specFilePath = getRelatedSpecFilePath(file); 82 | const specs = specFilePath ? buildSpecs(specFilePath) : undefined; 83 | 84 | const newComponentAst = ts.createSourceFile( 85 | file, 86 | component, 87 | ts.ScriptTarget.TS, 88 | true 89 | ); 90 | const newComponentClass = newComponentAst.statements.find( 91 | x => x.kind === ts.SyntaxKind.ClassDeclaration 92 | ); 93 | const newComponentClassName = newComponentClass.name.text; 94 | const mocks = getMocks(newComponentAst); 95 | const inputsAndOutputs = getInputsAndOutputs(newComponentAst); 96 | const template = getAngularTemplate(outputFilePath); 97 | 98 | const lifecycleEvents = getLifecycleEvents(ast); 99 | let testFile = renderTemplate('component-test', { 100 | selector: /selector: '(.*)'/.exec(newComponentAst.text)[1], 101 | testName: newComponentClassName.replace(/([A-Z])/g, ' $1').trim(), 102 | componentClass: newComponentClassName, 103 | componentPath: `./${path 104 | .basename(outputFilePath) 105 | .replace(/\.ts$/, '')}`, 106 | specs, 107 | methods: lifecycleEvents.concat( 108 | methods.public(newComponentClass).map(m => m.name.text) 109 | ), 110 | mocks: sortBy(mocks, m => m.name), 111 | inputs: inputsAndOutputs.filter(x => x.type === 'Input'), 112 | outputs: inputsAndOutputs.filter(x => x.type === 'Output'), 113 | imports: mocks 114 | .filter(x => x.import) 115 | .map(x => x.import) 116 | .sort(), 117 | importForms: /"ngModel"/.test(template), 118 | providers: mocks 119 | .map(x => x.provide) 120 | .filter(x => !!x) 121 | .sort(), 122 | variables: mocks 123 | .map(x => x.variable) 124 | .filter(x => !!x) 125 | .sort(), 126 | assignments: mocks 127 | .map(x => x.assignment) 128 | .filter(x => !!x) 129 | .sort(), 130 | testBedGets: mocks.filter(x => x.useReal).map(x => { 131 | return `${x.mockName} = TestBed.get(${x.type});`; 132 | }), 133 | }); 134 | 135 | // Do a bunch of string replacements 136 | testFile = processStringReplacements(testFile); 137 | 138 | // Update References 139 | testFile = updateReferences(testFile); 140 | 141 | testFile = pretty.printString(testFile); 142 | 143 | // Reverting hack because of prettier choking 144 | testFile = testFile.replace(/constructxr/g, 'constructor'); 145 | 146 | writeFile(outputFilePath.replace('.ts', '.spec.ts'), testFile); 147 | 148 | console.log( 149 | `${outputFilePath 150 | .replace(process.cwd(), '') 151 | .replace('.ts', '.spec.ts')}` 152 | ); 153 | }); 154 | 155 | console.log( 156 | `Converted ${files.length} components in ${tmpPath.replace( 157 | process.cwd() + '/', 158 | '' 159 | )}.\n\n` 160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /src/tools/process-files.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const path = require('path'); 3 | const config = require('../config'); 4 | const writeFile = require('../lib/write-file'); 5 | const makeDirectoriesInPath = require('../lib/make-directories-in-path'); 6 | const sortBy = require('lodash.sortby'); 7 | const getOutputFilePath = require('../lib/get-output-file-path'); 8 | const tmpPath = path.join(process.cwd(), config.outputRoot); 9 | const { readFileSync } = require('fs'); 10 | 11 | module.exports = function (files) { 12 | 13 | sortBy(files).forEach(file => { 14 | 15 | //console.log(`Processing: ${path.basename(file)}.`); 16 | const contents = readFileSync(file).toString(); 17 | 18 | // Write it out 19 | const outputFilePath = getOutputFilePath(file); 20 | makeDirectoriesInPath(outputFilePath); 21 | writeFile(outputFilePath, contents); 22 | 23 | console.log(`${outputFilePath.replace(process.cwd(), '')}`); 24 | }); 25 | 26 | console.log(`Copied ${files.length} files in ${tmpPath.replace(process.cwd() + '/', '')}.\n\n`); 27 | }; -------------------------------------------------------------------------------- /src/tools/process-service-specs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('../config'); 3 | const tmpPath = path.join(process.cwd(), config.outputRoot); 4 | const updateReferences = require('../lib/update-references'); 5 | const processStringReplacements = require('../lib/process-string-replacements'); 6 | const getOutputFilePath = require('../lib/get-output-file-path'); 7 | const { printString } = require('../lib/pretty-print'); 8 | const readFile = require('../lib/read-file'); 9 | const writeFile = require('../lib/write-file'); 10 | const buildSpecs = require('../lib/build-specs'); 11 | const getNamedImports = require('../lib/get-named-imports'); 12 | const getSourceFile = require('../lib/get-source-file'); 13 | const renderTemplate = require('../lib/render-template'); 14 | 15 | module.exports = function(files) { 16 | 17 | files.forEach(file => { 18 | 19 | //console.log(`Processing: ${path.basename(file)}`); 20 | 21 | let code; 22 | 23 | //Model and vdom tests stand on their own so we don't comment out their code 24 | if (!/model|vdom/.test(file)) { 25 | const ast = getSourceFile(file); 26 | const namedImports = getNamedImports(ast); 27 | const specs = buildSpecs(file); 28 | 29 | code = renderTemplate('service-test', { 30 | testName: 'test', 31 | imports: namedImports, 32 | specs 33 | }); 34 | } 35 | else { 36 | code = readFile(file); 37 | // Try and clean up the formatting some 38 | // Have to replace constructor with constructxr as a hack prior to running prettier 39 | code = code.replace(/constructor/g, 'constructxr'); 40 | code = printString(code); 41 | // Now change it back 42 | code = code.replace(/constructxr/g, 'constructor'); 43 | } 44 | 45 | // Do a bunch of string replacements 46 | code = processStringReplacements(code); 47 | 48 | // Update the references 49 | code = updateReferences(code); 50 | 51 | // Write it out 52 | let outputFilePath = getOutputFilePath(file); 53 | outputFilePath = outputFilePath.replace('-spec', '.spec'); 54 | writeFile(outputFilePath, code); 55 | 56 | console.log(`${outputFilePath.replace(process.cwd(), '')}`); 57 | }); 58 | 59 | console.log(`Converted ${files.length} models/services specs in ${tmpPath.replace(process.cwd() + '/', '')}.\n\n`); 60 | }; -------------------------------------------------------------------------------- /src/tools/process-services.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('../config'); 3 | const tmpPath = path.join(process.cwd(), config.outputRoot); 4 | const getSourceFile = require('../lib/get-source-file'); 5 | const remove$Injects = require('../lib/remove-$injects'); 6 | const removeImports = require('../lib/remove-imports'); 7 | const removeFromConstructor = require('../lib/remove-from-constructor'); 8 | 9 | const renderSourceFile = require('../lib/render-source-file'); 10 | const processStringReplacements = require('../lib/process-string-replacements'); 11 | const updateReferences = require('../lib/update-references'); 12 | const serviceImports = require('../lib/services/service-imports'); 13 | const serviceHttp = require('../lib/services/service-http'); 14 | const getOutputFilePath = require('../lib/get-output-file-path'); 15 | const { printString } = require('../lib/pretty-print'); 16 | const writeFile = require('../lib/write-file'); 17 | 18 | module.exports = function(files) { 19 | 20 | files.forEach(file => { 21 | 22 | console.log(`Processing: ${path.basename(file)}`); 23 | 24 | // Read and parse the file's code 25 | let ast = getSourceFile(file); 26 | 27 | // Let do some stuff before we start modifying the AST 28 | const imports = serviceImports.get(ast); 29 | 30 | 31 | // Now we are modifying the AST 32 | 33 | // Remove all the $injects 34 | ast = remove$Injects(ast); 35 | 36 | // Remove the existing imports 37 | ast = removeImports(ast); 38 | 39 | // Remove $q, $timeout and $interval from the constructor 40 | ast = removeFromConstructor(ast, ['$q', '$timeout', '$interval']); 41 | 42 | 43 | 44 | // Render the ast back to typescript 45 | let code = renderSourceFile(ast); 46 | 47 | // Combine the code and new imports 48 | code = `${imports}${'\n'}${code}`; 49 | 50 | // Add @Injectable() 51 | code = code.replace(/export (abstract )?class/g, '@Injectable()\nexport $1class'); 52 | 53 | // Do a bunch of string replacements 54 | code = processStringReplacements(code); 55 | 56 | // Update the references 57 | code = updateReferences(code); 58 | 59 | // Try and clean up the formatting some 60 | code = printString(code); 61 | 62 | // Write it out 63 | const outputFilePath = getOutputFilePath(file); 64 | writeFile(outputFilePath, code); 65 | 66 | console.log(` Created: ${path.basename(outputFilePath)}\n`); 67 | }); 68 | 69 | console.log(`Converted ${files.length} models/services in ${tmpPath.replace(process.cwd() + '/', '')}.\n\n`); 70 | }; -------------------------------------------------------------------------------- /src/tools/process-templates.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const encoding = 'UTF-8'; 4 | const makeDirectoriesInPath = require('../lib/make-directories-in-path'); 5 | const getOutputFilePath = require('../lib/get-output-file-path'); 6 | const processTemplate = require('../lib/process-template'); 7 | const getSourceFile = require('../lib/get-source-file'); 8 | const getBindings = require('../lib/components/get-bindings'); 9 | const tmpPath = path.join(process.cwd(), './upgrade'); 10 | 11 | module.exports = function(templateFiles, componentFiles) { 12 | /** 13 | * key is tag selector 14 | * value is { 15 | * inputs: [], 16 | * outputs: [], 17 | * both: [] 18 | * } 19 | */ 20 | const componentDict = {}; 21 | 22 | (componentFiles || []).forEach(file => { 23 | // Estimation based on the filename - assumed consistent 24 | const componentKey = path.basename(file).replace(/\.component\.ts/, ''); 25 | 26 | const componentBindings = { 27 | input: [], 28 | output: [], 29 | 'two-way': [], 30 | }; 31 | const ast = getSourceFile(file); 32 | const bindings = getBindings(ast); 33 | for (let binding of bindings) { 34 | componentBindings[binding.type].push(binding.name); 35 | } 36 | componentDict[componentKey] = componentBindings; 37 | }); 38 | 39 | templateFiles.forEach(file => { 40 | console.log(`Processing: ${path.basename(file)}`); 41 | 42 | const template = fs.readFileSync(file, encoding); 43 | const outputTemplatePath = getOutputFilePath(file); 44 | makeDirectoriesInPath(outputTemplatePath); 45 | fs.writeFileSync( 46 | outputTemplatePath, 47 | processTemplate(template, componentDict), 48 | encoding 49 | ); 50 | 51 | console.log(` Processed: ${path.basename(outputTemplatePath)}`); 52 | }); 53 | 54 | console.log( 55 | `Processed ${templateFiles.length} templates in ${tmpPath.replace( 56 | process.cwd() + '/', 57 | '' 58 | )}.\n\n` 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /tests/add-emit-to-outputs.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import addEmitsToOutputs from '../src/lib/components/add-emit-to-outputs'; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const getAst = require('./utils/get-ast'); 6 | 7 | test('should return an array of the named imports', (t) => { 8 | const file = path.join(process.cwd(), 'tests/data/search-input.component.ts'); 9 | const code = fs.readFileSync(file, 'UTF-8'); 10 | t.is((code.match(/onQueryTextUpdated.emit\(/g) || []).length, 0); 11 | 12 | const ast = getAst(file); 13 | const result = addEmitsToOutputs(ast, code); 14 | 15 | t.is((result.match(/onQueryTextUpdated.emit\(/g) || []).length, 1); 16 | }); -------------------------------------------------------------------------------- /tests/component-constructor.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import constructor from '../src/lib/components/component-constructor'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const glob = require('glob'); 6 | 7 | const files = glob.sync(path.join(process.cwd(), 'tests/data/**/*component.ts')); 8 | 9 | files.forEach(file => { 10 | 11 | test('#get should not return $onInit property in contructor', t => { 12 | const ast = getAst(file); 13 | const result = constructor.get(ast); 14 | t.false(/\$onInit/.test(result), file); 15 | }); 16 | 17 | test('#get should not return $onChanges property in constructor', t => { 18 | const ast = getAst(file); 19 | const result = constructor.get(ast); 20 | t.false(/\$onChanges/.test(result), file); 21 | }); 22 | 23 | test('#get should not return $onDestroy property in constructor', t => { 24 | const ast = getAst(file); 25 | const result = constructor.get(ast); 26 | t.false(/\$onDestroy/.test(result), file); 27 | }); 28 | 29 | }); 30 | 31 | test('#get should return the contructor text', t => { 32 | const ast = getAst(path.join(process.cwd(), 'tests/data/account.component.ts')); 33 | const result = constructor.get(ast); 34 | t.deepEqual(result.trim(), ` 35 | constructor(private oauthService: OauthService, 36 | private redditService: RedditService, 37 | private $location: ng.ILocationService) { 38 | "ngInject"; 39 | }`.trim()); 40 | }); -------------------------------------------------------------------------------- /tests/component-decorator.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import decorator from '../src/lib/components/component-decorator'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | 6 | test('#get when component has a template url', (t) => { 7 | const file = path.join(process.cwd(), 'tests/data/account.component.ts'); 8 | const ast = getAst(file); 9 | const result = decorator.get('account.component.ts', ast); 10 | const expected = ` 11 | @Component({ 12 | selector: 'account', 13 | templateUrl: './account.component.html' 14 | })`; 15 | t.deepEqual(result, expected); 16 | }); 17 | 18 | test('#get when component has a template', (t) => { 19 | const file = path.join(process.cwd(), 'tests/data/app.component.ts'); 20 | const ast = getAst(file); 21 | const result = decorator.get('app.component.ts', ast); 22 | const expected = ` 23 | @Component({ 24 | selector: 'app', 25 | template: \`
26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 |
\` 34 | })`; 35 | t.deepEqual(result, expected); 36 | }); -------------------------------------------------------------------------------- /tests/component-imports.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import imports from '../src/lib/components/component-imports'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/account.component.ts'); 6 | 7 | test('#get', t => { 8 | const ast = getAst(file); 9 | const result = imports.get(ast); 10 | const expected = `import { Component, OnInit } from \'@angular/core\'; 11 | import { Credentials } from '../../shared/models/credentials.model'; 12 | import { Identity } from '../../shared/models/identity.model'; 13 | import { Location } from \'@angular/common\'; 14 | import { OauthService } from '../../shared/services/oauth.service'; 15 | import { RedditService } from '../../shared/services/reddit.service';`; 16 | t.deepEqual(result, expected); 17 | }); -------------------------------------------------------------------------------- /tests/component-inputs-and-outputs.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import inputsAndOutputs from '../src/lib/components/component-inputs-and-outputs'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | 6 | test('#get', (t) => { 7 | const file = path.join(process.cwd(), 'tests/data/search-input.component.ts'); 8 | const ast = getAst(file); 9 | const result = inputsAndOutputs.get(ast); 10 | const expected = ` 11 | @Input() public searchQuery; 12 | @Output() public onQueryTextUpdated: EventEmitter = new EventEmitter(); 13 | `; 14 | console.log(result); 15 | t.deepEqual(result, expected); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/component-interfaces.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import interfaces from '../src/lib/components/component-interfaces'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/events-manager.utility.ts'); 6 | 7 | test('#get', (t) => { 8 | const ast = getAst(file); 9 | const result = interfaces.get(ast); 10 | const expected = `// Event map is a dictionary with event name and list of callbacks 11 | interface IEventMap { 12 | [name: string]: Array<(data?: any) => void>; 13 | }`; 14 | t.deepEqual(result, expected); 15 | }); -------------------------------------------------------------------------------- /tests/component-methods.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import methods from '../src/lib/components/component-methods'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const glob = require('glob'); 6 | 7 | const files = glob.sync(path.join(process.cwd(), 'tests/data/**/*component.ts')); 8 | 9 | files.forEach(file => { 10 | 11 | test('#get should not return $inject property', t => { 12 | const ast = getAst(file); 13 | const result = methods.get(ast); 14 | t.true(result.indexOf('$inject') === -1, file); 15 | }); 16 | 17 | test('#get should not return $onInit property declaration', t => { 18 | const ast = getAst(file); 19 | const result = methods.get(ast); 20 | t.false(/[public|private] \$onInit/.test(result), file); 21 | }); 22 | 23 | test('#get should not return $onChanges property declaration', t => { 24 | const ast = getAst(file); 25 | const result = methods.get(ast); 26 | t.false(/[public|private] \$onChanges/.test(result), file); 27 | }); 28 | 29 | test('#get should not return $onDestroy property declaration', t => { 30 | const ast = getAst(file); 31 | const result = methods.get(ast); 32 | t.false(/[public|private] \$onDestroy/.test(result), file); 33 | }); 34 | 35 | test('#get should not return the constructor', t => { 36 | const ast = getAst(file); 37 | const result = methods.get(ast); 38 | t.false(/constructor/.test(result), file); 39 | }); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /tests/component-properties.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import properties from '../src/lib/components/component-properties'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const glob = require('glob'); 6 | 7 | const files = glob.sync(path.join(process.cwd(), 'tests/data/**/*component.ts')); 8 | 9 | files.forEach(file => { 10 | 11 | test('#get should not return $inject property', t => { 12 | const ast = getAst(file); 13 | const result = properties.get(ast); 14 | t.false(/\$inject/.test(result), file); 15 | }); 16 | 17 | test('#get should not return $onInit property', t => { 18 | const ast = getAst(file); 19 | const result = properties.get(ast); 20 | t.false(/[public|private] \$onInit/.test(result), file); 21 | }); 22 | 23 | test('#get should not return $onChanges property', t => { 24 | const ast = getAst(file); 25 | const result = properties.get(ast); 26 | t.false(/[public|private] \$onChanges/.test(result), file); 27 | }); 28 | 29 | test('#get should not return $onDestroy property', t => { 30 | const ast = getAst(file); 31 | const result = properties.get(ast); 32 | t.false(/[public|private] \$onDestroy/.test(result), file); 33 | }); 34 | 35 | test('#get should not return the constructor', t => { 36 | const ast = getAst(file); 37 | const result = properties.get(ast); 38 | t.false(/constructor/.test(result), file); 39 | }); 40 | }); -------------------------------------------------------------------------------- /tests/data/account.component.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from '../../shared/models/credentials.model'; 2 | import { OauthService } from '../../shared/services/oauth.service'; 3 | import { RedditService } from '../../shared/services/reddit.service'; 4 | import { Identity } from '../../shared/models/identity.model'; 5 | 6 | import * as template from './account.component.html'; 7 | 8 | export class AccountComponent implements ng.IComponentOptions { 9 | controller: ng.IControllerConstructor; 10 | template: string; 11 | 12 | constructor() { 13 | this.controller = AccountController; 14 | this.template = String(template); 15 | } 16 | } 17 | 18 | class AccountController implements ng.IComponentController { 19 | 20 | public credentials: Credentials; 21 | public identity: Identity; 22 | 23 | constructor(private oauthService: OauthService, 24 | private redditService: RedditService, 25 | private $location: ng.ILocationService) { 26 | "ngInject"; 27 | } 28 | 29 | public $onInit() { 30 | this.credentials = this.oauthService.getCredentials(); 31 | if (!this.credentials || this.credentials.is_expired) { 32 | return this.$location.path('/login'); 33 | } 34 | 35 | // Initialize request for 36 | this.redditService.getIdentity() 37 | .then((identity) => { 38 | this.identity = identity; 39 | }); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /tests/data/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Credentials } from './shared/models/credentials.model'; 2 | import { OauthService } from "./shared/services/oauth.service"; 3 | import { EventsManager } from './shared/utilities/events-manager.utility'; 4 | 5 | 6 | /** 7 | * App Component 8 | * 9 | * @export 10 | * @class AppComponent 11 | * @implements {ng.IComponentOptions} 12 | */ 13 | export class AppComponent implements ng.IComponentOptions { 14 | template: string; 15 | controller: ng.IControllerConstructor; 16 | 17 | constructor() { 18 | this.template = `
19 | 20 | 21 | 22 |
23 | 24 |
25 | 26 |
`; 27 | this.controller = AppController; 28 | } 29 | }; 30 | 31 | /** 32 | * App Controller 33 | * 34 | * @class AppController 35 | * @implements {ng.IComponentController} 36 | */ 37 | export class AppController implements ng.IComponentController { 38 | 39 | public credentials: Credentials; 40 | 41 | private subscription; 42 | 43 | constructor(private oauthService: OauthService, 44 | private eventsManager: EventsManager) { 45 | "ngInject"; 46 | } 47 | 48 | public $onInit() { 49 | this.retrieveCredentials(); 50 | this.subscription = this.eventsManager.subscribe('credentials:updated', () => { 51 | this.retrieveCredentials(); 52 | }); 53 | } 54 | 55 | public $onDestroy() { 56 | this.subscription(); 57 | } 58 | 59 | private retrieveCredentials() { 60 | const credentials = this.oauthService.getCredentials(); 61 | this.credentials = credentials && !credentials.is_expired ? 62 | credentials : null; 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /tests/data/events-manager.utility.ts: -------------------------------------------------------------------------------- 1 | // Event map is a dictionary with event name and list of callbacks 2 | interface IEventMap { 3 | [name: string]: Array<(data?: any) => void>; 4 | } 5 | 6 | export class EventsManager { 7 | 8 | public static $inject = [ 9 | '$timeout' 10 | ]; 11 | 12 | private eventMap: IEventMap; 13 | 14 | constructor(private $timeout: ng.ITimeoutService) { 15 | this.eventMap = {}; 16 | } 17 | 18 | /** 19 | * 20 | * @param name - event name that we are subscribing to 21 | * @param func - callback function that executes if event signal was subscribed to 22 | */ 23 | public subscribe = (name: string, func: (...args: any[]) => void): () => void => { 24 | if (!this.eventMap.hasOwnProperty(name)) { 25 | this.eventMap[name] = []; 26 | } 27 | 28 | let currentIndex = this.eventMap[name].length; 29 | this.eventMap[name].push(func); 30 | 31 | return () => { 32 | delete this.eventMap[name][currentIndex]; 33 | }; 34 | } 35 | 36 | /** 37 | * 38 | * @param name - event name that we are triggering/signaling 39 | * @param data - optional data that we can send as a parameter 40 | */ 41 | public publish = (name: string, data?: any): void => { 42 | 43 | if (this.eventMap.hasOwnProperty(name)) { 44 | for (let callback of this.eventMap[name]) { 45 | if (callback) { 46 | this.$timeout(() => { 47 | callback(data); 48 | }); 49 | } 50 | } 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tests/data/reddit.service.ts: -------------------------------------------------------------------------------- 1 | import { StorageUtility, StorageKeys } from "../utilities/storage.utility"; 2 | import { Credentials } from '../models/credentials.model'; 3 | import { OauthService } from './oauth.service'; 4 | import { Identity } from '../models/identity.model'; 5 | import { SearchQuery } from '../models/search-query.model'; 6 | 7 | export class RedditService { 8 | 9 | private static ANGULAR = 'angular'; 10 | private static DEFAULT_PARAMS = { 11 | count: 10 12 | }; 13 | 14 | private apiUrl: string; 15 | private authorizationUrl: string; 16 | private clientId: string; 17 | private redirectUrl: string; 18 | private scope: string[]; 19 | 20 | constructor( 21 | private $http: ng.IHttpService, 22 | private configuration, 23 | private oauthService: OauthService, 24 | private storageUtility: StorageUtility 25 | ) { 26 | this.apiUrl = this.configuration.reddit.apiUrl; 27 | } 28 | 29 | public getSubreddit(searchQuery?: SearchQuery) { 30 | const requestConfig = this.getRequestConfig(searchQuery); 31 | const url = searchQuery && searchQuery.isSearch() ? 32 | `${this.apiUrl}/r/${RedditService.ANGULAR}/search` : 33 | `${this.apiUrl}/r/${RedditService.ANGULAR}`; 34 | return this.$http.get(url, requestConfig) 35 | .then((response) => { 36 | return response.data; 37 | }); 38 | } 39 | 40 | public getIdentity() { 41 | const requestConfig = this.getRequestConfig(); 42 | return this.$http.get(`${this.apiUrl}/api/v1/me`, ) 43 | .then((response) => { 44 | return new Identity(response.data); 45 | }); 46 | } 47 | 48 | private getRequestConfig(searchQuery?: SearchQuery) { 49 | const isSearch: boolean = searchQuery && searchQuery.isSearch(); 50 | 51 | const credentials = this.oauthService.getCredentials(); 52 | return { 53 | params: isSearch ? searchQuery.toQuery() : RedditService.DEFAULT_PARAMS, 54 | headers: { 55 | Authorization: `Bearer ${credentials.access_token}` 56 | } 57 | }; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /tests/data/search-input.component.ts: -------------------------------------------------------------------------------- 1 | import { SearchQuery } from '../../models/search-query.model'; 2 | 3 | import * as template from './search-input.component.html'; 4 | 5 | const ENTER_KEYCODE = 13; 6 | 7 | export class SearchInputComponent implements ng.IComponentOptions { 8 | controller: ng.IControllerConstructor; 9 | template: string; 10 | bindings; 11 | 12 | constructor() { 13 | this.controller = SearchInputController; 14 | this.template = String(template); 15 | this.bindings = { 16 | searchQuery: '<', 17 | onQueryTextUpdated: '&' 18 | }; 19 | } 20 | } 21 | 22 | class SearchInputController implements ng.IComponentController { 23 | 24 | public searchQuery: SearchQuery; 25 | public queryText: string; 26 | public onQueryTextUpdated: Function; 27 | 28 | constructor() { 29 | "ngInject"; 30 | } 31 | 32 | public $onInit() { 33 | this.getQueryText(); 34 | } 35 | 36 | public $onChanges() { 37 | this.getQueryText(); 38 | } 39 | 40 | public initiateSearch(event, queryText) { 41 | if (event && event.keyCode !== ENTER_KEYCODE) { 42 | return; 43 | } 44 | this.onQueryTextUpdated({ $event: queryText }); 45 | } 46 | 47 | private getQueryText() { 48 | this.queryText = this.searchQuery ? this.searchQuery.queryText : ''; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/get-bindings.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import getBindings from '../src/lib/components/get-bindings'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | 6 | test('should return an array of the named imports', (t) => { 7 | const file = path.join(process.cwd(), 'tests/data/search-input.component.ts'); 8 | const ast = getAst(file); 9 | const result = getBindings(ast); 10 | const expected = [ 11 | { 12 | name: 'searchQuery', 13 | type: 'input', 14 | optional: false 15 | }, 16 | { 17 | name: 'onQueryTextUpdated', 18 | type: 'output', 19 | optional: false 20 | } 21 | ]; 22 | t.deepEqual(result, expected); 23 | }); -------------------------------------------------------------------------------- /tests/get-constructor.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import getConstructor from '../src/lib/get-constructor'; 3 | import getControllerClass from '../src/lib/components/get-controller-class'; 4 | const path = require('path'); 5 | const getAst = require('./utils/get-ast'); 6 | const file = path.join(process.cwd(), 'tests/data/account.component.ts'); 7 | 8 | test('should return the contructor', (t) => { 9 | const ast = getAst(file); 10 | const controllerClass = getControllerClass(ast); 11 | const controller = getConstructor(controllerClass); 12 | const result = ast.text.slice(controller.pos, controller.end); 13 | 14 | const expected = `constructor(private oauthService: OauthService, 15 | private redditService: RedditService, 16 | private $location: ng.ILocationService) { 17 | "ngInject"; 18 | }`; 19 | 20 | t.deepEqual(result.trim(), expected.trim()); 21 | 22 | }); -------------------------------------------------------------------------------- /tests/get-named-imports.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import getNamedImports from '../src/lib/get-named-imports'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/account.component.ts'); 6 | 7 | test('should return an array of the named imports', (t) => { 8 | const ast = getAst(file); 9 | const result = getNamedImports(ast); 10 | const expected = [ 11 | { 12 | name: 'Credentials', 13 | path: '../../shared/models/credentials.model' 14 | }, 15 | { 16 | name: 'Identity', 17 | path: '../../shared/models/identity.model' 18 | }, 19 | { 20 | name: 'OauthService', 21 | path: '../../shared/services/oauth.service' 22 | }, 23 | { 24 | name: 'RedditService', 25 | path: '../../shared/services/reddit.service' 26 | } 27 | ]; 28 | t.deepEqual(result, expected); 29 | }); -------------------------------------------------------------------------------- /tests/get-template-url.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import getTemplateUrl from '../src/lib/components/get-template-url'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/search-input.component.ts'); 6 | 7 | test('should return url for component that has a single template', (t) => { 8 | const ast = getAst(file); 9 | const result = getTemplateUrl(ast); 10 | const expected = '\'./search-input.component.html\''; 11 | t.deepEqual(result, expected); 12 | }); -------------------------------------------------------------------------------- /tests/process-string-replacements.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import processReplacements from '../src/lib/process-string-replacements'; 3 | 4 | test('should replace this.$q.when() with Promise.resolve()', t => { 5 | const text = 'this.$q.when()'; 6 | const result = processReplacements(text); 7 | t.is(result, 'Promise.resolve()'); 8 | }); 9 | 10 | test('should replace this.$q.all with Promise.all', t => { 11 | const text = 'this.$q.all'; 12 | const result = processReplacements(text); 13 | t.is(result, 'Promise.all'); 14 | }); 15 | 16 | test('should replace this.$q.when({}) with Promise.resolve({})', t => { 17 | const text = 'this.$q.when({})'; 18 | const result = processReplacements(text); 19 | t.is(result, 'Promise.resolve({})'); 20 | }); 21 | 22 | test('should replace this.$q.reject() with Promise.reject()', t => { 23 | const text = 'this.$q.reject(test)'; 24 | const result = processReplacements(text); 25 | t.is(result, 'Promise.reject(test)'); 26 | }); 27 | 28 | test('should replace let deferred = this.$q.defer(); with native promise', (t) => { 29 | const text = 'let deferred = this.$q.defer();'; 30 | const result = processReplacements(text); 31 | t.is(result, `let deferredResolve; 32 | let deferredReject; 33 | let deferred: Promise = new Promise((resolve, reject) => { 34 | deferredResolve = resolve; 35 | deferredReject = reject; 36 | });`); 37 | }); 38 | 39 | test('should replace let deferred: ng.IDeferred = this.$q.defer(); with native promise', (t) => { 40 | const text = 'let deferred: ng.IDeferred = this.$q.defer();'; 41 | const result = processReplacements(text); 42 | t.is(result, `let deferredResolve; 43 | let deferredReject; 44 | let deferred: Promise = new Promise((resolve, reject) => { 45 | deferredResolve = resolve; 46 | deferredReject = reject; 47 | });`); 48 | }); 49 | 50 | test('should replace this.$timeout.cancel with clearTimeout', t => { 51 | const text = 'this.$timeout.cancel(test)'; 52 | const result = processReplacements(text); 53 | t.is(result, 'clearTimeout(test)'); 54 | }); 55 | 56 | test('should replace this.$timeout with setTimeout', t => { 57 | const text = 'this.$timeout(() => {}, 200);'; 58 | const result = processReplacements(text); 59 | t.is(result, 'setTimeout(() => {}, 200);'); 60 | }); 61 | 62 | test('should replace this.$interval.cancel with clearTimeout', t => { 63 | const text = 'this.$interval.cancel(test)'; 64 | const result = processReplacements(text); 65 | t.is(result, 'clearInterval(test)'); 66 | }); 67 | 68 | test('should replace this.$interval with setInterval', t => { 69 | const text = 'this.$interval(() => {}, 200);'; 70 | const result = processReplacements(text); 71 | t.is(result, 'setInterval(() => {}, 200);'); 72 | }); 73 | 74 | test('should replace finally with then', t => { 75 | const text = ` .finally(() => { 76 | this.reorderInProgress = false; 77 | });`; 78 | const result = processReplacements(text); 79 | t.is(result, ` .then(() => { 80 | // Native promise does not have finally. This then will execute last. 81 | this.reorderInProgress = false; 82 | });`); 83 | }); 84 | 85 | test('should replace finally with then when typed', t => { 86 | const text = `.finally((): void => { 87 | this.reorderInProgress = false; 88 | });`; 89 | const result = processReplacements(text); 90 | t.is(result, `.then((): void => { 91 | // Native promise does not have finally. This then will execute last. 92 | this.reorderInProgress = false; 93 | });`); 94 | }); -------------------------------------------------------------------------------- /tests/process-template.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import processTemplate from '../src/lib/process-template'; 3 | 4 | // Cheerio replacements // 5 | 6 | test('replaces ng-bind with expression', (t) => { 7 | const template = ''; 8 | const result = processTemplate(template); 9 | const expected = `{{favoritesNavLink}} 11 | {{nested}}`; 12 | t.deepEqual(result, expected); 13 | }); 14 | 15 | test('replaces ng-show with hidden attribute', t => { 16 | const template = ''; 17 | const result = processTemplate(template); 18 | const expected = ``; 21 | t.deepEqual(result, expected); 22 | }); 23 | 24 | test('if the same element has ng-if and ng-repeat, ng-if is moved to an ng-container and wraps ng-repeat', t => { 25 | const template = '
'; 26 | const result = processTemplate(template); 27 | const expected = ` 28 |
29 |
30 | 31 |
32 |
`; 33 | t.deepEqual(result, expected); 34 | }); 35 | 36 | // String replacements // 37 | test('replaces ng-if with *ngIf', t => { 38 | const template = ''; 39 | const result = processTemplate(template); 40 | const expected = ''; 41 | t.deepEqual(result, expected); 42 | }); 43 | 44 | test('replaces one time bindings in text', t => { 45 | const template = ''; 46 | const result = processTemplate(template); 47 | const expected = ''; 48 | t.deepEqual(result, expected); 49 | }); 50 | 51 | test('replaces one time bindings in attribute', t => { 52 | const template = ''; 53 | const result = processTemplate(template); 54 | const expected = ''; 55 | t.deepEqual(result, expected); 56 | }); 57 | 58 | test('removes $ctrl. from template', t => { 59 | const template = ''; 60 | const result = processTemplate(template); 61 | const expected = ''; 62 | t.deepEqual(result, expected); 63 | }); 64 | 65 | test('replaces ng-click with (click)', t => { 66 | const template = ''; 67 | const result = processTemplate(template); 68 | const expected = ''; 69 | t.deepEqual(result, expected); 70 | }); 71 | 72 | test('replaces ng-dblclick with (dblclick)', t => { 73 | const template = ''; 74 | const result = processTemplate(template); 75 | const expected = ''; 76 | t.deepEqual(result, expected); 77 | }); 78 | 79 | test('replaces ng-blur with (blur)', t => { 80 | const template = ''; 81 | const result = processTemplate(template); 82 | const expected = ''; 83 | t.deepEqual(result, expected); 84 | }); 85 | 86 | test('replaces ng-focus with (focus)', t => { 87 | const template = ''; 88 | const result = processTemplate(template); 89 | const expected = ''; 90 | t.deepEqual(result, expected); 91 | }); 92 | 93 | 94 | test('replaces ng-mouseenter with (mouseenter)', t => { 95 | const template = ''; 96 | const result = processTemplate(template); 97 | const expected = ''; 98 | t.deepEqual(result, expected); 99 | }); 100 | 101 | test('replaces ng-mouseleave with (mouseleave)', t => { 102 | const template = ''; 103 | const result = processTemplate(template); 104 | const expected = ''; 105 | t.deepEqual(result, expected); 106 | }); 107 | 108 | test('replaces ng-mouseover with (mouseover)', t => { 109 | const template = ''; 110 | const result = processTemplate(template); 111 | const expected = ''; 112 | t.deepEqual(result, expected); 113 | }); 114 | 115 | test('replaces ng-keypress with (keypress)', t => { 116 | const template = ''; 117 | const result = processTemplate(template); 118 | const expected = ''; 119 | t.deepEqual(result, expected); 120 | }); 121 | 122 | test('replaces ng-keydown with (keydown)', t => { 123 | const template = ''; 124 | const result = processTemplate(template); 125 | const expected = ''; 126 | t.deepEqual(result, expected); 127 | }); 128 | 129 | test('replaces ng-change with (change)', t => { 130 | const template = ``; 133 | const result = processTemplate(template); 134 | const expected = ``; 136 | t.deepEqual(result, expected); 137 | }); 138 | 139 | test('replaces ng-submit with (ngSubmit)', t => { 140 | const template = ''; 141 | const result = processTemplate(template); 142 | const expected = ''; 143 | t.deepEqual(result, expected); 144 | }); 145 | 146 | 147 | test('replaces ng-href with href', t => { 148 | const template = 'test'; 149 | const result = processTemplate(template); 150 | const expected = 'test'; 151 | t.deepEqual(result, expected); 152 | }); 153 | 154 | test('replaces ng-class with [ngClass]', t => { 155 | const template = ''; 156 | const result = processTemplate(template); 157 | const expected = ''; 158 | t.deepEqual(result, expected); 159 | }); 160 | 161 | test('replaces ngStyle with [ngStyle]', t => { 162 | const template = ''; 163 | const result = processTemplate(template); 164 | const expected = ''; 165 | t.deepEqual(result, expected); 166 | }); 167 | 168 | test('replaces ng-src with [src]', t => { 169 | const template = ''; 170 | const result = processTemplate(template); 171 | const expected = ``; 173 | t.deepEqual(result, expected); 174 | }); 175 | 176 | test('replaces ng-bind-html with [innerHtml]', t => { 177 | const template = ''; 178 | const result = processTemplate(template); 179 | const expected = ``; 181 | t.deepEqual(result, expected); 182 | }); 183 | 184 | test('replaces ng-hide with hidden attribute', t => { 185 | const template = ''; 186 | const result = processTemplate(template); 187 | const expected = ''; 188 | t.deepEqual(result, expected); 189 | }); 190 | 191 | test('replaces ng-disabled with disabled attribute', t => { 192 | const template = ''; 193 | const result = processTemplate(template); 194 | const expected = ''; 195 | t.deepEqual(result, expected); 196 | }); 197 | 198 | test('replaces ng-checked with checked attribute', t => { 199 | const template = ''; 200 | const result = processTemplate(template); 201 | const expected = ``; 203 | t.deepEqual(result, expected); 204 | }); 205 | 206 | test('replaces ng-model with [(ngModel])', t => { 207 | const template = ''; 208 | const result = processTemplate(template); 209 | const expected = ``; 211 | t.deepEqual(result, expected); 212 | }); 213 | 214 | test('replaces ng-value with [value]', t => { 215 | const template = ''; 216 | const result = processTemplate(template); 217 | const expected = ``; 219 | t.deepEqual(result, expected); 220 | }); 221 | 222 | test('replaces ng-required with [required]', t => { 223 | const template = ''; 224 | const result = processTemplate(template); 225 | const expected = ``; 227 | t.deepEqual(result, expected); 228 | }); 229 | 230 | test('replaces ng-pattern with [pattern]', t => { 231 | const template = ''; 232 | const result = processTemplate(template); 233 | const expected = ``; 235 | t.deepEqual(result, expected); 236 | }); 237 | 238 | test('replaces ng-repeat with *ngFor', t => { 239 | const template = '
'; 240 | const result = processTemplate(template); 241 | const expected = '
'; 242 | t.deepEqual(result, expected); 243 | }); 244 | 245 | test('replaces ng-repeat with *ngFor when ng-repeat breaks across lines', t => { 246 | const template = `
248 |
`; 249 | 250 | const result = processTemplate(template); 251 | 252 | const expected = `
254 |
`; 255 | t.deepEqual(result, expected); 256 | }); 257 | 258 | test('replace limitTo pipe with slice pipe', t => { 259 | const template = `
  • 260 |
  • `; 261 | const result = processTemplate(template); 262 | const expected = `
  • 263 |
  • `; 264 | 265 | t.deepEqual(result, expected); 266 | }); 267 | 268 | test('replace limitTo pipe with slice pipe when limitTo is complex', t => { 269 | const template = `
  • 271 |
  • `; 272 | const result = processTemplate(template); 273 | const expected = `
  • 274 |
  • `; 275 | 276 | t.deepEqual(result, expected); 277 | }); 278 | 279 | test('replaces limitTo pipe in binding', t => { 280 | const template = ''; 281 | const result = processTemplate(template); 282 | const expected = '{{itemController.getOrderSummaryText(slide) | slice:0:22}}'; 283 | 284 | t.deepEqual(result, expected); 285 | }); 286 | 287 | test('replaces ng-switch with [ngSwitch]', t => { 288 | const template = ` 289 | 290 | Junk 291 | 292 | 293 | More Junk 294 | 295 | `; 296 | 297 | const result = processTemplate(template); 298 | 299 | const expected = ` 300 | 301 | Junk 302 | 303 | 304 | More Junk 305 | 306 | `; 307 | t.deepEqual(result, expected); 308 | }); -------------------------------------------------------------------------------- /tests/service-http.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import imports from '../src/lib/services/service-imports'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/reddit.service.ts'); 6 | 7 | test('#get', t => { 8 | const ast = getAst(file); 9 | const result = imports.get(ast); 10 | const expected = `import { Credentials } from '../models/credentials.model'; 11 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 12 | import { Identity } from '../models/identity.model'; 13 | import { Injectable, Inject } from '@angular/core'; 14 | import { OauthService } from './oauth.service'; 15 | import { SearchQuery } from '../models/search-query.model'; 16 | import { StorageUtility, StorageKeys } from "../utilities/storage.utility";`; 17 | t.deepEqual(result, expected); 18 | }); -------------------------------------------------------------------------------- /tests/service-imports.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import imports from '../src/lib/services/service-imports'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/reddit.service.ts'); 6 | 7 | test('#get', t => { 8 | const ast = getAst(file); 9 | const result = imports.get(ast); 10 | const expected = `import { Credentials } from '../models/credentials.model'; 11 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 12 | import { Identity } from '../models/identity.model'; 13 | import { Injectable, Inject } from '@angular/core'; 14 | import { OauthService } from './oauth.service'; 15 | import { SearchQuery } from '../models/search-query.model'; 16 | import { StorageUtility, StorageKeys } from "../utilities/storage.utility";`; 17 | t.deepEqual(result, expected); 18 | }); -------------------------------------------------------------------------------- /tests/utils/get-ast.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const parser = require('../../src/lib/parser'); 3 | 4 | module.exports = function(path) { 5 | const code = fs.readFileSync(path, 'UTF-8'); 6 | const ast = parser.parse(code, {}); 7 | return ast; 8 | }; -------------------------------------------------------------------------------- /tests/variables.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import variables from '../src/lib/variables'; 3 | const path = require('path'); 4 | const getAst = require('./utils/get-ast'); 5 | const file = path.join(process.cwd(), 'tests/data/search-input.component.ts'); 6 | 7 | test('#get', (t) => { 8 | const ast = getAst(file); 9 | const result = variables.get(ast); 10 | const expected = ` 11 | 12 | const ENTER_KEYCODE = 13;`; 13 | t.deepEqual(result, expected); 14 | }); --------------------------------------------------------------------------------