├── .npmignore ├── .travis.yml ├── packages ├── hekla-cli │ ├── test │ │ ├── cases │ │ │ └── simple │ │ │ │ ├── src │ │ │ │ ├── a.js │ │ │ │ ├── b.js │ │ │ │ ├── c.js │ │ │ │ └── index.js │ │ │ │ ├── hekla.config.js │ │ │ │ └── testcase.js │ │ └── helpers │ │ │ └── setup.js │ ├── bin │ │ └── hekla │ ├── package.json │ ├── src │ │ ├── run.js │ │ └── commands │ │ │ └── analyze.js │ ├── README.md │ └── jest.config.js ├── hekla-webpack-plugin │ ├── test │ │ ├── helpers │ │ │ └── setup.js │ │ └── cases │ │ │ └── simple │ │ │ ├── src │ │ │ └── index.js │ │ │ ├── hekla.config.js │ │ │ ├── webpack.config.js │ │ │ └── testcase.js │ ├── src │ │ ├── index.js │ │ └── HeklaWebpackPlugin.js │ ├── package.json │ ├── README.md │ └── jest.config.js ├── hekla-core │ ├── src │ │ ├── plugins │ │ │ ├── OwnershipPlugin │ │ │ │ ├── index.js │ │ │ │ ├── OwnershipPlugin.js │ │ │ │ └── ProjectTreeAnnotations.js │ │ │ ├── LinesOfCodePlugin.js │ │ │ ├── index.js │ │ │ ├── ListImportsPlugin.js │ │ │ └── StatusUpdatePlugin.js │ │ ├── legacy │ │ │ └── parsers │ │ │ │ ├── index.js │ │ │ │ ├── DefaultParser │ │ │ │ ├── test-examples │ │ │ │ │ ├── es6.js │ │ │ │ │ └── commonjs.js │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ │ ├── AngularDirectiveParser │ │ │ │ ├── test-examples │ │ │ │ │ ├── basic.html │ │ │ │ │ ├── no-scope.js │ │ │ │ │ ├── minimal.js │ │ │ │ │ ├── template-url.js │ │ │ │ │ ├── webpack-loader-template.js │ │ │ │ │ ├── inline-concat.js │ │ │ │ │ ├── basic.js │ │ │ │ │ ├── template-url-function.js │ │ │ │ │ ├── inline-array-join.js │ │ │ │ │ ├── inline-variable.js │ │ │ │ │ ├── inline.js │ │ │ │ │ ├── injected.js │ │ │ │ │ ├── two-directives.js │ │ │ │ │ └── split-definition-injected.js │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ │ ├── BaseParser │ │ │ │ └── index.js │ │ │ │ └── AngularFactoryParser │ │ │ │ ├── test-examples │ │ │ │ ├── basic.js │ │ │ │ └── split-definition.js │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ ├── index.js │ │ ├── utils │ │ │ ├── ast-utils │ │ │ │ ├── test-examples │ │ │ │ │ └── imports.js │ │ │ │ ├── ASTWrapper.js │ │ │ │ ├── DOMWrapper.js │ │ │ │ ├── ASTWrapper.spec.js │ │ │ │ ├── DOMWrapper.spec.js │ │ │ │ ├── index.spec.js │ │ │ │ └── index.js │ │ │ ├── fs-utils │ │ │ │ ├── index.spec.js │ │ │ │ └── index.js │ │ │ ├── parser-result │ │ │ │ ├── index.js │ │ │ │ └── index.spec.js │ │ │ ├── dependency-graph │ │ │ │ ├── index.spec.js │ │ │ │ └── index.js │ │ │ └── ng-utils │ │ │ │ └── index.js │ │ ├── Module.js │ │ ├── ConfigValidator.js │ │ ├── Module.spec.js │ │ ├── StatusMessage.js │ │ ├── WorkQueue.spec.js │ │ ├── WorkQueue.js │ │ ├── ConfigValidator.spec.js │ │ ├── Analyzer.js │ │ └── Analyzer.spec.js │ ├── test │ │ └── test-helpers.js │ ├── package.json │ ├── README.md │ ├── LICENSE │ └── package-lock.json ├── hekla-viewer │ ├── src │ │ ├── favicon.ico │ │ ├── ContentPane │ │ │ ├── ContentPane.css │ │ │ ├── DependencyChart │ │ │ │ ├── constants.js │ │ │ │ ├── ComponentBox.js │ │ │ │ ├── DependencyChart.css │ │ │ │ ├── ComponentDependencyArrow.js │ │ │ │ ├── ComponentContextMenu.js │ │ │ │ └── index.js │ │ │ ├── ComponentSearcher │ │ │ │ ├── ComponentSearcher.css │ │ │ │ ├── ComponentSearchBar │ │ │ │ │ ├── ComponentSearchBar.css │ │ │ │ │ └── index.js │ │ │ │ ├── ComponentSearchResults │ │ │ │ │ ├── index.js │ │ │ │ │ └── ComponentSearchResults.css │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── components │ │ │ ├── PaneTitle │ │ │ │ ├── PaneTitle.css │ │ │ │ └── index.js │ │ │ └── ScrollingPane │ │ │ │ ├── ScrollingPane.css │ │ │ │ └── index.js │ │ ├── header │ │ │ └── Header.css │ │ ├── App.test.js │ │ ├── index.css │ │ ├── Sidebar │ │ │ ├── Sidebar.css │ │ │ ├── ComponentDetailsPane │ │ │ │ ├── ComponentDetailsPane.css │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Header │ │ │ └── index.js │ │ ├── App.css │ │ ├── index.js │ │ └── App.js │ ├── index.html │ ├── README.md │ ├── package.json │ ├── lib │ │ └── server │ │ │ └── index.js │ └── LICENSE └── hekla-plugin-csv-reporter │ ├── src │ ├── index.js │ └── CSVReporterPlugin.js │ ├── package.json │ ├── package-lock.json │ └── README.md ├── assets └── intro.png ├── lerna.json ├── package.json ├── .gitignore ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | packages/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/src/a.js: -------------------------------------------------------------------------------- 1 | export default 'a'; 2 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/src/b.js: -------------------------------------------------------------------------------- 1 | export default 'b'; 2 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/src/c.js: -------------------------------------------------------------------------------- 1 | export default 'c'; 2 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/helpers/setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(60 * 1000); 2 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/test/helpers/setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(60 * 1000); 2 | -------------------------------------------------------------------------------- /assets/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewjensen/hekla/HEAD/assets/intro.png -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/test/cases/simple/src/index.js: -------------------------------------------------------------------------------- 1 | console.log('hello world'); 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.4", 3 | "packages": [ 4 | "packages/*" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/OwnershipPlugin/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./OwnershipPlugin'); 2 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewjensen/hekla/HEAD/packages/hekla-viewer/src/favicon.ico -------------------------------------------------------------------------------- /packages/hekla-cli/bin/hekla: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node --harmony 2 | 3 | const run = require('../src/run'); 4 | 5 | run(process.argv); 6 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ContentPane.css: -------------------------------------------------------------------------------- 1 | .ContentPane { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | } 6 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/components/PaneTitle/PaneTitle.css: -------------------------------------------------------------------------------- 1 | .PaneTitle { 2 | padding: 16px; 3 | font-size: 20px; 4 | font-weight: bold; 5 | } 6 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/src/index.js: -------------------------------------------------------------------------------- 1 | const HeklaWebpackPlugin = require('./HeklaWebpackPlugin'); 2 | 3 | module.exports = HeklaWebpackPlugin; 4 | -------------------------------------------------------------------------------- /packages/hekla-plugin-csv-reporter/src/index.js: -------------------------------------------------------------------------------- 1 | const CSVReporterPlugin = require('./CSVReporterPlugin'); 2 | 3 | module.exports = CSVReporterPlugin; 4 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import valueA from './a'; 2 | import valueB from './b'; 3 | import valueC from './c'; 4 | 5 | console.log('hello world:', valueA, valueB, valueC); 6 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/header/Header.css: -------------------------------------------------------------------------------- 1 | /*.Header { 2 | padding: 15px; 3 | border-bottom: 1px solid #999; 4 | } 5 | 6 | .Header > h1 { 7 | font-size: 16px; 8 | padding: 0; 9 | }*/ 10 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | AngularDirectiveParser: require('./AngularDirectiveParser'), 5 | AngularFactoryParser: require('./AngularFactoryParser') 6 | }; 7 | -------------------------------------------------------------------------------- /packages/hekla-core/test/test-helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | global.expect = require('chai').expect; 6 | 7 | global.loadContents = function(filename) { 8 | return fs.readFileSync(filename, 'utf-8'); 9 | } 10 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/components/PaneTitle/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './PaneTitle.css'; 4 | 5 | const PaneTitle = (props) => ( 6 |
{props.text}
7 | ); 8 | 9 | export default PaneTitle; 10 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/DefaultParser/test-examples/es6.js: -------------------------------------------------------------------------------- 1 | import superCool from 'superCool'; 2 | import superFun from 'superFun'; 3 | 4 | export function firstFunction() { 5 | 6 | } 7 | 8 | export function secondFunction() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/components/ScrollingPane/ScrollingPane.css: -------------------------------------------------------------------------------- 1 | .ScrollingPane { 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | } 6 | 7 | .ScrollingPane-contents { 8 | width: 100%; 9 | height: 100%; 10 | overflow: auto; 11 | } 12 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/basic.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{title}}
4 |
Dogs: {{dogs}}
5 |
Cats: {{cats}}
6 |
7 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/BaseParser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class BaseParser { 4 | constructor() { 5 | 6 | } 7 | 8 | extractComponents(module) { 9 | return Promise.reject(new Error('Abstract method extractComponents not implemented')); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/no-scope.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | template: require('./basic.html'), 4 | link: function (scope, el, attrs) { 5 | console.log('This is my pet shop!'); 6 | } 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/test/cases/simple/hekla.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { 4 | LinesOfCodePlugin 5 | } = require('hekla-core').plugins; 6 | 7 | module.exports = { 8 | rootPath: path.resolve(__dirname), 9 | 10 | plugins: [ 11 | new LinesOfCodePlugin() 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/DefaultParser/test-examples/commonjs.js: -------------------------------------------------------------------------------- 1 | var thingOne = require('thingOne'); 2 | var thingTwo = require('thingTwo'); 3 | 4 | module.exports = { 5 | firstFunction, 6 | secondFunction 7 | }; 8 | 9 | function firstFunction() { 10 | 11 | } 12 | 13 | function secondFunction() { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/minimal.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myMouseHover', function() { 2 | 'use strict'; 3 | return function myMouseHoverLinkFn(scope, el, attrs) { 4 | el.on('mouseenter', function(event) { 5 | console.log('you hovered on me!'); 6 | }); 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | margin: 0; 4 | overflow: hidden; 5 | font-family: 'Roboto', sans-serif; 6 | } 7 | 8 | body { 9 | height: 100%; 10 | margin: 0; 11 | } 12 | 13 | #root { 14 | height: 100%; 15 | } 16 | 17 | h1, h2, h3, h4, h5, h6 { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/Sidebar/Sidebar.css: -------------------------------------------------------------------------------- 1 | .Sidebar { 2 | display: flex; 3 | flex-direction: column; 4 | box-sizing: border-box; 5 | height: 100%; 6 | width: 100%; 7 | border-left: 1px solid #999; 8 | background-color: #ffffff; 9 | } 10 | 11 | .Sidebar > .details-container { 12 | flex: 1; 13 | overflow: hidden; 14 | } 15 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/template-url.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: {}, 4 | templateUrl: '/test-examples/basic.html', 5 | link: function (scope, el, attrs) { 6 | console.log('This is my pet shop!'); 7 | } 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/webpack-loader-template.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: { 4 | title: '@', 5 | dogs: '=', 6 | cats: '=', 7 | 'quoted': '=' 8 | }, 9 | template: require('text!./basic.html') 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularFactoryParser/test-examples/basic.js: -------------------------------------------------------------------------------- 1 | angular.module('app').factory('animalService', function($http, anotherService) { 2 | return { 3 | fetchAnimals: fetchAnimals, 4 | fetchOneAnimal: fetchOneAnimal 5 | }; 6 | 7 | function fetchAnimals() { } 8 | 9 | function fetchOneAnimal(animalId) { } 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /packages/hekla-core/src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Analyzer: require('./Analyzer'), 3 | Module: require('./Module'), 4 | ConfigValidator: require('./ConfigValidator'), 5 | DependencyGraph: require('./utils/dependency-graph'), 6 | astUtils: require('./utils/ast-utils'), 7 | fsUtils: require('./utils/fs-utils'), 8 | plugins: require('./plugins') 9 | }; 10 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/LinesOfCodePlugin.js: -------------------------------------------------------------------------------- 1 | module.exports = class LinesOfCodePlugin { 2 | apply(analyzer) { 3 | analyzer.hooks.moduleRawSource.tap('LinesOfCodePlugin', this.moduleRawSource.bind(this)); 4 | } 5 | 6 | moduleRawSource(module, source) { 7 | const lines = source.split('\n').length; 8 | module.set('lines', lines); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/test-examples/imports.js: -------------------------------------------------------------------------------- 1 | import somethingGreat from 'somethingGreat'; 2 | 3 | var thingOne = require('thingOne'); 4 | var thingTwo = require('thingTwo'); 5 | 6 | module.exports = { 7 | firstFunction, 8 | secondFunction 9 | }; 10 | 11 | function firstFunction() { 12 | 13 | } 14 | 15 | function secondFunction() { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/constants.js: -------------------------------------------------------------------------------- 1 | // Grid settings 2 | export const OFFSET_X = 48; 3 | export const OFFSET_Y = 100; 4 | export const GRID_X = 250; 5 | export const GRID_Y = 100; 6 | 7 | // Component sizing 8 | export const COMPONENT_BOX_WIDTH = 200; 9 | export const COMPONENT_BOX_HEIGHT = 50; 10 | export const COMPONENT_BOX_TEXT_OFFSET = 30; 11 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/Sidebar/ComponentDetailsPane/ComponentDetailsPane.css: -------------------------------------------------------------------------------- 1 | .ComponentDetailsPane { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | border-top: 1px solid #ccc; 7 | } 8 | 9 | .ComponentDetailsPane-title { 10 | flex: 0; 11 | } 12 | 13 | .ComponentDetailsPane-content { 14 | flex: 1; 15 | overflow: hidden; 16 | } 17 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularFactoryParser/test-examples/split-definition.js: -------------------------------------------------------------------------------- 1 | angular.module('app').factory('animalService', animalService); 2 | 3 | function animalService($http, anotherService) { 4 | return { 5 | fetchAnimals: fetchAnimals, 6 | fetchOneAnimal: fetchOneAnimal 7 | }; 8 | 9 | function fetchAnimals() { } 10 | 11 | function fetchOneAnimal(animalId) { } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | const LinesOfCodePlugin = require('./LinesOfCodePlugin'); 2 | const ListImportsPlugin = require('./ListImportsPlugin'); 3 | const OwnershipPlugin = require('./OwnershipPlugin'); 4 | const StatusUpdatePlugin = require('./StatusUpdatePlugin'); 5 | 6 | module.exports = { 7 | LinesOfCodePlugin, 8 | ListImportsPlugin, 9 | OwnershipPlugin, 10 | StatusUpdatePlugin 11 | }; 12 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/inline-concat.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: {}, 4 | template: 5 | '
' + 6 | '' + 7 | '
', 8 | link: function (scope, el, attrs) { 9 | console.log('This is my pet shop!'); 10 | } 11 | }; 12 | }); 13 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/basic.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: { 4 | title: '@', 5 | dogs: '=', 6 | cats: '=', 7 | 'quoted': '=' 8 | }, 9 | template: require('./basic.html'), 10 | link: function (scope, el, attrs) { 11 | console.log('This is my pet shop!'); 12 | } 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/template-url-function.js: -------------------------------------------------------------------------------- 1 | var TemplateUrlModule = require('path/to/TemplateUrlModule'); 2 | 3 | angular.module('app').directive('myPetShop', function() { 4 | return { 5 | scope: {}, 6 | templateUrl: TemplateUrlModule.resolve('myPetShop'), 7 | link: function (scope, el, attrs) { 8 | console.log('This is my pet shop!'); 9 | } 10 | }; 11 | }); 12 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/ComponentSearcher.css: -------------------------------------------------------------------------------- 1 | .ComponentSearcher { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | pointer-events: none; 8 | } 9 | 10 | .ComponentSearcher > .content { 11 | margin: 24px 48px; 12 | overflow: hidden; 13 | max-height: calc(100% - 48px); 14 | display: flex; 15 | flex-direction: column; 16 | pointer-events: auto; 17 | } 18 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import AppBar from 'material-ui/AppBar'; 3 | 4 | import './Header.css'; 5 | 6 | class Header extends Component { 7 | render() { 8 | return ( 9 | 14 | ); 15 | } 16 | } 17 | 18 | export default Header; 19 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/ListImportsPlugin.js: -------------------------------------------------------------------------------- 1 | const { getImportInfo } = require('../utils/ast-utils'); 2 | 3 | module.exports = class ListImportsPlugin { 4 | apply(analyzer) { 5 | analyzer.hooks.moduleSyntaxTreeJS.tap('ListImportsPlugin', this.moduleSyntaxTreeJS.bind(this)); 6 | } 7 | 8 | moduleSyntaxTreeJS(module, ast) { 9 | const imports = getImportInfo(ast.unwrap()); 10 | module.set('imports', imports); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/inline-array-join.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: {}, 4 | template: [ 5 | '
', 6 | '', 7 | '
', 8 | ].join('\n'), 9 | link: function (scope, el, attrs) { 10 | console.log('This is my pet shop!'); 11 | } 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/components/ScrollingPane/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './ScrollingPane.css'; 3 | 4 | class ScrollingPane extends Component { 5 | render() { 6 | return ( 7 |
8 |
9 | {this.props.children} 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default ScrollingPane; 17 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/inline-variable.js: -------------------------------------------------------------------------------- 1 | var template = [ 2 | '
', 3 | '', 4 | '
', 5 | ].join('\n'); 6 | 7 | angular.module('app').directive('myPetShop', function() { 8 | return { 9 | scope: {}, 10 | template: template, 11 | link: function (scope, el, attrs) { 12 | console.log('This is my pet shop!'); 13 | } 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /packages/hekla-viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hekla Viewer 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/inline.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', function() { 2 | return { 3 | scope: { 4 | title: '@', 5 | dogs: '=', 6 | cats: '=', 7 | 'quoted': '=' 8 | }, 9 | template: '
', 10 | link: function (scope, el, attrs) { 11 | console.log('This is my pet shop!'); 12 | } 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/test/cases/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HeklaWebpackPlugin = require('hekla-webpack-plugin'); 3 | 4 | const heklaConfig = require('./hekla.config'); 5 | 6 | module.exports = { 7 | entry: path.resolve(__dirname, 'src', 'index.js'), 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'bundle.js' 11 | }, 12 | plugins: [ 13 | new HeklaWebpackPlugin(heklaConfig) 14 | ] 15 | }; 16 | -------------------------------------------------------------------------------- /packages/hekla-plugin-csv-reporter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-plugin-csv-reporter", 3 | "version": "0.3.3", 4 | "description": "Hekla Plugin to report analysis through CSV files", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Andrew Jensen ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "csv-stringify-as-promised": "^2.0.7" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | overflow: hidden; 6 | } 7 | 8 | .App > .header-container { 9 | flex: 0; 10 | } 11 | 12 | .App > .content { 13 | flex: 1; 14 | display: flex; 15 | flex-direction: row; 16 | } 17 | 18 | .App > .content > .content-pane-container { 19 | flex: 6; 20 | overflow: hidden; 21 | } 22 | 23 | .App > .content > .sidebar-container { 24 | flex: 2; 25 | overflow: hidden; 26 | } 27 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-webpack-plugin", 3 | "version": "0.3.3", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "test-ci": "CI=true jest" 9 | }, 10 | "author": "Andrew Jensen ", 11 | "license": "MIT", 12 | "dependencies": { 13 | "hekla-core": "^0.3.3" 14 | }, 15 | "devDependencies": { 16 | "jest": "^24.8.0", 17 | "webpack": "^4.20.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/injected.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetShop', ['$timeout', '$document', function($timeout, $document) { 2 | return { 3 | scope: { 4 | title: '@', 5 | dogs: '=', 6 | cats: '=', 7 | 'quoted': '=' 8 | }, 9 | template: '
', 10 | link: function (scope, el, attrs) { 11 | console.log('This is my pet shop!'); 12 | } 13 | }; 14 | }]); 15 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 4 | // TODO: replace with official React version once released 5 | import injectTapEventPlugin from 'react-tap-event-plugin'; 6 | 7 | import App from './App'; 8 | import './index.css'; 9 | 10 | injectTapEventPlugin(); 11 | 12 | ReactDOM.render(( 13 | 14 | 15 | ), document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /packages/hekla-viewer/README.md: -------------------------------------------------------------------------------- 1 | # hekla-viewer 2 | 3 | ## Usage 4 | 5 | It is easiest to start the viewer through the CLI. 6 | 7 | ## Development 8 | 9 | You will need to set up a proxy server on port 3001 to serve up `hekla.json` to the dev server. A good way to do this is with the `http-server` package: 10 | 11 | ```bash 12 | npm install -g http-server 13 | cd path/to/your/project 14 | # Start the server. Don't forget to enable CORS! 15 | http-server --cors -p 3001. 16 | ``` 17 | 18 | Then start the dev server: 19 | 20 | ```bash 21 | npm install 22 | npm start 23 | ``` 24 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import './Sidebar.css'; 3 | 4 | import ComponentDetailsPane from './ComponentDetailsPane'; 5 | 6 | class Sidebar extends Component { 7 | render() { 8 | const { selectedComponent } = this.props; 9 | 10 | return ( 11 |
12 |
13 | 16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | export default Sidebar; 23 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/two-directives.js: -------------------------------------------------------------------------------- 1 | angular.module('app').directive('myPetMenu', function() { 2 | return { 3 | scope: { 4 | pets: '=' 5 | }, 6 | template: [ 7 | '
', 8 | '', 9 | '
' 10 | ].join('\n') 11 | }; 12 | }); 13 | 14 | angular.module('app').directive('myPetMenuItem', function() { 15 | return { 16 | scope: { 17 | name: '@' 18 | }, 19 | template: '
Pet: {{name}}
' 20 | }; 21 | }); 22 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/ComponentSearchBar/ComponentSearchBar.css: -------------------------------------------------------------------------------- 1 | .ComponentSearchBar { 2 | min-height: 48px; 3 | height: 48px; 4 | display: flex; 5 | flex-direction: row; 6 | align-items: center; 7 | padding: 0px 16px; 8 | } 9 | 10 | .ComponentSearchBar > .icon { 11 | padding: 12px 0; 12 | color: #BDBDBD !important; 13 | cursor: pointer; 14 | } 15 | .ComponentSearchBar > .icon:hover, .ComponentSearchBar > .icon:active { 16 | color: #616161 !important; 17 | } 18 | 19 | .ComponentSearchBar > .input { 20 | flex: 1; 21 | margin: 0px 8px; 22 | } 23 | -------------------------------------------------------------------------------- /packages/hekla-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-cli", 3 | "version": "0.3.4", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "rm -rf ./test/cases/simple/dist/ && mkdir -p ./test/cases/simple/dist/ && jest", 8 | "test-ci": "npm run test" 9 | }, 10 | "bin": { 11 | "hekla": "./bin/hekla" 12 | }, 13 | "author": "Andrew Jensen ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "commander": "^2.9.0", 17 | "hekla-core": "^0.3.3" 18 | }, 19 | "devDependencies": { 20 | "jest": "^24.9.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/test-examples/split-definition-injected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var petShopDirective = ['$timeout', '$document', function($timeout, $document) { 4 | return { 5 | scope: { 6 | title: '@', 7 | dogs: '=', 8 | cats: '=', 9 | 'quoted': '=' 10 | }, 11 | template: '
', 12 | link: function (scope, el, attrs) { 13 | console.log('This is my pet shop!'); 14 | } 15 | }; 16 | }]; 17 | 18 | angular.module('app').directive('myPetShop', petShopDirective); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla", 3 | "version": "0.0.0", 4 | "description": "A toolset to make large JS codebases more understandable", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "lerna run test-ci", 8 | "postinstall": "lerna bootstrap" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/andrewjensen/hekla.git" 13 | }, 14 | "keywords": [ 15 | "analysis" 16 | ], 17 | "author": "Andrew Jensen ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/andrewjensen/hekla/issues" 21 | }, 22 | "homepage": "https://github.com/andrewjensen/hekla#readme", 23 | "devDependencies": { 24 | "lerna": "^3.4.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/hekla-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-viewer", 3 | "version": "0.3.3", 4 | "main": "lib/server/index.js", 5 | "proxy": "http://localhost:3001", 6 | "scripts": { 7 | "start": "react-scripts start", 8 | "build": "react-scripts build", 9 | "test": "react-scripts test --env=jsdom", 10 | "eject": "react-scripts eject" 11 | }, 12 | "author": "Andrew Jensen ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "d3-request": "^1.0.2", 16 | "express": "^4.14.0", 17 | "hekla-core": "^0.3.3", 18 | "material-ui": "^0.16.1", 19 | "react": "~15.3.2", 20 | "react-dom": "~15.3.2", 21 | "react-tap-event-plugin": "^1.0.0" 22 | }, 23 | "devDependencies": { 24 | "react-scripts": "0.4.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/hekla-cli/src/run.js: -------------------------------------------------------------------------------- 1 | const pkg = require('../package.json'); 2 | 3 | const analyze = require('./commands/analyze'); 4 | 5 | /** 6 | * Run the CLI, with process.argv as inputs. 7 | * 8 | * Example usage: 9 | * 10 | * run([ 11 | * '/path/to/node', 12 | * '/path/to/hekla', 13 | * 'analyze', 14 | * '--config', 15 | * '/path/to/hekla.config.js' 16 | * ]) 17 | */ 18 | module.exports = function run(argv) { 19 | const program = require('commander'); 20 | 21 | program 22 | .version(pkg.version, '-v, --version'); 23 | 24 | program 25 | .command('analyze') 26 | .description('Analyze a project') 27 | .option('-s, --single ', 'Analyze a single file') 28 | .option('-c, --config ', 'Config file location') 29 | .action(analyze); 30 | 31 | program.parse(argv); 32 | } 33 | -------------------------------------------------------------------------------- /packages/hekla-viewer/lib/server/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const express = require('express'); 5 | 6 | const buildPath = path.resolve(__dirname, '../../build'); 7 | 8 | module.exports = class ViewerServer { 9 | constructor(heklaData) { 10 | const app = express(); 11 | 12 | app.get('/', (req, res) => res.sendFile(path.resolve(buildPath, 'index.html'))); 13 | 14 | app.use('/static', express.static(path.resolve(buildPath, 'static'))); 15 | 16 | app.get('/hekla.json', (req, res) => res.json(heklaData)); 17 | 18 | this._server = app; 19 | } 20 | 21 | listen(port) { 22 | return new Promise((resolve, reject) => { 23 | this._server.listen(port, (err) => { 24 | if (err) return reject(err); 25 | 26 | return resolve(); 27 | }); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/ComponentSearchResults/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './ComponentSearchResults.css'; 4 | 5 | const renderResultItem = (component, onSelect) => ( 6 |
onSelect(component)} 10 | > 11 |
{component.name}
12 |
{component.path}
13 |
{component.type}
14 |
15 | ); 16 | 17 | const ComponentSearchResults = (props) => ( 18 |
19 |
20 | {props.results.map(component => renderResultItem(component, props.onSelect))} 21 |
22 |
23 | ); 24 | 25 | export default ComponentSearchResults; 26 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/OwnershipPlugin/OwnershipPlugin.js: -------------------------------------------------------------------------------- 1 | const ProjectTreeAnnotations = require('./ProjectTreeAnnotations'); 2 | 3 | module.exports = class OwnershipPlugin { 4 | constructor(owners) { 5 | if (!owners) { 6 | throw new TypeError(); 7 | } 8 | this.annotations = new ProjectTreeAnnotations(); 9 | for (let ownerRule of owners) { 10 | const { path, owner } = ownerRule; 11 | this.annotations.add(path, { owner }); 12 | } 13 | } 14 | 15 | apply(analyzer) { 16 | analyzer.hooks.moduleRawSource.tap('OwnershipPlugin', this.moduleRawSource.bind(this)); 17 | } 18 | 19 | moduleRawSource(module, source) { 20 | const match = this.annotations.match(module.getName()); 21 | if (match) { 22 | module.set('owner', match.owner); 23 | } else { 24 | module.set('owner', null); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/hekla-plugin-csv-reporter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-plugin-csv-reporter", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "csv-stringify": { 8 | "version": "5.3.3", 9 | "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.3.3.tgz", 10 | "integrity": "sha512-q8Qj+/lN74LRmG7Mg0LauE5WcnJOD5MEGe1gI57IYJCB61KWuEbAFHm1uIPDkI26aqElyBB57SlE2GGwq2EY5A==" 11 | }, 12 | "csv-stringify-as-promised": { 13 | "version": "2.0.7", 14 | "resolved": "https://registry.npmjs.org/csv-stringify-as-promised/-/csv-stringify-as-promised-2.0.7.tgz", 15 | "integrity": "sha512-P0K1ieNYe8jV+kPIgiaWhaUDTAm7o7d4h/AVPqES6E2H8fWHL1cXJ2TVBARWGyQokpJWxM98s2deMfSOCJ0E+w==", 16 | "requires": { 17 | "csv-stringify": "^5.3.3" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/ASTWrapper.js: -------------------------------------------------------------------------------- 1 | const walk = require('tree-walk'); 2 | const { VISITOR_KEYS } = require('@babel/types'); 3 | 4 | // TODO: define a custom walker based on VISITOR_KEYS 5 | // (See DOMWalker) 6 | 7 | module.exports = class ASTWrapper { 8 | constructor(ast) { 9 | this.ast = ast; 10 | } 11 | 12 | unwrap() { 13 | return this.ast; 14 | } 15 | 16 | visit(visitors) { 17 | for (let key in visitors) { 18 | if (!VISITOR_KEYS.hasOwnProperty(key)) { 19 | throw new TypeError(`Invalid visitor type: ${key}`); 20 | } 21 | } 22 | 23 | walk.preorder(this.ast, (node) => callVisitor(node, visitors, this.ast)); 24 | } 25 | } 26 | 27 | function callVisitor(node, visitors, dom) { 28 | if (!node) { 29 | return; 30 | } 31 | if (node.type && visitors[node.type]) { 32 | visitors[node.type](node); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | ########################### 40 | 41 | packages/hekla-cli/test/cases/*/dist/ 42 | packages/hekla-viewer/build/ 43 | packages/hekla-webpack-plugin/test/cases/*/dist/ 44 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import ComponentSearcher from './ComponentSearcher'; 4 | import DependencyChart from './DependencyChart'; 5 | 6 | import './ContentPane.css'; 7 | 8 | class ContentPane extends Component { 9 | render() { 10 | const { graph, selectedComponent, onSelect } = this.props; 11 | return ( 12 |
13 | 17 | {!graph ? null : ( 18 | 24 | )} 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default ContentPane; 31 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/ComponentSearchResults/ComponentSearchResults.css: -------------------------------------------------------------------------------- 1 | .ComponentSearchResults { 2 | flex: 1; 3 | display: flex; 4 | flex-direction: column; 5 | border-top: 1px solid #BDBDBD; 6 | } 7 | 8 | .ComponentSearchResults > .content { 9 | overflow: auto; 10 | } 11 | 12 | .ComponentSearchResultItem { 13 | border-top: 1px solid #BDBDBD; 14 | padding: 16px 16px; 15 | background-color: #F5F5F5; 16 | cursor: pointer; 17 | } 18 | .ComponentSearchResultItem:first-child { 19 | border-top: none; 20 | } 21 | .ComponentSearchResultItem:hover, .ComponentSearchResultItem.active { 22 | background-color: white; 23 | } 24 | 25 | .ComponentSearchResultItem > .name { 26 | font-size: 16px; 27 | padding-bottom: 4px; 28 | } 29 | 30 | .ComponentSearchResultItem > .path, .ComponentSearchResultItem > .type { 31 | font-size: 13px; 32 | } 33 | 34 | .ComponentSearchResultItem > .path { 35 | padding-bottom: 4px; 36 | } 37 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/ComponentBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | COMPONENT_BOX_WIDTH, 4 | COMPONENT_BOX_HEIGHT, 5 | COMPONENT_BOX_TEXT_OFFSET 6 | } from './constants'; 7 | 8 | const ComponentBox = (props) => { 9 | const { 10 | x, 11 | y, 12 | component, 13 | selected, 14 | onSelect, 15 | onContextMenu 16 | } = props; 17 | const { name } = component; 18 | const className = (selected ? 'ComponentBox selected' : 'ComponentBox'); 19 | return ( 20 | onSelect(component)} onContextMenu={(event) => onContextMenu(event, component)}> 21 | 26 | {name} 30 | 31 | ); 32 | }; 33 | 34 | export default ComponentBox; 35 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/DependencyChart.css: -------------------------------------------------------------------------------- 1 | .DependencyChart { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .DependencyChart > svg { 7 | width: 100%; 8 | height: 100%; 9 | background-color: #f0f0f0; 10 | } 11 | 12 | .ComponentBox { 13 | cursor: pointer; 14 | } 15 | .ComponentBox:hover { 16 | opacity: 0.7; 17 | } 18 | 19 | .ComponentBox > rect { 20 | fill: white; 21 | stroke: #303F9F; /* indigo700 */ 22 | stroke-width: 2px; 23 | } 24 | .ComponentBox.selected > rect { 25 | stroke: black; 26 | } 27 | 28 | .ComponentBox > text { 29 | fill: #424242; /* grey800 */ 30 | text-anchor: middle; 31 | font-size: 14px; 32 | user-select: none; 33 | } 34 | .ComponentBox.selected > text { 35 | fill: black; 36 | } 37 | 38 | .ComponentDependencyArrow { 39 | fill: transparent; 40 | stroke: #424242; /* grey800 */ 41 | stroke-width: 1px; 42 | marker-end: url(#triangle); 43 | } 44 | 45 | .ComponentContextMenu { 46 | position: absolute; 47 | } 48 | -------------------------------------------------------------------------------- /packages/hekla-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-core", 3 | "version": "0.3.3", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha src/**/*.spec.js --require ./test/test-helpers.js", 8 | "test-ci": "npm run test", 9 | "tdd": "mocha src/**/*.spec.js --require ./test/test-helpers.js --reporter min --watch" 10 | }, 11 | "author": "Andrew Jensen ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@babel/parser": "^7.0.0", 15 | "@babel/types": "^7.1.2", 16 | "async": "^2.1.2", 17 | "camel-case": "^3.0.0", 18 | "chalk": "^2.4.2", 19 | "dashify": "^0.2.2", 20 | "domhandler": "^2.4.2", 21 | "glob": "^7.1.4", 22 | "htmlparser2": "^3.9.1", 23 | "minimatch": "^3.0.4", 24 | "sticky-terminal-display": "^0.1.0", 25 | "tapable": "^1.1.0", 26 | "tree-walk": "^0.4.0" 27 | }, 28 | "devDependencies": { 29 | "chai": "^4.1.2", 30 | "mocha": "^5.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/hekla-cli/README.md: -------------------------------------------------------------------------------- 1 | # hekla-cli 2 | 3 | CLI for running static analysis with Hekla 4 | 5 | [![Build Status](https://travis-ci.org/andrewjensen/hekla.svg?branch=master)](https://travis-ci.org/andrewjensen/hekla) 6 | 7 | ## Usage 8 | 9 | ### Step 1: Configure your project to use Hekla 10 | 11 | You usually want to integrate Hekla with your Webpack configuration, so follow the steps in the [hekla-webpack-plugin](https://www.npmjs.com/package/hekla-webpack-plugin) project. 12 | 13 | ### Step 2: Install the CLI tool globally 14 | 15 | ```bash 16 | npm install -g hekla-cli 17 | ``` 18 | 19 | ### Step 3: Run the analyzer 20 | 21 | ```bash 22 | # Start in your project directory 23 | cd /path/to/your/project/ 24 | 25 | # See usage help 26 | hekla --help 27 | 28 | # Analyze the whole project directory recursively 29 | hekla analyze 30 | 31 | # Analyze a single file 32 | hekla analyze --single src/your-feature/components/YourComponent.js 33 | 34 | # Specify a custom config file location 35 | hekla analyze --config path/to/your/hekla.config.js 36 | ``` 37 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/hekla.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const writeFile = promisify(fs.writeFile); 6 | 7 | const REPORTED_FILE_LOCATION = path.resolve(__dirname, 'dist', 'report.txt'); 8 | 9 | class TestModulePlugin { 10 | apply(analyzer) { 11 | analyzer.hooks.moduleRawSource 12 | .tap('TestModulePlugin', (module, source) => 13 | module.set('visited', true) 14 | ); 15 | } 16 | } 17 | 18 | class TestReporterPlugin { 19 | apply(analyzer) { 20 | analyzer.hooks.reporter 21 | .tap('TestReporterPlugin', async (analyzer, analysis) => { 22 | const moduleCount = analysis.modules.length; 23 | const contents = `I found ${moduleCount} modules!`; 24 | await writeFile(REPORTED_FILE_LOCATION, contents); 25 | }); 26 | } 27 | } 28 | 29 | module.exports = { 30 | rootPath: path.resolve(__dirname, 'src'), 31 | outputPath: path.resolve(__dirname, 'dist'), 32 | plugins: [ 33 | new TestModulePlugin(), 34 | new TestReporterPlugin() 35 | ] 36 | }; 37 | -------------------------------------------------------------------------------- /packages/hekla-core/README.md: -------------------------------------------------------------------------------- 1 | # hekla-core 2 | 3 | Core logic for running static analysis with Hekla 4 | 5 | [![Build Status](https://travis-ci.org/andrewjensen/hekla.svg?branch=master)](https://travis-ci.org/andrewjensen/hekla) 6 | 7 | This package is required by the Hekla [Webpack plugin](https://www.npmjs.com/package/hekla-webpack-plugin) and [CLI tool](https://www.npmjs.com/package/hekla-cli). It includes built-in plugins, as well as other core logic that you can use in custom plugins. The Analyzer can also be imported and called directly from other code. 8 | 9 | ## Configuring `hekla.config.js` 10 | 11 | TODO: add this section! 12 | 13 | ## Built-in plugins 14 | 15 | See the `src/plugins` directory. 16 | 17 | ## Logic for other plugins 18 | 19 | See the `src/utils/ast-utils` directory. 20 | 21 | ## Using hekla-core from other code 22 | 23 | ```js 24 | const Analyzer = require('hekla-core').Analyzer; 25 | const config = require('./path/to/hekla-config.js'); 26 | 27 | const analyzer = new Analyzer(); 28 | analyzer.applyConfig(config); 29 | 30 | // ... 31 | ``` 32 | 33 | See the Analyzer unit tests for more example usage. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrew Jensen 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 | -------------------------------------------------------------------------------- /packages/hekla-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrew Jensen 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 | -------------------------------------------------------------------------------- /packages/hekla-viewer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrew Jensen 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 | -------------------------------------------------------------------------------- /packages/hekla-core/src/Module.js: -------------------------------------------------------------------------------- 1 | const { 2 | getModuleName, 3 | getModuleShortName 4 | } = require('./utils/fs-utils'); 5 | 6 | const RESERVED_PROPERTIES = [ 7 | 'name', 8 | 'shortName' 9 | ]; 10 | 11 | module.exports = class Module { 12 | constructor(resource, rootPath) { 13 | this._resource = resource; 14 | this._rootPath = rootPath; 15 | this._name = getModuleName(resource, rootPath); 16 | this._shortName = getModuleShortName(this._name); 17 | this._meta = {}; 18 | this._error = null; 19 | } 20 | 21 | getName() { 22 | return this._name; 23 | } 24 | 25 | getResource() { 26 | return this._resource; 27 | } 28 | 29 | set(propertyName, propertyValue) { 30 | if(RESERVED_PROPERTIES.includes(propertyName)) { 31 | throw new Error(`The '${propertyName}' property is reserved`); 32 | } 33 | this._meta[propertyName] = propertyValue; 34 | } 35 | 36 | setError(error) { 37 | this._error = error; 38 | } 39 | 40 | serialize() { 41 | const result = { 42 | name: this._name, 43 | shortName: this._shortName, 44 | ...this._meta 45 | }; 46 | if (this._error) { 47 | result.error = this._error; 48 | } 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/Sidebar/ComponentDetailsPane/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import PaneTitle from '../../components/PaneTitle'; 4 | import ScrollingPane from '../../components/ScrollingPane'; 5 | 6 | import './ComponentDetailsPane.css'; 7 | 8 | class ComponentDetailsPane extends Component { 9 | render() { 10 | const { component } = this.props; 11 | 12 | if (component) { 13 | return ( 14 |
15 |
16 | 17 |
18 |
19 | 20 |
{JSON.stringify(component, null, 2)}
21 |
22 |
23 |
24 | ); 25 | } else { 26 | return ( 27 |
28 |
29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | } 37 | } 38 | 39 | export default ComponentDetailsPane; 40 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/test/cases/simple/testcase.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const ANALYSIS_PATH = path.resolve(__dirname, 'dist', 'analysis.json'); 5 | 6 | async function compile(config) { 7 | return new Promise((resolve, reject) => { 8 | webpack(config, (err, stats) => { 9 | if (err) { 10 | return reject(err); 11 | } else if (stats.hasErrors()) { 12 | // TODO: include the actual errors 13 | return reject(new Error('Compilation has errors')); 14 | } else { 15 | return resolve(stats); 16 | } 17 | }); 18 | }); 19 | } 20 | 21 | function getAnalysis() { 22 | return require(ANALYSIS_PATH); 23 | } 24 | 25 | describe('simple', () => { 26 | 27 | it('should compile a simple project', async () => { 28 | const config = require('./webpack.config'); 29 | await compile(config); 30 | 31 | const analysis = getAnalysis(); 32 | expect(analysis.modules).toBeDefined(); 33 | expect(analysis.modules).toHaveLength(1); 34 | 35 | const indexModule = analysis.modules[0]; 36 | expect(indexModule.name).toEqual('./src/index.js'); 37 | expect(indexModule.shortName).toEqual('src/index.js'); 38 | expect(indexModule.lines).toEqual(2); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /packages/hekla-core/src/ConfigValidator.js: -------------------------------------------------------------------------------- 1 | module.exports = class ConfigValidator { 2 | constructor() { 3 | this.errors = []; 4 | } 5 | 6 | validate(config) { 7 | if (!config.hasOwnProperty('rootPath')) { 8 | this.errors.push('rootPath is not configured'); 9 | } 10 | 11 | if (config.hasOwnProperty('outputPath')) { 12 | if (typeof config.outputPath !== 'string') { 13 | this.errors.push('Output path is not a string'); 14 | } else if (config.outputPath.match(/\.[a-zA-Z0-9]+$/)) { 15 | this.errors.push('Output path must be a directory'); 16 | } 17 | } 18 | 19 | if (config.hasOwnProperty('exclude')) { 20 | for (let excludePattern of config.exclude) { 21 | if (typeof excludePattern !== 'string') { 22 | this.errors.push('Exclude pattern is not a string'); 23 | } 24 | } 25 | } 26 | 27 | if (config.hasOwnProperty('plugins')) { 28 | for (let plugin of config.plugins) { 29 | if (!(plugin.apply && typeof plugin.apply === 'function')) { 30 | this.errors.push('Plugin does not have an `apply` method'); 31 | } 32 | } 33 | } 34 | 35 | return this.isValid(); 36 | } 37 | 38 | isValid() { 39 | return (this.errors.length === 0); 40 | } 41 | 42 | getErrors() { 43 | return this.errors; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/hekla-core/src/Module.spec.js: -------------------------------------------------------------------------------- 1 | const Module = require('./Module'); 2 | 3 | describe('Module', () => { 4 | 5 | describe('set', () => { 6 | 7 | it('should not allow `name` to be changed', () => { 8 | const module = new Module('/path/to/project/src/filename.js', '/path/to/project'); 9 | expect(() => module.set('name', 'something new')).to.throw(); 10 | }); 11 | 12 | it('should not allow `shortName` to be changed', () => { 13 | const module = new Module('/path/to/project/src/filename.js', '/path/to/project'); 14 | expect(() => module.set('shortName', 'something new')).to.throw(); 15 | }); 16 | 17 | }); 18 | 19 | describe('serialize', () => { 20 | 21 | it('should include custom metadata', () => { 22 | const module = new Module('/path/to/project/src/filename.js', '/path/to/project'); 23 | module.set('special', true); 24 | const serialized = module.serialize(); 25 | expect(serialized.special).to.equal(true); 26 | }); 27 | 28 | it('should put the name and shortName before everything else', () => { 29 | const module = new Module('/path/to/project/src/filename.js', '/path/to/project'); 30 | module.set('special', true); 31 | const serialized = module.serialize(); 32 | const json = JSON.stringify(serialized); 33 | expect(json).to.equal(`{"name":"./src/filename.js","shortName":"filename.js","special":true}`); 34 | }); 35 | 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This project is deprecated. I have not kept up with maintenance of Hekla so I believe it is best to archive it. 4 | 5 | Using static code analysis for reporting is still immensely useful. Here are some related projects that might be helpful in doing this analysis: 6 | 7 | - [jscodeshift](https://github.com/facebook/jscodeshift), a codemod toolkit that can also be used for gathering data 8 | - [tree-sitter](https://tree-sitter.github.io/tree-sitter/), a cross-language parser generator tool 9 | - [AST Explorer](https://astexplorer.net/), for quickly seeing ASTs and prototyping codemods+linter rules 10 | - [react-scanner](https://github.com/moroshko/react-scanner), static code analysis specifically for React components 11 | 12 | # hekla 13 | 14 | A pluggable static code analysis toolset for understanding large Javascript projects 15 | 16 | [![Build Status](https://travis-ci.org/andrewjensen/hekla.svg?branch=master)](https://travis-ci.org/andrewjensen/hekla) 17 | 18 | ![Hekla Intro Diagram](https://raw.githubusercontent.com/andrewjensen/hekla/master/assets/intro.png) 19 | 20 | ## Getting Started 21 | 22 | The easiest way to start using Hekla is through the [Webpack plugin](packages/hekla-webpack-plugin) and the [CLI tool](packages/hekla-cli). You can also install and use the [core analysis](packages/hekla-core) package directly. 23 | 24 | ## "Hekla?" 25 | 26 | Hekla is named after a [volcano in Iceland](https://en.wikipedia.org/wiki/Hekla)! 27 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/DOMWrapper.js: -------------------------------------------------------------------------------- 1 | const walk = require('tree-walk'); 2 | 3 | const NODE_TYPES = [ 4 | 'tag', 5 | 'text', 6 | 'comment', 7 | 'directive' 8 | ]; 9 | 10 | const domWalker = walk(el => { 11 | // https://www.npmjs.com/package/tree-walk#custom-walkers 12 | if (Array.isArray(el)) { 13 | return el; 14 | } else if (el.type) { 15 | if (el.children) { 16 | return el.children; 17 | } else { 18 | return []; 19 | } 20 | } else { 21 | throw new TypeError('Unrecognized tree node'); 22 | } 23 | }); 24 | 25 | module.exports = class DOMWrapper { 26 | constructor(dom) { 27 | this.dom = dom; 28 | } 29 | 30 | unwrap() { 31 | return this.dom; 32 | } 33 | 34 | visit(visitors) { 35 | for (let key in visitors) { 36 | if (!NODE_TYPES.includes(key)) { 37 | throw new TypeError(`Invalid visitor type: ${key}`); 38 | } 39 | } 40 | 41 | domWalker.preorder(this.dom, (node) => callVisitor(node, visitors, this.dom)); 42 | } 43 | } 44 | 45 | function callVisitor(node, visitors, dom) { 46 | if (node === dom) { 47 | // Skip the root-level array of nodes, wait to visit each actual node 48 | return; 49 | } 50 | 51 | if (!node || !node.type) { 52 | throw new TypeError('Unrecognized tree node'); 53 | } 54 | 55 | if (node.type === 'tag' && visitors['tag']) { 56 | visitors['tag'](node); 57 | } else if (node.type === 'text' && visitors['text']) { 58 | visitors['text'](node); 59 | } 60 | } -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/ComponentSearchBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import ActionSearch from 'material-ui/svg-icons/action/search'; 4 | import NavigationClose from 'material-ui/svg-icons/navigation/close'; 5 | 6 | import './ComponentSearchBar.css'; 7 | 8 | export default class ComponentSearchBar extends Component { 9 | constructor(props) { 10 | super(props); 11 | this._onSearchTextChange = this._onSearchTextChange.bind(this); 12 | this._onClickClose = this._onClickClose.bind(this); 13 | } 14 | 15 | _onSearchTextChange(event) { 16 | const updatedText = event.target.value; 17 | this.props.onSearchTextChange(updatedText); 18 | } 19 | 20 | _onClickClose() { 21 | this.props.onSearchTextChange(''); 22 | } 23 | 24 | render() { 25 | const { searchText } = this.props; 26 | return ( 27 |
28 | 31 |
32 | 39 |
40 | 44 |
45 | ); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /packages/hekla-core/src/StatusMessage.js: -------------------------------------------------------------------------------- 1 | const TYPES = { 2 | 'STATUS_ANALYSIS_STARTED': 'STATUS_ANALYSIS_STARTED', 3 | 'STATUS_ANALYSIS_SUCCESSFUL': 'STATUS_ANALYSIS_SUCCESSFUL', 4 | 'STATUS_ANALYSIS_FAILED': 'STATUS_ANALYSIS_FAILED', 5 | 'STATUS_MODULE_QUEUED': 'STATUS_MODULE_QUEUED', 6 | 'STATUS_MODULE_SUCCESSFUL': 'STATUS_MODULE_SUCCESSFUL', 7 | 'STATUS_MODULE_FAILED': 'STATUS_MODULE_FAILED', 8 | }; 9 | 10 | const analysisStarted = (workerCount) => ({ 11 | type: TYPES.STATUS_ANALYSIS_STARTED, 12 | payload: { 13 | workerCount 14 | } 15 | }); 16 | 17 | const analysisSuccessful = () => ({ 18 | type: TYPES.STATUS_ANALYSIS_SUCCESSFUL, 19 | payload: {} 20 | }); 21 | 22 | const analysisFailed = (error) => ({ 23 | type: TYPES.STATUS_ANALYSIS_FAILED, 24 | payload: { 25 | error 26 | } 27 | }); 28 | 29 | const moduleQueued = (moduleName, workerId) => ({ 30 | type: TYPES.STATUS_MODULE_QUEUED, 31 | payload: { 32 | moduleName, 33 | workerId 34 | } 35 | }); 36 | 37 | const moduleSuccessful = (moduleName, workerId) => ({ 38 | type: TYPES.STATUS_MODULE_SUCCESSFUL, 39 | payload: { 40 | moduleName, 41 | workerId 42 | } 43 | }); 44 | 45 | const moduleFailed = (moduleName, workerId, error) => ({ 46 | type: TYPES.STATUS_MODULE_FAILED, 47 | payload: { 48 | moduleName, 49 | workerId, 50 | error 51 | } 52 | }); 53 | 54 | module.exports = { 55 | TYPES, 56 | analysisStarted, 57 | analysisSuccessful, 58 | analysisFailed, 59 | moduleQueued, 60 | moduleSuccessful, 61 | moduleFailed, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/hekla-core/src/WorkQueue.spec.js: -------------------------------------------------------------------------------- 1 | const WorkQueue = require('./WorkQueue'); 2 | const Module = require('./Module'); 3 | 4 | class MockAnalyzer { 5 | async processModule(module) { 6 | await sleep(10); 7 | } 8 | } 9 | 10 | async function sleep(ms) { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | } 13 | 14 | describe('WorkQueue', () => { 15 | 16 | it('sends status updates', async () => { 17 | const messages = []; 18 | 19 | const analyzer = new MockAnalyzer(); 20 | const queue = new WorkQueue(analyzer); 21 | queue.onStatusUpdate(statusMessage => { 22 | messages.push(statusMessage); 23 | }); 24 | queue.start(2); 25 | 26 | const rootPath = '/path/to/project'; 27 | queue.enqueue(new Module('/path/to/project/src/truck.js', rootPath)); 28 | queue.enqueue(new Module('/path/to/project/src/bus.js', rootPath)); 29 | 30 | await queue.waitForFinish(); 31 | 32 | expect(messages).to.deep.equal([ 33 | { 34 | type: 'STATUS_MODULE_QUEUED', 35 | payload: { 36 | moduleName: './src/truck.js', 37 | workerId: 0 38 | } 39 | }, 40 | { 41 | type: 'STATUS_MODULE_QUEUED', 42 | payload: { 43 | moduleName: './src/bus.js', 44 | workerId: 1 45 | } 46 | }, 47 | { 48 | type: 'STATUS_MODULE_SUCCESSFUL', 49 | payload: { 50 | moduleName: './src/truck.js', 51 | workerId: 0 52 | } 53 | }, 54 | { 55 | type: 'STATUS_MODULE_SUCCESSFUL', 56 | payload: { 57 | moduleName: './src/bus.js', 58 | workerId: 1 59 | } 60 | } 61 | ]); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/ComponentDependencyArrow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | COMPONENT_BOX_WIDTH, 4 | COMPONENT_BOX_HEIGHT 5 | } from './constants'; 6 | 7 | const ComponentDependencyArrow = (props) => { 8 | const { fromNode, toNode } = props; 9 | 10 | const fromX = (fromNode.x + (COMPONENT_BOX_WIDTH * 0.5)); 11 | const fromY = (fromNode.y <= toNode.y ? 12 | (fromNode.y + COMPONENT_BOX_HEIGHT) : // Bottom of node 13 | (fromNode.y) // Top of node 14 | ); 15 | 16 | const toX = (toNode.x + (COMPONENT_BOX_WIDTH * 0.5)); 17 | const toY = (fromNode.y <= toNode.y ? 18 | (toNode.y) : // Top of node 19 | (toNode.y + COMPONENT_BOX_HEIGHT) // Bottom of node 20 | ); 21 | 22 | const d = (fromX === toX ? 23 | directPath(fromX, fromY, toX, toY) : 24 | curvedLinesPath(fromX, fromY, toX, toY) 25 | ); 26 | 27 | return (); 28 | }; 29 | 30 | export default ComponentDependencyArrow; 31 | 32 | function directPath(fromX, fromY, toX, toY) { 33 | return `M ${fromX} ${fromY} L ${toX} ${toY}`; 34 | } 35 | 36 | // function straightLinesPath(fromX, fromY, toX, toY) { 37 | // const middleY = (fromY + toY) * 0.5; 38 | // return `\ 39 | // M ${fromX} ${fromY} \ 40 | // L ${fromX} ${middleY} \ 41 | // L ${toX} ${middleY} \ 42 | // L ${toX} ${toY}`; 43 | // } 44 | 45 | function curvedLinesPath(fromX, fromY, toX, toY) { 46 | const middleX = (fromX + toX) * 0.5; 47 | const middleY = (fromY + toY) * 0.5; 48 | const ctrlY = fromY + ((toY - fromY) * 0.4); 49 | return `\ 50 | M ${fromX} ${fromY} \ 51 | Q ${fromX} ${ctrlY} ${middleX} ${middleY} \ 52 | T ${toX} ${toY}`; 53 | } 54 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { json } from 'd3-request'; 3 | import './App.css'; 4 | 5 | import Header from './Header'; 6 | import ContentPane from './ContentPane'; 7 | import Sidebar from './Sidebar'; 8 | 9 | const JSON_URL = '/hekla.json'; 10 | 11 | class App extends Component { 12 | constructor(props, context) { 13 | super(props, context); 14 | 15 | this.state = { 16 | loaded: false, 17 | graph: null, 18 | selectedNode: null 19 | }; 20 | 21 | this._onSelectNode = this._onSelectNode.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | json(JSON_URL, (err, data) => { 26 | this.setState({ 27 | loaded: true, 28 | graph: data 29 | }); 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 | 49 |
50 | 51 |
52 | 57 |
58 | 59 |
60 | 61 |
62 | ); 63 | } 64 | 65 | _onSelectNode(node) { 66 | this.setState({ 67 | selectedNode: node 68 | }); 69 | } 70 | 71 | } 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /packages/hekla-plugin-csv-reporter/src/CSVReporterPlugin.js: -------------------------------------------------------------------------------- 1 | const stringify = require('csv-stringify-as-promised'); 2 | 3 | const fs = require('fs'); 4 | const { promisify } = require('util'); 5 | 6 | const writeFile = promisify(fs.writeFile); 7 | 8 | module.exports = class CSVReporterPlugin { 9 | constructor(config) { 10 | this.setConfig(config) 11 | } 12 | 13 | apply(analyzer) { 14 | analyzer.hooks.reporter.tap('CSVReporterPlugin', this.reporter.bind(this)); 15 | } 16 | 17 | setConfig(config) { 18 | if (!config) { 19 | throw new TypeError('Plugin not configured!'); 20 | } 21 | 22 | if (!config.destination) { 23 | throw new TypeError('Plugin destination is not configured!'); 24 | } 25 | 26 | if (!config.headers) { 27 | throw new TypeError('Plugin headers are not configured!'); 28 | } 29 | 30 | if (!config.moduleToRows) { 31 | throw new TypeError('Plugin `moduleToRows` mapping is not configured!'); 32 | } else if (typeof config.moduleToRows !== 'function') { 33 | throw new TypeError('Plugin `moduleToRows` mapping must be a function'); 34 | } 35 | 36 | const { destination, headers, moduleToRows } = config; 37 | 38 | this.destination = destination; 39 | this.headers = headers; 40 | this.moduleToRows = moduleToRows; 41 | } 42 | 43 | async reporter(analyzer, analysis) { 44 | const outputRows = []; 45 | 46 | outputRows.push(this.headers); 47 | 48 | for (let module of analysis.modules) { 49 | for (let moduleRow of this.moduleToRows(module)) { 50 | outputRows.push(moduleRow); 51 | } 52 | } 53 | 54 | const outputCsv = await stringify(outputRows); 55 | 56 | console.log(`CSVReporterPlugin: writing to output file: ${this.destination}`); 57 | await writeFile(this.destination, outputCsv); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/README.md: -------------------------------------------------------------------------------- 1 | # hekla-webpack-plugin 2 | 3 | Webpack integration for running static analysis with Hekla 4 | 5 | [![Build Status](https://travis-ci.org/andrewjensen/hekla.svg?branch=master)](https://travis-ci.org/andrewjensen/hekla) 6 | 7 | Supports Webpack v3 and above. 8 | 9 | ## Quick setup 10 | 11 | ### Step 1: Add the dependencies to your `package.json` file: 12 | 13 | ```bash 14 | npm install --save-dev hekla-core hekla-webpack-plugin 15 | ``` 16 | 17 | ### Step 2: Create a `hekla.config.js` file in your project root that looks generally like this: 18 | 19 | ```js 20 | const path = require('path'); 21 | const { 22 | LinesOfCodePlugin, 23 | // Import other built-in plugins here 24 | } = require('hekla-core').plugins; 25 | 26 | module.exports = { 27 | rootPath: path.resolve(__dirname, 'src'), 28 | outputPath: path.resolve(__dirname), 29 | exclude: [ 30 | 'vendor/**', 31 | 'other-directory-to-skip/**' 32 | ], 33 | plugins: [ 34 | new LinesOfCodePlugin() 35 | // Add your plugins here! 36 | ] 37 | }; 38 | 39 | ``` 40 | 41 | ### Step 3: Add the plugin to your webpack configuration: 42 | 43 | ```js 44 | // ... 45 | 46 | const HeklaWebpackPlugin = require('hekla-webpack-plugin'); 47 | const heklaConfig = require('./hekla.config.js'); 48 | 49 | module.exports = { 50 | // ... 51 | plugins: [ 52 | // ... 53 | new HeklaWebpackPlugin(heklaConfig) 54 | ] 55 | } 56 | ``` 57 | 58 | Now Hekla will produce analysis every time you build your project! 59 | 60 | ## Setup tips 61 | 62 | Consider applying the `HeklaWebpackPlugin` conditionally, so the analysis only runs when you need it. You might want to run it only in production mode, or only when running along with CI. 63 | 64 | Read more about configuring Hekla in the [hekla-core](https://www.npmjs.com/package/hekla-core) project. 65 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/fs-utils/index.spec.js: -------------------------------------------------------------------------------- 1 | const fsUtils = require('./index'); 2 | 3 | describe('fsUtils', () => { 4 | 5 | describe('getModuleName', () => { 6 | 7 | it('should remove the rootPath', () => { 8 | const filePath = '/path/to/project/src/main.js'; 9 | const rootPath = '/path/to/project'; 10 | expect(fsUtils.getModuleName(filePath, rootPath)).to.equal('./src/main.js'); 11 | }); 12 | 13 | it('should remove webpack loaders from the front', () => { 14 | const resourceRequest = 'module!/path/to/project/src/styles.css'; 15 | const rootPath = '/path/to/project'; 16 | expect(fsUtils.getModuleName(resourceRequest, rootPath)).to.equal('./src/styles.css'); 17 | }); 18 | 19 | it('should throw if rootPath is not specified', () => { 20 | const filePath = '/path/to/project/src/main.js'; 21 | expect(() => fsUtils.getModuleName(filePath)).to.throw('rootPath not specified'); 22 | }); 23 | 24 | }); 25 | 26 | describe('getModuleShortName', () => { 27 | 28 | it('should shorten a normal module name', () => { 29 | const moduleName = './src/main.js'; 30 | expect(fsUtils.getModuleShortName(moduleName)).to.equal('main.js'); 31 | }); 32 | 33 | it('should shorten a module name with extra file extensions', () => { 34 | const moduleName = './src/special.thing.js'; 35 | expect(fsUtils.getModuleShortName(moduleName)).to.equal('special.thing.js'); 36 | }); 37 | 38 | it('should shorten a module named index.js', () => { 39 | const moduleName = './src/my-feature/index.js'; 40 | expect(fsUtils.getModuleShortName(moduleName)).to.equal('my-feature/index.js'); 41 | }); 42 | 43 | it('should shorten a module named app.js', () => { 44 | const moduleName = './src/my-feature/app.js'; 45 | expect(fsUtils.getModuleShortName(moduleName)).to.equal('my-feature/app.js'); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /packages/hekla-cli/src/commands/analyze.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const { 5 | Analyzer, 6 | ConfigValidator 7 | } = require('hekla-core'); 8 | const { 9 | writeJSON 10 | } = require('hekla-core').fsUtils; 11 | 12 | module.exports = async function analyze(cmd) { 13 | const singleFileLocation = cmd.single; 14 | const customConfigLocation = cmd.config; 15 | 16 | let configPath = customConfigLocation 17 | ? path.resolve(process.cwd(), customConfigLocation) 18 | : path.resolve(process.cwd(), 'hekla.config.js'); 19 | 20 | let config; 21 | 22 | try { 23 | config = require(configPath); 24 | } catch (err) { 25 | console.error('Unable to evaluate config file:', err.stack); 26 | process.exit(1); 27 | } 28 | 29 | const validator = new ConfigValidator(); 30 | validator.validate(config); 31 | 32 | if (!validator.isValid()) { 33 | console.log('Invalid Hekla configuration:'); 34 | for (let error of validator.getErrors()) { 35 | console.log(` ${error}`); 36 | } 37 | console.log(); 38 | process.exit(1); 39 | } 40 | 41 | const analyzer = new Analyzer(); 42 | analyzer.setInputFileSystem(fs); 43 | analyzer.applyConfig(config); 44 | 45 | console.log('Analyzing...'); 46 | if (singleFileLocation) { 47 | const filePath = path.resolve(process.cwd(), singleFileLocation); 48 | const module = analyzer.createModule(filePath); 49 | await analyzer.processModule(module) 50 | } else { 51 | await analyzer.run(); 52 | } 53 | 54 | console.log('Saving...'); 55 | const analysis = analyzer.getAnalysis(); 56 | await saveAnalysis(analysis, analyzer.config); 57 | await analyzer.processReporters(analysis); 58 | 59 | console.log('Done.'); 60 | }; 61 | 62 | async function saveAnalysis(analysis, config) { 63 | const outputFilename = config.outputPath 64 | ? path.resolve(config.outputPath, 'analysis.json') 65 | : path.resolve(config.rootPath, 'analysis.json'); 66 | 67 | await writeJSON(analysis, outputFilename); 68 | } 69 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularFactoryParser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const astUtils = require('../../../utils/ast-utils'); 5 | const ngUtils = require('../../../utils/ng-utils'); 6 | const BaseParser = require('../BaseParser'); 7 | const ParserResult = require('../../../utils/parser-result'); 8 | 9 | module.exports = class AngularFactoryParser extends BaseParser { 10 | constructor() { 11 | super(); 12 | } 13 | 14 | extractComponents(module) { 15 | return astUtils.parseAST(module.contents, module.path) 16 | .then(ast => analyzeAllInFile(ast, module)) 17 | .catch(err => { 18 | console.error(`Error parsing AST for ${module.path}: `, err.stack); 19 | return ParserResult.create([], err, module); 20 | }); 21 | } 22 | }; 23 | 24 | function analyzeAllInFile(ast, module) { 25 | return Promise.resolve(getFactoryCallNodes(ast)) 26 | .then(factoryCallNodes => { 27 | return Promise.all(factoryCallNodes.map(node => getComponentDetails(node, module, ast))); 28 | }) 29 | .then(components => ParserResult.create(components)) 30 | .catch(err => ParserResult.create([], err, module)); 31 | } 32 | 33 | function getFactoryCallNodes(ast) { 34 | return astUtils 35 | .getNodesByType(ast.program, 'CallExpression') 36 | .filter(node => (astUtils.getDeepProperty(node, 'callee.property.name') === 'factory')); 37 | } 38 | 39 | function getComponentDetails(callNode, module, ast) { 40 | const definitionFunction = ngUtils.getDefinitionFunction(callNode, ast); 41 | return { 42 | name: ngUtils.getName(callNode), 43 | type: 'angular-factory', 44 | path: module.path, 45 | properties: { 46 | angularModule: ngUtils.getModuleName(callNode) 47 | }, 48 | dependencies: getDependencies(definitionFunction) 49 | }; 50 | } 51 | 52 | function getDependencies(definitionFunction) { 53 | if (definitionFunction) { 54 | return definitionFunction.params.map(p => p.name); 55 | } else { 56 | return []; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/DefaultParser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelCase = require('camel-case'); 5 | 6 | const astUtils = require('../../../utils/ast-utils'); 7 | const fsUtils = require('../../../utils/fs-utils'); 8 | const BaseParser = require('../BaseParser'); 9 | const ParserResult = require('../../../utils/parser-result'); 10 | 11 | module.exports = class DefaultParser extends BaseParser { 12 | constructor() { 13 | super(); 14 | } 15 | 16 | extractComponents(module) { 17 | return astUtils.parseAST(module.contents, module.path) 18 | .then(ast => analyzeDefaultComponent(ast, module)) 19 | .catch(err => { 20 | console.error(`Error parsing AST for ${module.path}: `, err.stack); 21 | return ParserResult.create([], err, module); 22 | }); 23 | } 24 | }; 25 | 26 | function analyzeDefaultComponent(ast, module) { 27 | return Promise.resolve(getComponentDetails(module, ast)) 28 | .then(component => ParserResult.create([component])) 29 | .catch(err => ParserResult.create([], err, module)); 30 | } 31 | 32 | function getComponentDetails(module, ast) { 33 | return { 34 | name: getSmartModuleName(module.path), 35 | type: 'default', 36 | path: module.path, 37 | dependencies: getDependencies(ast) 38 | }; 39 | } 40 | 41 | function getDependencies(ast) { 42 | return astUtils.getImportInfo(ast) 43 | .map(importItem => importItem.value); 44 | } 45 | 46 | function getSmartModuleName(filePath) { 47 | const pathPieces = filePath.split('/'); 48 | const filename = pathPieces[pathPieces.length - 1]; 49 | const directory = _maybeCamelCase(pathPieces[pathPieces.length - 2]); 50 | 51 | const filePieces = filename.split('.'); 52 | const filenameNoExt = _maybeCamelCase(filePieces[0]); 53 | 54 | if (filenameNoExt === 'index') { 55 | return directory; 56 | } else if (filenameNoExt === 'app') { 57 | return directory + 'App'; 58 | } else { 59 | return filenameNoExt; 60 | } 61 | } 62 | 63 | function _maybeCamelCase(name) { 64 | if (name.indexOf('-')) { 65 | return camelCase(name); 66 | } else { 67 | return name; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/ComponentSearcher/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Paper from 'material-ui/Paper'; 3 | 4 | import ComponentSearchBar from './ComponentSearchBar'; 5 | import ComponentSearchResults from './ComponentSearchResults'; 6 | 7 | import './ComponentSearcher.css'; 8 | 9 | export default class ComponentSearcher extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | searchText: '', 14 | searchResults: [] 15 | }; 16 | this._onSearchTextChange = this._onSearchTextChange.bind(this); 17 | this._onSelectSearchResult = this._onSelectSearchResult.bind(this); 18 | } 19 | 20 | _onSearchTextChange(updatedText) { 21 | this.setState({ 22 | searchText: updatedText, 23 | searchResults: filterSearchResults(this.props.components, updatedText) 24 | }); 25 | } 26 | 27 | _onSelectSearchResult(component) { 28 | this.setState({ 29 | searchText: '', 30 | searchResults: [] 31 | }); 32 | this.props.onSelect(component); 33 | } 34 | 35 | render() { 36 | const { searchText, searchResults } = this.state; 37 | return ( 38 |
39 | 40 | 44 | {searchResults.length === 0 ? null : ( 45 | 49 | )} 50 | 51 |
52 | ); 53 | } 54 | }; 55 | 56 | function filterSearchResults(components, searchText) { 57 | if (searchText.length === 0) return []; 58 | 59 | return components.filter(component => ( 60 | contains(component.name, searchText) || 61 | contains(component.path, searchText) || 62 | contains(component.type, searchText) 63 | )); 64 | } 65 | 66 | function contains(searchString, substring) { 67 | return (searchString.toLowerCase().indexOf(substring.toLowerCase()) !== -1); 68 | } 69 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/DefaultParser/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const DefaultParser = require('./index'); 5 | 6 | function makeModule(filename) { 7 | const modulePath = path.resolve(__dirname, './test-examples/', filename); 8 | return { 9 | path: modulePath, 10 | contents: loadContents(modulePath) 11 | }; 12 | } 13 | 14 | function getComponentFromResults(results) { 15 | if (results.errors.length > 0) { 16 | console.error('Error while extracting components:'); 17 | console.error(results.errors[0].stack); 18 | } 19 | expect(results.components).have.length(1); 20 | expect(results.errors).to.have.length(0); 21 | return results.components[0]; 22 | } 23 | 24 | describe('DefaultParser', () => { 25 | let examples = {}; 26 | 27 | before(() => { 28 | examples = { 29 | commonjs: makeModule('commonjs.js'), 30 | es6: makeModule('es6.js') 31 | }; 32 | }); 33 | 34 | describe('extractComponents', () => { 35 | 36 | it('should extract a component with CommonJS syntax', (done) => { 37 | const parser = new DefaultParser(); 38 | parser.extractComponents(examples.commonjs) 39 | .then(getComponentFromResults) 40 | .then(component => { 41 | expect(component.name).to.equal('commonjs'); 42 | expect(component.type).to.equal('default'); 43 | expect(component.dependencies).to.deep.equal([ 44 | 'thingOne', 45 | 'thingTwo' 46 | ]); 47 | done(); 48 | }) 49 | .catch(err => done(err)); 50 | }); 51 | 52 | it('should extract a component with ES6 syntax', (done) => { 53 | const parser = new DefaultParser(); 54 | parser.extractComponents(examples.es6) 55 | .then(getComponentFromResults) 56 | .then(component => { 57 | expect(component.name).to.equal('es6'); 58 | expect(component.type).to.equal('default'); 59 | expect(component.dependencies).to.deep.equal([ 60 | 'superCool', 61 | 'superFun' 62 | ]); 63 | done(); 64 | }) 65 | .catch(err => done(err)); 66 | }); 67 | 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/OwnershipPlugin/ProjectTreeAnnotations.js: -------------------------------------------------------------------------------- 1 | const DEBUG_ADD = false; 2 | const DEBUG_MATCH = false; 3 | 4 | module.exports = class ProjectTreeAnnotations { 5 | constructor() { 6 | this.root = makeNode('.'); 7 | } 8 | 9 | toJson() { 10 | return this.root; 11 | } 12 | 13 | add(path, metadata) { 14 | if (DEBUG_ADD) { 15 | console.log(`Adding ${path} to filesystem`); 16 | } 17 | 18 | const pieces = path.split('/'); 19 | let node = this.root; 20 | 21 | for (let i = 1; i < pieces.length; i++) { 22 | const piece = pieces[i]; 23 | if (DEBUG_ADD) { 24 | console.log(` Looking at ${piece}`); 25 | } 26 | 27 | let childNode = node.children.find(n => n.name === piece); 28 | if (!childNode) { 29 | if (DEBUG_ADD) { 30 | console.log(` No match, creating node`); 31 | } 32 | const newChild = makeNode(piece); 33 | node.children.push(newChild); 34 | childNode = newChild; 35 | } 36 | 37 | node = childNode; 38 | } 39 | 40 | node.metadata = metadata; 41 | } 42 | 43 | match(path) { 44 | if (DEBUG_MATCH) { 45 | console.log(`Looking for a match for ${path}`); 46 | } 47 | 48 | const pieces = path.split('/'); 49 | let node = this.root; 50 | let foundMetadata = null; 51 | 52 | for (let i = 1; i < pieces.length; i++) { 53 | if (node.metadata) { 54 | foundMetadata = node.metadata; 55 | if (DEBUG_MATCH) { 56 | console.log(` Found metadata: ${JSON.stringify(foundMetadata)}`); 57 | } 58 | } 59 | 60 | const piece = pieces[i]; 61 | if (DEBUG_MATCH) { 62 | console.log(` Looking at ${piece}`); 63 | } 64 | 65 | let childNode = node.children.find(n => n.name === piece); 66 | if (!childNode) { 67 | if (DEBUG_MATCH) { 68 | console.log(` Not in the tree`); 69 | } 70 | return foundMetadata; 71 | } 72 | node = childNode; 73 | } 74 | 75 | if (DEBUG_MATCH) { 76 | console.log(` Found match: ${JSON.stringify(node.metadata)}`); 77 | } 78 | return node.metadata; 79 | } 80 | } 81 | 82 | function makeNode(name) { 83 | return { 84 | name, 85 | children: [] 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /packages/hekla-cli/test/cases/simple/testcase.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { promisify } = require('util'); 4 | 5 | const analyze = require('../../../src/commands/analyze'); 6 | 7 | const stat = promisify(fs.stat); 8 | const readFile = promisify(fs.readFile); 9 | 10 | const CONFIG_FILE_LOCATION = path.resolve(__dirname, 'hekla.config.js'); 11 | const ANALYSIS_FILE_LOCATION = path.resolve(__dirname, 'dist', 'analysis.json'); 12 | const REPORTED_FILE_LOCATION = path.resolve(__dirname, 'dist', 'report.txt'); 13 | 14 | async function runAnalysis() { 15 | await analyze({ 16 | config: CONFIG_FILE_LOCATION 17 | }); 18 | } 19 | 20 | async function fileExists(filePath) { 21 | try { 22 | await stat(filePath); 23 | return true; 24 | } catch (err) { 25 | if (err.code === 'ENOENT') { 26 | return false; 27 | } else { 28 | throw err; 29 | } 30 | } 31 | } 32 | 33 | async function readFileContents(filePath) { 34 | const buffer = await readFile(filePath); 35 | const contents = buffer.toString('utf-8'); 36 | return contents; 37 | } 38 | 39 | describe('hekla-cli simple usage', () => { 40 | it('should create an analysis.json file', async () => { 41 | await runAnalysis(); 42 | const analysisFileExists = await fileExists(ANALYSIS_FILE_LOCATION); 43 | 44 | expect(analysisFileExists).toBe(true); 45 | }); 46 | 47 | it('should apply plugins to modules', async () => { 48 | await runAnalysis(); 49 | const analysisFileExists = await fileExists(ANALYSIS_FILE_LOCATION); 50 | 51 | expect(analysisFileExists).toBe(true); 52 | 53 | const contentsJson = await readFileContents(ANALYSIS_FILE_LOCATION); 54 | const contents = JSON.parse(contentsJson); 55 | expect(contents).toHaveProperty('modules'); 56 | expect(contents.modules).toHaveLength(4); 57 | 58 | for (let module of contents.modules) { 59 | expect(module.visited).toBe(true); 60 | } 61 | }); 62 | 63 | it('should run reporter plugins', async () => { 64 | await runAnalysis(); 65 | 66 | const reportedFileExists = await fileExists(REPORTED_FILE_LOCATION); 67 | expect(reportedFileExists).toBe(true); 68 | 69 | const contents = await readFileContents(REPORTED_FILE_LOCATION); 70 | expect(contents).toEqual('I found 4 modules!'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/parser-result/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class ParserResult { 4 | /** 5 | * @private 6 | */ 7 | constructor(components, parserErrorList) { 8 | if (!components || components.length === undefined) { 9 | throw new Error('components not specified'); 10 | } 11 | 12 | if (!parserErrorList || parserErrorList.length === undefined) { 13 | throw new Error('parserErrorList not specified'); 14 | } 15 | 16 | this.components = components; 17 | this.errors = parserErrorList; 18 | } 19 | 20 | static create(components, errors, module) { 21 | let parserErrorList; 22 | 23 | if (!errors) { 24 | parserErrorList = []; 25 | } else { 26 | // We have errors 27 | if (!errors.length) { 28 | // Single error = make into an array 29 | const error = errors; 30 | parserErrorList = [(new ParserError(error, module))]; 31 | } else { 32 | // Array of errors 33 | parserErrorList = errors.map(e => (new ParserError(e, module))); 34 | } 35 | } 36 | 37 | return new ParserResult(components, parserErrorList); 38 | } 39 | 40 | static mergeAll(analysisResultArray) { 41 | analysisResultArray.forEach(result => { 42 | if (!(result instanceof ParserResult)) { 43 | throw new Error('Trying to merge an object that is not a ParserResult'); 44 | } 45 | }); 46 | 47 | const mergedComponents = mergeArrays(analysisResultArray.map(result => result.components)); 48 | const mergedErrors = mergeArrays(analysisResultArray.map(result => result.errors)); 49 | return new ParserResult(mergedComponents, mergedErrors); 50 | } 51 | }; 52 | 53 | function mergeArrays(arrays) { 54 | return [].concat.apply([], arrays); 55 | } 56 | 57 | class ParserError { 58 | constructor(error, module) { 59 | if (error === undefined || module === undefined) { 60 | throw new Error('Invalid arguments for ParserError constructor'); 61 | } 62 | 63 | this.reason = error; 64 | this.message = error.message; 65 | this.stack = error.stack; 66 | this.modulePath = module.path; 67 | } 68 | 69 | toString() { 70 | return `Parsing error in module '${this.modulePath}':\n${this.message}`; 71 | } 72 | } 73 | 74 | ParserResult._ParserError = ParserError; // Export for testing purposes - don't use in prod code! 75 | 76 | module.exports = ParserResult; 77 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/ComponentContextMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Paper from 'material-ui/Paper'; 3 | import Menu from 'material-ui/Menu'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | import ArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward'; 6 | import ArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward'; 7 | import Delete from 'material-ui/svg-icons/action/delete'; 8 | import Divider from 'material-ui/Divider'; 9 | 10 | const OFFSET_X = 5; 11 | 12 | const ComponentContextMenu = (props) => { 13 | const { 14 | component, 15 | projectGraph, 16 | subgraph, 17 | x, 18 | y, 19 | onExpandDependants, 20 | onExpandDependencies, 21 | onRemove 22 | } = props; 23 | const { id } = component; 24 | 25 | const totalDependantCount = projectGraph.getLinksTo(id).length; 26 | const visibleDependantCount = subgraph.getLinksTo(id).length; 27 | const canExpandDependants = (totalDependantCount - visibleDependantCount > 0); 28 | 29 | const totalDependencyCount = projectGraph.getLinksFrom(id).length; 30 | const visibleDependencyCount = subgraph.getLinksFrom(id).length; 31 | const canExpandDependencies = (totalDependencyCount - visibleDependencyCount > 0); 32 | 33 | const style = { 34 | left: (x + OFFSET_X), 35 | top: y 36 | }; 37 | return ( 38 |
39 | 40 | 41 | {(canExpandDependants ? ( 42 | } 45 | onTouchTap={onExpandDependants} 46 | /> 47 | ) : null)} 48 | {(canExpandDependencies ? ( 49 | } 52 | onTouchTap={onExpandDependencies} 53 | /> 54 | ) : null)} 55 | {(canExpandDependants && canExpandDependencies) ? ( 56 | 57 | ) : null} 58 | } 61 | onTouchTap={onRemove} 62 | /> 63 | 64 | 65 |
66 | ); 67 | }; 68 | export default ComponentContextMenu 69 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/fs-utils/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const glob = require('glob'); 3 | 4 | module.exports = { 5 | getProjectFiles, 6 | getFileExists, 7 | getFileContents, 8 | writeJSON, 9 | getModuleName, 10 | getModuleShortName 11 | }; 12 | 13 | const defaultOptions = { 14 | ignorePatterns: [], 15 | globPattern: '**' 16 | }; 17 | function getProjectFiles(rootPath, options) { 18 | const combinedOptions = { ...defaultOptions, ...options }; 19 | return new Promise((resolve, reject) => { 20 | const globOptions = { 21 | cwd: rootPath, 22 | ignore: combinedOptions.ignorePatterns, 23 | absolute: true, 24 | nodir: true 25 | }; 26 | glob(combinedOptions.globPattern, globOptions, (err, files) => { 27 | if (err) return reject(err); 28 | 29 | resolve(files); 30 | }); 31 | }); 32 | } 33 | 34 | 35 | function getFileExists(filePath) { 36 | return new Promise((resolve, reject) => { 37 | fs.stat(filePath, (err, stats) => { 38 | if (err) return reject(err); 39 | 40 | resolve(stats.isFile()); 41 | }); 42 | }); 43 | } 44 | 45 | function getFileContents(filePath) { 46 | return new Promise((resolve, reject) => { 47 | fs.readFile(filePath, 'utf-8', (err, contents) => { 48 | if (err) return reject(err); 49 | 50 | resolve(contents); 51 | }); 52 | }); 53 | } 54 | 55 | function writeJSON(data, filePath) { 56 | return new Promise((resolve, reject) => { 57 | fs.writeFile(filePath, JSON.stringify(data, null, 2), err => { 58 | if (err) return reject(err); 59 | 60 | resolve(); 61 | }); 62 | }) 63 | } 64 | 65 | function getModuleName(resource, rootPath) { 66 | if (!rootPath) { 67 | throw new Error('rootPath not specified'); 68 | } 69 | let fullPath = resource; 70 | if (fullPath.indexOf('!') !== -1) { 71 | const pieces = resource.split('!'); 72 | fullPath = pieces[pieces.length - 1]; 73 | } 74 | const projectPath = fullPath.replace(rootPath, ''); 75 | return `.${projectPath}`; 76 | } 77 | 78 | function getModuleShortName(moduleName) { 79 | const pathPieces = moduleName.split('/'); 80 | const filename = pathPieces[pathPieces.length - 1]; 81 | const directory = pathPieces[pathPieces.length - 2]; 82 | 83 | const filePieces = filename.split('.'); 84 | const filenameNoExt = filePieces[0]; 85 | 86 | if (['index', 'app'].includes(filenameNoExt)) { 87 | return `${directory}/${filename}`; 88 | } else { 89 | return filename; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularFactoryParser/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const AngularFactoryParser = require('./index'); 5 | 6 | function makeModule(filename) { 7 | const modulePath = path.resolve(__dirname, './test-examples/', filename); 8 | return { 9 | path: modulePath, 10 | contents: loadContents(modulePath) 11 | }; 12 | } 13 | 14 | function getComponentFromResults(results) { 15 | if (results.errors.length > 0) { 16 | console.error('Error while extracting components:'); 17 | console.error(results.errors[0].stack); 18 | } 19 | expect(results.components).have.length(1); 20 | expect(results.errors).to.have.length(0); 21 | return results.components[0]; 22 | } 23 | 24 | describe('AngularFactoryParser', () => { 25 | let examples = {}; 26 | 27 | before(() => { 28 | examples = { 29 | basic: makeModule('basic.js'), 30 | splitDefinition: makeModule('split-definition.js') 31 | }; 32 | }); 33 | 34 | describe('extractComponents', () => { 35 | 36 | it('should extract a basic factory', (done) => { 37 | const parser = new AngularFactoryParser(); 38 | parser.extractComponents(examples.basic) 39 | .then(getComponentFromResults) 40 | .then(component => { 41 | expect(component.name).to.equal('animalService'); 42 | expect(component.type).to.equal('angular-factory'); 43 | expect(component.properties).to.deep.equal({ 44 | angularModule: 'app' 45 | }); 46 | expect(component.dependencies).to.deep.equal([ 47 | '$http', 48 | 'anotherService' 49 | ]); 50 | done(); 51 | }) 52 | .catch(err => done(err)); 53 | }); 54 | 55 | it('should extract a factory with a split definition function', (done) => { 56 | const parser = new AngularFactoryParser(); 57 | parser.extractComponents(examples.splitDefinition) 58 | .then(getComponentFromResults) 59 | .then(component => { 60 | expect(component.name).to.equal('animalService'); 61 | expect(component.type).to.equal('angular-factory'); 62 | expect(component.properties).to.deep.equal({ 63 | angularModule: 'app' 64 | }); 65 | expect(component.dependencies).to.deep.equal([ 66 | '$http', 67 | 'anotherService' 68 | ]); 69 | done(); 70 | }) 71 | .catch(err => done(err)); 72 | }); 73 | 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/ASTWrapper.spec.js: -------------------------------------------------------------------------------- 1 | const ASTWrapper = require('./ASTWrapper'); 2 | const { parseAST } = require('./index'); 3 | 4 | describe('ASTWrapper', () => { 5 | 6 | describe('unwrap', () => { 7 | 8 | it('should return the AST it wraps', async () => { 9 | const js = `console.log('hello world')`; 10 | const ast = await parseAST(js); 11 | const wrapper = new ASTWrapper(ast); 12 | expect(wrapper.unwrap()).to.equal(ast); 13 | }); 14 | 15 | }); 16 | 17 | describe('visit', () => { 18 | 19 | it('should call the visitor when it finds a matching node', async () => { 20 | const js = `console.log('hello world')`; 21 | const ast = await parseAST(js); 22 | const wrapper = new ASTWrapper(ast); 23 | const identifiers = []; 24 | wrapper.visit({ 25 | Identifier(node) { 26 | identifiers.push(node.name); 27 | } 28 | }); 29 | expect(identifiers).to.deep.equal(['console', 'log']) 30 | }); 31 | 32 | it('should work for FunctionDeclaration nodes', async () => { 33 | const js = ` 34 | function add(x, y) { 35 | return x + y; 36 | } 37 | `; 38 | const ast = await parseAST(js); 39 | const wrapper = new ASTWrapper(ast); 40 | let functionNode = null; 41 | wrapper.visit({ 42 | FunctionDeclaration(node) { 43 | functionNode = node; 44 | } 45 | }); 46 | expect(functionNode).to.not.be.undefined; 47 | expect(functionNode.id.type).to.equal('Identifier'); 48 | expect(functionNode.id.name).to.equal('add'); 49 | }); 50 | 51 | it('should work for multiple visitors at the same time', async () => { 52 | const js = ` 53 | function add(x, y) { 54 | return x + y; 55 | } 56 | 57 | function sub(x, y) { 58 | return x - y; 59 | } 60 | `; 61 | const ast = await parseAST(js); 62 | const wrapper = new ASTWrapper(ast); 63 | let functions = 0; 64 | const identifiers = []; 65 | wrapper.visit({ 66 | FunctionDeclaration(node) { 67 | functions++; 68 | }, 69 | Identifier(node) { 70 | identifiers.push(node.name); 71 | } 72 | }); 73 | expect(functions).to.equal(2); 74 | expect(identifiers).to.deep.equal([ 75 | 'add', 76 | 'x', 77 | 'y', 78 | 'x', 79 | 'y', 80 | 'sub', 81 | 'x', 82 | 'y', 83 | 'x', 84 | 'y', 85 | ]); 86 | }); 87 | 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/DOMWrapper.spec.js: -------------------------------------------------------------------------------- 1 | const DOMWrapper = require('./DOMWrapper'); 2 | const { parseHTML } = require('./index'); 3 | 4 | describe('DOMWrapper', () => { 5 | 6 | describe('unwrap', () => { 7 | 8 | it('should return the DOM it wraps', async () => { 9 | const html = '
Hello world
'; 10 | const dom = await parseHTML(html); 11 | const wrapper = new DOMWrapper(dom); 12 | expect(wrapper.unwrap()).to.equal(dom); 13 | }); 14 | 15 | }); 16 | 17 | describe('visit', () => { 18 | 19 | it('should call the visitor on each tag', async () => { 20 | const html = '
Hello world
'; 21 | const dom = await parseHTML(html); 22 | const wrapper = new DOMWrapper(dom); 23 | const foundTags = []; 24 | wrapper.visit({ 25 | tag: (node) => { 26 | foundTags.push(node.name); 27 | } 28 | }); 29 | expect(foundTags).to.deep.equal(['div', 'b']); 30 | }); 31 | 32 | it('should call the visitor on each text node', async () => { 33 | const html = '
Hello world
'; 34 | const dom = await parseHTML(html); 35 | const wrapper = new DOMWrapper(dom); 36 | let fullText = ''; 37 | wrapper.visit({ 38 | text: (node) => { 39 | fullText += node.data; 40 | } 41 | }); 42 | expect(fullText).to.equal('Hello world'); 43 | }); 44 | 45 | it('should handle multiple visitors', async () => { 46 | const html = '
Hello world
'; 47 | const dom = await parseHTML(html); 48 | const wrapper = new DOMWrapper(dom); 49 | 50 | // Learn lots of things about the tree in a single pass 51 | let foundTags = []; 52 | let tagCount = 0; 53 | let fullText = ''; 54 | wrapper.visit({ 55 | tag: (node) => { 56 | foundTags.push(node.name); 57 | } 58 | }); 59 | wrapper.visit({ 60 | tag: (node) => { 61 | tagCount++; 62 | }, 63 | text: (node) => { 64 | fullText += node.data; 65 | } 66 | }); 67 | expect(foundTags).to.deep.equal(['div', 'b']); 68 | expect(tagCount).to.equal(2); 69 | expect(fullText).to.equal('Hello world'); 70 | }); 71 | 72 | it('should reject visitors for node types that do not exist', async () => { 73 | const html = '
Hello world
'; 74 | const dom = await parseHTML(html); 75 | const wrapper = new DOMWrapper(dom); 76 | expect(() => { 77 | wrapper.visit({ 78 | truck: (node) => {} 79 | }); 80 | }).to.throw('Invalid visitor type: truck'); 81 | }); 82 | 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /packages/hekla-core/src/WorkQueue.js: -------------------------------------------------------------------------------- 1 | const asyncLib = require('async'); 2 | 3 | const { 4 | moduleQueued, 5 | moduleSuccessful, 6 | moduleFailed 7 | } = require('./StatusMessage'); 8 | 9 | module.exports = class WorkQueue { 10 | constructor(analyzer) { 11 | this.analyzer = analyzer; 12 | this.statusCallbacks = []; 13 | this.started = false; 14 | } 15 | 16 | start(workerCount) { 17 | this.started = true; 18 | this.workerCount = workerCount; 19 | this.workersOccupied = new Set(); 20 | 21 | this.queueWorking = true; 22 | this.queueDrainResolver = null; 23 | this.queueDrainRejecter = null; 24 | 25 | this.queue = asyncLib.queue(this._queueWorker.bind(this), workerCount); 26 | this.queue.drain = () => { 27 | this.queueWorking = false; 28 | if (this.queueDrainResolver) { 29 | this.queueDrainResolver(); 30 | } 31 | }; 32 | } 33 | 34 | enqueue(module) { 35 | if (!this.started) { 36 | throw new Error('WorkQueue has not been started yet'); 37 | } 38 | 39 | this.queue.push(makeTask(module)) 40 | } 41 | 42 | async waitForFinish() { 43 | if (!this.queueWorking) { 44 | return Promise.resolve(); 45 | } 46 | 47 | return new Promise((resolve, reject) => { 48 | this.queueDrainResolver = resolve; 49 | this.queueDrainRejecter = reject; 50 | }); 51 | } 52 | 53 | onStatusUpdate(callback) { 54 | this.statusCallbacks.push(callback); 55 | 56 | // Return an unsubscribe function 57 | return () => { 58 | this.statusCallbacks = this.statusCallbacks 59 | .filter(cb => cb !== callback); 60 | } 61 | } 62 | 63 | // Private methods 64 | 65 | async _queueWorker(task) { 66 | this.queueWorking = true; 67 | const { module } = task; 68 | 69 | const workerId = this._claimWorkerId(); 70 | 71 | try { 72 | this._sendStatusUpdate(moduleQueued(module.getName(), workerId)); 73 | 74 | await this.analyzer.processModule(module); 75 | this._sendStatusUpdate(moduleSuccessful(module.getName(), workerId)); 76 | this._freeWorkerId(workerId); 77 | } catch (err) { 78 | this._sendStatusUpdate(moduleFailed(module.getName(), workerId, err)); 79 | } 80 | } 81 | 82 | _sendStatusUpdate(statusMessage) { 83 | for (let callback of this.statusCallbacks) { 84 | callback(statusMessage); 85 | } 86 | } 87 | 88 | _claimWorkerId() { 89 | let availableId = -1; 90 | for (let id = 0; id < this.workerCount; id++) { 91 | if (!this.workersOccupied.has(id)) { 92 | availableId = id; 93 | break; 94 | } 95 | } 96 | if (availableId === -1) { 97 | throw new Error('No free workers'); 98 | } 99 | this.workersOccupied.add(availableId); 100 | return availableId; 101 | } 102 | 103 | _freeWorkerId(id) { 104 | if (id === -1) { 105 | throw new Error('Unknown renderer'); 106 | } 107 | this.workersOccupied.delete(id); 108 | } 109 | } 110 | 111 | function makeTask(module) { 112 | return { module }; 113 | } 114 | -------------------------------------------------------------------------------- /packages/hekla-plugin-csv-reporter/README.md: -------------------------------------------------------------------------------- 1 | # hekla-plugin-csv-reporter 2 | 3 | A Hekla plugin to report analysis through CSV files 4 | 5 | ## Usage 6 | 7 | Install the package into your project: 8 | 9 | ```bash 10 | npm install --save-dev hekla-plugin-csv-reporter 11 | ``` 12 | 13 | Then add it to your `hekla.config.js` file, with configuration: 14 | 15 | ```js 16 | const CSVReporterPlugin = require('hekla-plugin-csv-reporter'); 17 | 18 | module.exports = { 19 | // ... 20 | plugins: [ 21 | // ... 22 | new CSVReporterPlugin({ 23 | destination: '/path/to/output.csv', 24 | headers: [ 25 | 'file', 26 | 'myProperty', 27 | 'myOtherProperty' 28 | ], 29 | moduleToRows: (module) => ([ 30 | [ 31 | module.name, 32 | module.myProperty, 33 | module.myOtherProperty 34 | ] 35 | ]) 36 | }) 37 | ] 38 | }; 39 | ``` 40 | 41 | ## Usage with list properties 42 | 43 | Suppose you track a list of items for each module, like this example: 44 | 45 | ```json 46 | { 47 | "modules": [ 48 | { 49 | "name": "./common/components/AppWrapper.js", 50 | "shortName": "AppWrapper.js", 51 | "nativeElements": [ 52 | "div", 53 | "footer", 54 | "h1", 55 | "header", 56 | "main" 57 | ] 58 | } 59 | { 60 | "name": "./my-feature/components/ContactForm.js", 61 | "shortName": "ContactForm.js", 62 | "nativeElements": [ 63 | "button", 64 | "div", 65 | "input", 66 | "span", 67 | "textarea" 68 | ] 69 | }, 70 | ] 71 | } 72 | ``` 73 | 74 | You could create a row for each item in the module, with a `hekla.config.js` configuration like this: 75 | 76 | ```js 77 | const CSVReporterPlugin = require('hekla-plugin-csv-reporter'); 78 | 79 | module.exports = { 80 | // ... 81 | plugins: [ 82 | // ... 83 | new CSVReporterPlugin({ 84 | destination: '/path/to/output.csv', 85 | headers: [ 86 | 'file', 87 | 'nativeElement' 88 | ], 89 | moduleToRows: (module) => 90 | module.nativeElements 91 | .map(nativeElement => ([module.name, nativeElement])) 92 | }) 93 | ] 94 | }; 95 | ``` 96 | 97 | This would create the following data in `output.csv`: 98 | 99 | | file | nativeElement | 100 | | -------------------------------------- | ------------- | 101 | | ./common/components/AppWrapper.js | div | 102 | | ./common/components/AppWrapper.js | footer | 103 | | ./common/components/AppWrapper.js | h1 | 104 | | ./common/components/AppWrapper.js | header | 105 | | ./common/components/AppWrapper.js | main | 106 | | ./my-feature/components/ContactForm.js | button | 107 | | ./my-feature/components/ContactForm.js | div | 108 | | ./my-feature/components/ContactForm.js | input | 109 | | ./my-feature/components/ContactForm.js | span | 110 | | ./my-feature/components/ContactForm.js | textarea | 111 | -------------------------------------------------------------------------------- /packages/hekla-core/src/plugins/StatusUpdatePlugin.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const StickyTerminalDisplay = require('sticky-terminal-display'); 3 | const { TYPES } = require('../StatusMessage'); 4 | 5 | const MAX_MODULE_NAME_LENGTH = 80; 6 | 7 | module.exports = class StatusUpdatePlugin { 8 | apply(analyzer) { 9 | analyzer.hooks.statusUpdate.tap('StatusUpdatePlugin', this.onStatusUpdate.bind(this)); 10 | } 11 | 12 | onStatusUpdate(message) { 13 | try { 14 | switch (message.type) { 15 | case TYPES.STATUS_ANALYSIS_STARTED: 16 | return this.analysisStarted(message.payload.workerCount); 17 | 18 | case TYPES.STATUS_ANALYSIS_SUCCESSFUL: 19 | return this.analysisSuccessful(); 20 | 21 | case TYPES.STATUS_MODULE_QUEUED: 22 | return this.moduleQueued(message.payload.moduleName, message.payload.workerId); 23 | 24 | case TYPES.STATUS_MODULE_SUCCESSFUL: 25 | return this.moduleSuccessful(message.payload.workerId); 26 | 27 | case TYPES.STATUS_MODULE_FAILED: 28 | return this.moduleFailed(message.payload.workerId); 29 | 30 | default: 31 | throw new Exception(`Unhandled status update type: ${message.type}`); 32 | } 33 | } catch (err) { 34 | console.log('Error in event handler:', err); 35 | throw err; 36 | } 37 | } 38 | 39 | // Event handlers 40 | 41 | analysisStarted(workerCount) { 42 | this.workerCount = workerCount; 43 | this.stats = { 44 | completed: 0, 45 | errors: 0, 46 | found: 0 47 | }; 48 | const display = new StickyTerminalDisplay(); 49 | 50 | this.summaryRenderer = display.getLineRenderer(); 51 | 52 | this.workerRenderers = []; 53 | for (let i = 0; i < workerCount; i++) { 54 | const renderer = display.getLineRenderer(); 55 | this.workerRenderers.push(renderer); 56 | } 57 | } 58 | 59 | analysisSuccessful() { 60 | } 61 | 62 | moduleQueued(moduleName, workerId) { 63 | const truncatedModuleName = truncateStringRight(moduleName, MAX_MODULE_NAME_LENGTH); 64 | this.getWorkerRenderer(workerId) 65 | .write(` ${chalk.bold(`Worker ${workerId + 1}`)}: ${truncatedModuleName}`); 66 | 67 | this.stats.found++; 68 | this.printSummary(); 69 | } 70 | 71 | moduleSuccessful(workerId) { 72 | this.getWorkerRenderer(workerId) 73 | .write(` ${chalk.bold(`Worker ${workerId + 1}`)}: free`); 74 | 75 | this.stats.completed++; 76 | this.printSummary(); 77 | } 78 | 79 | moduleFailed(workerId) { 80 | this.getWorkerRenderer(workerId) 81 | .write(` ${chalk.bold(`Worker ${workerId + 1}`)}: free`); 82 | 83 | this.stats.errors++; 84 | this.printSummary(); 85 | } 86 | 87 | // Rendering helpers 88 | 89 | printSummary() { 90 | const { completed, errors, found } = this.stats; 91 | const processed = completed + errors; 92 | this.summaryRenderer.write(`Hekla: processed ${processed}/${found} modules with ${errors} errors`); 93 | } 94 | 95 | getWorkerRenderer(workerId) { 96 | return this.workerRenderers[workerId]; 97 | } 98 | } 99 | 100 | function truncateStringRight(input, maxLength) { 101 | if (input.length <= maxLength) { 102 | return input; 103 | } 104 | 105 | const truncated = input.substring(input.length - maxLength + 3); 106 | return `...${truncated}`; 107 | } 108 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/src/HeklaWebpackPlugin.js: -------------------------------------------------------------------------------- 1 | const minimatch = require('minimatch'); 2 | const { 3 | Analyzer, 4 | ConfigValidator, 5 | Module 6 | } = require('hekla-core'); 7 | const { getModuleName } = require('hekla-core').fsUtils; 8 | 9 | const WORKER_COUNT = 5; 10 | 11 | module.exports = class HeklaWebpackPlugin { 12 | constructor(config) { 13 | this.config = config || {}; 14 | this.analyzer = new Analyzer(); 15 | 16 | this.foundResources = new Set(); 17 | } 18 | 19 | // Webpack lifecycle hooks 20 | 21 | apply(compiler) { 22 | const validator = new ConfigValidator(); 23 | validator.validate(this.config); 24 | if (!validator.isValid()) { 25 | console.log('Invalid Hekla configuration:'); 26 | for (let error of validator.getErrors()) { 27 | console.log(` ${error}`); 28 | } 29 | console.log(); 30 | throw new Error('Invalid Hekla configuration'); 31 | } 32 | 33 | this.analyzer.applyConfig(this.config); 34 | this.analyzer.startWorkers(WORKER_COUNT); 35 | 36 | if (compiler.hooks) { 37 | // Webpack 4+ 38 | compiler.hooks.emit.tapPromise('AnalysisPlugin', this.emit.bind(this)); 39 | 40 | compiler.hooks.compilation.tap('AnalysisPlugin', (compilation) => { 41 | this.analyzer.setInputFileSystem(compilation.inputFileSystem); 42 | compilation.hooks.succeedModule.tap('AnalysisPlugin', this.succeedModule.bind(this)); 43 | }); 44 | } else { 45 | // Webpack 3 46 | compiler.plugin('emit', this.emit.bind(this)); 47 | 48 | compiler.plugin('compilation', (compilation) => { 49 | this.analyzer.setInputFileSystem(compilation.inputFileSystem); 50 | compilation.plugin('succeed-module', this.succeedModule.bind(this)); 51 | }); 52 | } 53 | } 54 | 55 | succeedModule(webpackModule) { 56 | const { resource } = webpackModule; 57 | if (typeof resource === 'undefined') { 58 | return; 59 | } 60 | 61 | const sanitizedResource = resource.replace(/\?.*$/, ''); 62 | const moduleName = getModuleName(sanitizedResource, this.analyzer.config.rootPath); 63 | 64 | if (moduleName.match(/node_modules/)) { 65 | return; 66 | } 67 | 68 | if (this.config.exclude) { 69 | for (let excludePattern of this.config.exclude) { 70 | if (minimatch(moduleName, excludePattern)) { 71 | return; 72 | } 73 | } 74 | } 75 | 76 | if (this.foundResources.has(sanitizedResource)) { 77 | return; 78 | } 79 | this.foundResources.add(sanitizedResource); 80 | 81 | const fileModule = new Module(sanitizedResource, this.analyzer.config.rootPath); 82 | this.analyzer.queueProcessModule(fileModule); 83 | } 84 | 85 | emit(compilation) { 86 | let analysis; 87 | return this.analyzer.waitForWorkers() 88 | .then(() => { 89 | analysis = this.analyzer.getAnalysis(); 90 | return this.analyzer.processReporters(analysis); 91 | }) 92 | .then(() => { 93 | const analysisFile = JSON.stringify(analysis, null, 2); 94 | 95 | compilation.assets['analysis.json'] = { 96 | source: function() { 97 | return analysisFile; 98 | }, 99 | size: function() { 100 | return analysisFile.length; 101 | } 102 | }; 103 | }) 104 | .catch(err => { 105 | console.error('Error waiting for analysis:', err); 106 | throw err; 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/hekla-core/src/ConfigValidator.spec.js: -------------------------------------------------------------------------------- 1 | const ConfigValidator = require('./ConfigValidator'); 2 | 3 | class ValidPlugin { 4 | apply(analyzer) {} 5 | } 6 | 7 | class PluginWithoutApplyMethod { 8 | otherMethod() {} 9 | } 10 | 11 | describe('ConfigValidator', () => { 12 | 13 | describe('validate', () => { 14 | 15 | describe('rootPath', () => { 16 | 17 | it('should be required', () => { 18 | const validator = new ConfigValidator(); 19 | validator.validate({}); 20 | expect(validator.getErrors()).to.deep.equal(['rootPath is not configured']); 21 | }); 22 | 23 | }); 24 | 25 | describe('outputPath', () => { 26 | 27 | it('should allow strings', () => { 28 | const validator = new ConfigValidator(); 29 | validator.validate({ 30 | rootPath: '/path/to/project', 31 | outputPath: '/path/to/project/build', 32 | }); 33 | expect(validator.isValid()).to.be.true; 34 | }); 35 | 36 | it('should fail on invalid input', () => { 37 | const validator = new ConfigValidator(); 38 | validator.validate({ 39 | rootPath: '/path/to/project', 40 | outputPath: 123, 41 | }); 42 | expect(validator.getErrors()).to.deep.equal(['Output path is not a string']); 43 | }); 44 | 45 | it('should fail if the path includes a file name', () => { 46 | const validator = new ConfigValidator(); 47 | validator.validate({ 48 | rootPath: '/path/to/project', 49 | outputPath: '/path/to/project/build/analysis.json', 50 | }); 51 | expect(validator.getErrors()).to.deep.equal(['Output path must be a directory']); 52 | }); 53 | 54 | }); 55 | 56 | describe('exclude', () => { 57 | 58 | it('should allow strings', () => { 59 | const validator = new ConfigValidator(); 60 | validator.validate({ 61 | rootPath: '/path/to/project', 62 | exclude: [ 63 | 'src/old/**', 64 | 'src/ignore/**', 65 | 'src/deprecated/**', 66 | 'vendor/**', 67 | '**/*.png' 68 | ] 69 | }); 70 | expect(validator.isValid()).to.be.true; 71 | }); 72 | 73 | it('should fail on invalid input', () => { 74 | const validator = new ConfigValidator(); 75 | validator.validate({ 76 | rootPath: '/path/to/project', 77 | exclude: [ 78 | 'src/old/**', 79 | 1234 80 | ] 81 | }); 82 | expect(validator.getErrors()).to.deep.equal(['Exclude pattern is not a string']); 83 | }); 84 | 85 | it('should fail on regular expressions', () => { 86 | const validator = new ConfigValidator(); 87 | validator.validate({ 88 | rootPath: '/path/to/project', 89 | exclude: [ 90 | 'src/old/**', 91 | /vendor/ 92 | ] 93 | }); 94 | expect(validator.getErrors()).to.deep.equal(['Exclude pattern is not a string']); 95 | }); 96 | 97 | }); 98 | 99 | describe('plugins', () => { 100 | 101 | it('should accept plugins', () => { 102 | const validator = new ConfigValidator(); 103 | validator.validate({ 104 | rootPath: '/path/to/project', 105 | plugins: [ 106 | new ValidPlugin() 107 | ] 108 | }); 109 | expect(validator.isValid()).to.be.true; 110 | }); 111 | 112 | it('should reject plugins without an apply method', () => { 113 | const validator = new ConfigValidator(); 114 | validator.validate({ 115 | rootPath: '/path/to/project', 116 | plugins: [ 117 | new PluginWithoutApplyMethod() 118 | ] 119 | }); 120 | expect(validator.getErrors()).to.deep.equal(['Plugin does not have an `apply` method']); 121 | }); 122 | 123 | }); 124 | 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/parser-result/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ParserResult = require('./index'); 4 | const ParserError = ParserResult._ParserError; 5 | 6 | describe('ParserResult', () => { 7 | 8 | let parsedComponentsA; 9 | let parsedComponentsB; 10 | let parserError; 11 | let parserErrorList; 12 | let moduleA; 13 | let moduleB; 14 | 15 | beforeEach(() => { 16 | parsedComponentsA = [ 17 | { name: 'One' }, 18 | { name: 'Two' }, 19 | { name: 'Three' } 20 | ]; 21 | parsedComponentsB = [ 22 | { name: 'Four' }, 23 | { name: 'Five' }, 24 | ]; 25 | parserError = new Error('Something bad happened'); 26 | parserErrorList = [ 27 | new Error('Something bad happened for sure'), 28 | new Error('Another bad thing happened') 29 | ]; 30 | moduleA = { 31 | id: 1, 32 | path: '/path/to/module-a/index.js' 33 | }; 34 | moduleB = { 35 | id: 2, 36 | path: '/path/to/module-b/index.js' 37 | }; 38 | }); 39 | 40 | describe('create', () => { 41 | 42 | it('should accept an array of components', () => { 43 | const result = ParserResult.create(parsedComponentsA); 44 | expect(result.components).to.have.length(3); 45 | expect(result.errors).to.have.length(0); 46 | }); 47 | 48 | it('should accept an error and a module', () => { 49 | const result = ParserResult.create([], parserError, moduleA); 50 | expect(result.components).to.have.length(0); 51 | expect(result.errors).to.have.length(1); 52 | 53 | const wrappedError = result.errors[0]; 54 | expect(wrappedError).to.be.instanceof(ParserError); 55 | expect(wrappedError.reason).to.equal(parserError); 56 | expect(wrappedError.message).to.equal(parserError.message); 57 | expect(wrappedError.stack).to.equal(parserError.stack); 58 | expect(wrappedError.modulePath).to.equal(moduleA.path); 59 | }); 60 | 61 | it('should accept a list of errors and a module', () => { 62 | const result = ParserResult.create([], parserErrorList, moduleB); 63 | expect(result.components).to.have.length(0); 64 | expect(result.errors).to.have.length(2); 65 | 66 | const firstError = result.errors[0]; 67 | expect(firstError).to.be.instanceof(ParserError); 68 | expect(firstError.reason).to.equal(parserErrorList[0]); 69 | expect(firstError.message).to.equal(parserErrorList[0].message); 70 | expect(firstError.stack).to.equal(parserErrorList[0].stack); 71 | expect(firstError.modulePath).to.equal(moduleB.path); 72 | 73 | const secondError = result.errors[1]; 74 | expect(secondError).to.be.instanceof(ParserError); 75 | expect(secondError.reason).to.equal(parserErrorList[1]); 76 | expect(secondError.message).to.equal(parserErrorList[1].message); 77 | expect(secondError.stack).to.equal(parserErrorList[1].stack); 78 | expect(secondError.modulePath).to.equal(moduleB.path); 79 | }); 80 | 81 | it('should throw an error if there are errors but no module', () => { 82 | expect(() => ParserResult.create([], parserErrorList)).to.throw(/Invalid arguments for ParserError constructor/); 83 | }); 84 | 85 | }); 86 | 87 | describe('mergeAll', () => { 88 | 89 | it('should combine two ParserResult objects into one', () => { 90 | const firstResult = ParserResult.create(parsedComponentsA); 91 | const secondResult = ParserResult.create([], parserErrorList, moduleA); 92 | const combinedResult = ParserResult.mergeAll([ 93 | firstResult, 94 | secondResult 95 | ]); 96 | expect(combinedResult.components).to.have.length(3); 97 | expect(combinedResult.components).to.deep.equal([ 98 | { name: 'One' }, 99 | { name: 'Two' }, 100 | { name: 'Three' } 101 | ]); 102 | expect(combinedResult.errors).to.have.length(2); 103 | expect(combinedResult.errors[0]).to.be.instanceof(ParserError); 104 | expect(combinedResult.errors[1]).to.be.instanceof(ParserError); 105 | }); 106 | 107 | }); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /packages/hekla-core/src/Analyzer.js: -------------------------------------------------------------------------------- 1 | const { 2 | SyncHook, 3 | AsyncSeriesHook 4 | } = require('tapable'); 5 | 6 | const Module = require('./Module'); 7 | const WorkQueue = require('./WorkQueue'); 8 | const { 9 | analysisStarted, 10 | analysisSuccessful, 11 | } = require('./StatusMessage'); 12 | const { 13 | getProjectFiles 14 | } = require('./utils/fs-utils'); 15 | const { 16 | parseAST, 17 | parseHTML, 18 | ASTWrapper, 19 | DOMWrapper 20 | } = require('./utils/ast-utils'); 21 | 22 | const DEFAULT_WORKER_COUNT = 5; 23 | 24 | module.exports = class Analyzer { 25 | constructor() { 26 | this.config = null; 27 | this.fs = null; 28 | this.modules = []; 29 | this.workQueue = null; 30 | this.hooks = { 31 | moduleRawSource: new SyncHook(['module', 'source']), 32 | moduleSyntaxTreeJS: new SyncHook(['module', 'ast']), 33 | moduleSyntaxTreeHTML: new SyncHook(['module', 'dom']), 34 | statusUpdate: new SyncHook(['message']), 35 | reporter: new AsyncSeriesHook(['analyzer', 'analysis']) 36 | }; 37 | } 38 | 39 | applyConfig(config) { 40 | this.config = config; 41 | 42 | if (config.plugins) { 43 | for (let plugin of config.plugins) { 44 | plugin.apply(this); 45 | } 46 | } 47 | } 48 | 49 | setInputFileSystem(fs) { 50 | this.fs = fs; 51 | } 52 | 53 | getAnalysis() { 54 | const analysis = { 55 | modules: this.modules.map(module => module.serialize()) 56 | }; 57 | return analysis; 58 | } 59 | 60 | async run() { 61 | this.startWorkers(DEFAULT_WORKER_COUNT); 62 | 63 | const files = await getProjectFiles(this.config.rootPath, { 64 | ignorePatterns: this.config.exclude || [] 65 | }); 66 | 67 | for (let file of files) { 68 | const fileModule = this.createModule(file); 69 | this.queueProcessModule(fileModule); 70 | } 71 | 72 | await this.waitForWorkers(); 73 | } 74 | 75 | startWorkers(workerCount) { 76 | this.workQueue = new WorkQueue(this); 77 | this.workQueue.onStatusUpdate(message => this.sendStatusUpdate(message)); 78 | 79 | this.workQueue.start(workerCount); 80 | this.sendStatusUpdate(analysisStarted(DEFAULT_WORKER_COUNT)); 81 | } 82 | 83 | async waitForWorkers() { 84 | await this.workQueue.waitForFinish(); 85 | this.sendStatusUpdate(analysisSuccessful()); 86 | } 87 | 88 | createModule(resource) { 89 | return new Module(resource, this.config.rootPath); 90 | } 91 | 92 | queueProcessModule(module) { 93 | this.workQueue.enqueue(module); 94 | } 95 | 96 | processModule(module) { 97 | const resource = module.getResource(); 98 | return readFile(this.fs, resource) 99 | .then(contents => { 100 | this.processModuleSource(module, contents); 101 | return this.processModuleSyntaxTree(module, contents); 102 | }) 103 | .then(() => { 104 | this.modules.push(module); 105 | }) 106 | .catch(err => { 107 | module.setError(err); 108 | this.modules.push(module); 109 | throw err; 110 | }); 111 | } 112 | 113 | processModuleSyntaxTree(module, contents) { 114 | const resource = module.getResource(); 115 | if (resource.match(/\.[jt]sx?$/)) { 116 | return parseAST(contents) 117 | .then(ast => { 118 | const astWrapper = new ASTWrapper(ast); 119 | this.hooks.moduleSyntaxTreeJS.call(module, astWrapper); 120 | }); 121 | } else if (resource.match(/\.html$/)) { 122 | return parseHTML(contents) 123 | .then(dom => { 124 | const domWrapper = new DOMWrapper(dom); 125 | this.hooks.moduleSyntaxTreeHTML.call(module, domWrapper); 126 | }); 127 | } else { 128 | // This file type doesn't support parsing its AST. 129 | return Promise.resolve(); 130 | } 131 | } 132 | 133 | processModuleSource(module, source) { 134 | this.hooks.moduleRawSource.call(module, source); 135 | } 136 | 137 | sendStatusUpdate(message) { 138 | this.hooks.statusUpdate.call(message); 139 | } 140 | 141 | processReporters(analysis) { 142 | return this.hooks.reporter.promise(this, analysis); 143 | } 144 | } 145 | 146 | function readFile(fs, filename) { 147 | return new Promise((resolve, reject) => { 148 | fs.readFile(filename, (err, buffer) => { 149 | if (err) { 150 | reject(err); 151 | } else { 152 | const contents = buffer.toString('utf-8'); 153 | resolve(contents); 154 | } 155 | }); 156 | }); 157 | } 158 | -------------------------------------------------------------------------------- /packages/hekla-core/src/Analyzer.spec.js: -------------------------------------------------------------------------------- 1 | const Analyzer = require('./Analyzer'); 2 | const ASTWrapper = require('./utils/ast-utils/ASTWrapper'); 3 | 4 | class MockFS { 5 | /** 6 | * Example: 7 | * 8 | * ``` 9 | const mockFS = new MockFS({ 10 | '/path/to/project/hello.js': `console.log('hello world');`, 11 | '/path/to/project/fun.js': `console.log('this is fun');` 12 | }); 13 | * ``` 14 | * 15 | * @param {object} files 16 | */ 17 | constructor(files) { 18 | this.files = files; 19 | } 20 | 21 | readFile(filename, callback) { 22 | if (this.files[filename]) { 23 | const buffer = Buffer.from(this.files[filename], 'utf8'); 24 | callback(null, buffer); 25 | } else { 26 | callback(new Error('File does not exist')); 27 | } 28 | } 29 | } 30 | 31 | describe('Analyzer', () => { 32 | 33 | describe('hooks', () => { 34 | 35 | it('should be available', () => { 36 | const analyzer = new Analyzer(); 37 | expect(analyzer.hooks).to.have.property('moduleRawSource'); 38 | expect(analyzer.hooks).to.have.property('moduleSyntaxTreeJS'); 39 | expect(analyzer.hooks).to.have.property('moduleSyntaxTreeHTML'); 40 | expect(analyzer.hooks).to.have.property('reporter'); 41 | }); 42 | 43 | }); 44 | 45 | describe('applyConfig', () => { 46 | 47 | it('should set the rootPath', () => { 48 | const analyzer = new Analyzer(); 49 | analyzer.applyConfig({ 50 | rootPath: '/path/to/project' 51 | }); 52 | expect(analyzer.config.rootPath).to.equal('/path/to/project'); 53 | }); 54 | 55 | it('should apply plugins', () => { 56 | let applied = false; 57 | let analyzerReference = null; 58 | const spyPlugin = { 59 | apply(analyzer) { 60 | analyzerReference = analyzer; 61 | applied = true; 62 | } 63 | }; 64 | const analyzer = new Analyzer(); 65 | analyzer.applyConfig({ 66 | rootPath: '/path/to/project', 67 | plugins: [ 68 | spyPlugin 69 | ] 70 | }); 71 | expect(applied).to.be.true; 72 | expect(analyzerReference).to.equal(analyzer); 73 | }); 74 | 75 | }); 76 | 77 | describe('processModule', () => { 78 | 79 | it('should call hooks for a JS file', async () => { 80 | const source = `console.log('hello world');`; 81 | const fs = new MockFS({ 82 | '/path/to/project/hello.js': source 83 | }); 84 | 85 | let receivedModule = null; 86 | let receivedSource = ''; 87 | let receivedAST = null; 88 | const spyPlugin = { 89 | apply(analyzer) { 90 | analyzer.hooks.moduleRawSource.tap('SpyPlugin', (module, source) => { 91 | receivedModule = module; 92 | receivedSource = source; 93 | }); 94 | analyzer.hooks.moduleSyntaxTreeJS.tap('SpyPlugin', (module, ast) => { 95 | receivedAST = ast; 96 | }); 97 | } 98 | }; 99 | 100 | const analyzer = new Analyzer(); 101 | analyzer.setInputFileSystem(fs); 102 | analyzer.applyConfig({ 103 | rootPath: '/path/to/project', 104 | plugins: [ 105 | spyPlugin 106 | ] 107 | }); 108 | const module = analyzer.createModule('/path/to/project/hello.js'); 109 | await analyzer.processModule(module); 110 | expect(receivedModule).to.equal(module); 111 | expect(receivedSource).to.equal(source); 112 | expect(receivedAST).to.be.an.instanceof(ASTWrapper); 113 | }); 114 | 115 | }); 116 | 117 | describe('processReporters', () => { 118 | 119 | it('should call hooks for reporters', async () => { 120 | const fs = new MockFS({}); 121 | 122 | let receivedAnalyzer = null; 123 | let receivedAnalysis = null; 124 | const spyPlugin = { 125 | apply(analyzer) { 126 | analyzer.hooks.reporter.tap('SpyPlugin', (analyzer, analysis) => { 127 | receivedAnalyzer = analyzer; 128 | receivedAnalysis = analysis; 129 | }); 130 | } 131 | }; 132 | 133 | const analyzer = new Analyzer(); 134 | analyzer.setInputFileSystem(fs); 135 | analyzer.applyConfig({ 136 | rootPath: '/path/to/project', 137 | plugins: [ 138 | spyPlugin 139 | ] 140 | }); 141 | 142 | const analysis = analyzer.getAnalysis(); 143 | await analyzer.processReporters(analysis); 144 | 145 | expect(receivedAnalyzer).to.equal(analyzer); 146 | expect(receivedAnalysis).to.equal(analysis); 147 | }); 148 | 149 | }); 150 | 151 | }); 152 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const astUtils = require('./index'); 5 | 6 | function parse(filename) { 7 | const filePath = path.resolve(__dirname, './test-examples/', filename); 8 | const contents = loadContents(filePath); 9 | return astUtils.parseAST(contents, filePath); 10 | } 11 | 12 | // const x = 5; 13 | const AST_VARIABLE_ASSIGNMENT = { 14 | type: 'VariableDeclaration', 15 | declarations: [ 16 | { 17 | type: 'VariableDeclarator', 18 | id: { 19 | type: 'Identifier', 20 | name: 'x' 21 | }, 22 | init: { 23 | type: 'NumericLiteral', 24 | value: 5 25 | } 26 | } 27 | ], 28 | kind: 'const' 29 | }; 30 | 31 | // function add(x, y) { 32 | // return x + y; 33 | // } 34 | const AST_FUNCTION_DECLARATION = { 35 | type: 'FunctionDeclaration', 36 | id: { 37 | type: 'Identifier', 38 | name: 'add' 39 | }, 40 | generator: false, 41 | expression: false, 42 | async: false, 43 | params: [ 44 | { 45 | type: 'Identifier', 46 | name: 'x' 47 | }, 48 | { 49 | type: 'Identifier', 50 | name: 'y' 51 | } 52 | ], 53 | body: { 54 | type: 'BlockStatement', 55 | body: [ 56 | { 57 | type: 'ReturnStatement', 58 | argument: { 59 | type: 'BinaryExpression', 60 | left: { 61 | type: 'Identifier', 62 | name: 'x' 63 | }, 64 | operator: '+', 65 | right: { 66 | type: 'Identifier', 67 | name: 'y' 68 | } 69 | } 70 | } 71 | ], 72 | directives: [] 73 | } 74 | }; 75 | 76 | describe('astUtils', () => { 77 | 78 | describe('parseHTML', () => { 79 | 80 | it('should parse a simple HTML string', async () => { 81 | const html = `
Hello world
`; 82 | const dom = await astUtils.parseHTML(html); 83 | 84 | expect(dom).to.be.an('array'); 85 | expect(dom).to.have.lengthOf(1); 86 | 87 | const div = dom[0]; 88 | expect(div.type).to.equal('tag'); 89 | expect(div.name).to.equal('div'); 90 | expect(div.children).to.have.lengthOf(2); 91 | 92 | const [text, bold] = div.children; 93 | 94 | expect(text.type).to.equal('text'); 95 | expect(text.data).to.equal('Hello '); 96 | 97 | expect(bold.type).to.equal('tag'); 98 | expect(bold.name).to.equal('b'); 99 | expect(bold.children).to.have.lengthOf(1); 100 | 101 | const world = bold.children[0]; 102 | expect(world.type).to.equal('text'); 103 | expect(world.data).to.equal('world'); 104 | }); 105 | 106 | it('should parse HTML with attributes', async () => { 107 | const html = `
Special
`; 108 | const dom = await astUtils.parseHTML(html); 109 | 110 | expect(dom).to.be.an('array'); 111 | expect(dom).to.have.lengthOf(1); 112 | 113 | const div = dom[0]; 114 | expect(div.type).to.equal('tag'); 115 | expect(div.name).to.equal('div'); 116 | expect(div.children).to.have.lengthOf(1); 117 | 118 | const { attribs } = div; 119 | expect(attribs['class']).to.equal('special'); 120 | expect(attribs['data-special']).to.equal(''); 121 | 122 | const text = div.children[0]; 123 | expect(text.type).to.equal('text'); 124 | expect(text.data).to.equal('Special'); 125 | }); 126 | 127 | }); 128 | 129 | describe('looksLike', () => { 130 | const { looksLike } = astUtils; 131 | 132 | it('should work on partial AST node definitions', () => { 133 | expect(looksLike(AST_VARIABLE_ASSIGNMENT, { 134 | type: 'VariableDeclaration', 135 | kind: 'const' 136 | })).to.be.true; 137 | expect(looksLike(AST_VARIABLE_ASSIGNMENT.declarations[0], { 138 | type: 'VariableDeclarator', 139 | id: {}, 140 | init: {} 141 | })).to.be.true; 142 | expect(looksLike(AST_VARIABLE_ASSIGNMENT.declarations[0].id, { 143 | type: 'Identifier', 144 | name: 'x' 145 | })).to.be.true; 146 | expect(looksLike(AST_VARIABLE_ASSIGNMENT.declarations[0].id, { 147 | name: 'truck' 148 | })).to.be.false; 149 | }); 150 | }); 151 | 152 | describe('getImportInfo', () => { 153 | 154 | it('should get CommonJS imports', (done) => { 155 | parse('imports.js') 156 | .then(ast => { 157 | expect(astUtils.getImportInfo(ast)).to.deep.equal([ 158 | { 159 | type: 'ES6', 160 | value: 'somethingGreat' 161 | }, 162 | { 163 | type: 'CommonJS', 164 | value: 'thingOne' 165 | }, 166 | { 167 | type: 'CommonJS', 168 | value: 'thingTwo' 169 | } 170 | ]); 171 | done(); 172 | }) 173 | .catch(err => done(err)); 174 | }); 175 | 176 | }); 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/dependency-graph/index.spec.js: -------------------------------------------------------------------------------- 1 | const DependencyGraph = require('./index'); 2 | 3 | describe('DependencyGraph', () => { 4 | 5 | function makeSimpleGraph() { 6 | const graph = new DependencyGraph(); 7 | graph.addNode(1, { name: 'One' }); 8 | graph.addNode(2, { name: 'Two' }); 9 | graph.addNode(3, { name: 'Three' }); 10 | graph.addLink(1, 2); 11 | graph.addLink(1, 3); 12 | graph.addLink(2, 3); 13 | return graph; 14 | } 15 | 16 | // NODES --------------------------------------------------------------------- 17 | 18 | describe('addNode', () => { 19 | 20 | it('should store nodes', () => { 21 | const graph = makeSimpleGraph(); 22 | 23 | expect(graph.countNodes()).to.equal(3); 24 | expect(graph.hasNode(1)).to.be.true; 25 | expect(graph.hasNode(2)).to.be.true; 26 | expect(graph.hasNode(3)).to.be.true; 27 | 28 | expect(graph.getNode(1)).to.deep.equal({ name: 'One' }); 29 | expect(graph.getNode(2)).to.deep.equal({ name: 'Two' }); 30 | expect(graph.getNode(3)).to.deep.equal({ name: 'Three' }); 31 | 32 | expect(graph.hasNode(4)).to.be.false; 33 | expect(graph.getNode(4)).to.be.undefined; 34 | }); 35 | 36 | it('should not allow adding the same node twice'); 37 | 38 | }); 39 | 40 | describe('removeNode', () => { 41 | it('should remove nodes', () => { 42 | const graph = makeSimpleGraph(); 43 | graph.removeNode(3); 44 | expect(graph.hasNode(3)).to.be.false; 45 | expect(graph.getNode(3)).to.be.undefined; 46 | }); 47 | it('should clean up links to and from the removed node', () => { 48 | const graph = makeSimpleGraph(); 49 | graph.removeNode(3); 50 | expect(graph.countLinks()).to.equal(1); 51 | expect(graph.getLinksFrom(1)).to.deep.equal([ 52 | { source: 1, target: 2 } 53 | ]); 54 | expect(graph.getLinksFrom(2)).to.deep.equal([]); 55 | expect(graph.getLinksTo(3)).to.deep.equal([]); 56 | }); 57 | }); 58 | 59 | // LINKS --------------------------------------------------------------------- 60 | 61 | describe('addLink', () => { 62 | it('should store links', () => { 63 | const graph = makeSimpleGraph(); 64 | const serialized = graph.serialize(); 65 | 66 | expect(graph.countNodes()).to.equal(3); 67 | expect(graph.countLinks()).to.equal(3); 68 | expect(serialized.links).to.have.length(3); 69 | expect(serialized.links).to.deep.include.members([ 70 | { source: 1, target: 2 }, 71 | { source: 1, target: 3 }, 72 | { source: 2, target: 3 } 73 | ]); 74 | }); 75 | it('should not allow adding the same link twice', () => { 76 | const graph = new DependencyGraph(); 77 | graph.addNode(1, { name: 'One' }); 78 | graph.addNode(2, { name: 'Two' }); 79 | graph.addLink(1, 2); 80 | expect(graph.countLinks()).to.equal(1); 81 | expect(() => graph.addLink(1, 2)).to.throw(); 82 | }); 83 | }); 84 | 85 | describe('getLink', () => { 86 | it('should return a link that was added', () => { 87 | const graph = new DependencyGraph(); 88 | graph.addNode(1, { name: 'One' }); 89 | graph.addNode(2, { name: 'Two' }); 90 | graph.addLink(1, 2); 91 | expect(graph.getLink(1, 2)).to.deep.equal({ 92 | source: 1, 93 | target: 2 94 | }); 95 | expect(graph.getLink(1, 3)).to.be.null; 96 | expect(graph.getLink(3, 2)).to.be.null; 97 | expect(graph.getLink(4, 5)).to.be.null; 98 | }); 99 | }); 100 | 101 | describe('getLinksFrom', () => { 102 | it('should get the correct links from a node', () => { 103 | const graph = makeSimpleGraph(); 104 | expect(graph.getLinksFrom(1)).to.deep.equal([ 105 | { source: 1, target: 2 }, 106 | { source: 1, target: 3 } 107 | ]); 108 | }); 109 | it('should order by targetId ascending', () => { 110 | const graph = new DependencyGraph(); 111 | graph.addNode(1, { name: 'One' }); 112 | graph.addNode(2, { name: 'Two' }); 113 | graph.addNode(3, { name: 'Three' }); 114 | graph.addLink(1, 3); // adding out of order 115 | graph.addLink(1, 2); 116 | graph.addLink(2, 3); 117 | expect(graph.getLinksFrom(1)).to.deep.equal([ 118 | { source: 1, target: 2 }, 119 | { source: 1, target: 3 } 120 | ]); 121 | }); 122 | it('should work with no results', () => { 123 | const graph = makeSimpleGraph(); 124 | expect(graph.getLinksFrom(6)).to.deep.equal([]); 125 | }); 126 | }); 127 | 128 | describe('getLinksTo', () => { 129 | it('should get the correct links to a node', () => { 130 | const graph = makeSimpleGraph(); 131 | expect(graph.getLinksTo(3)).to.deep.equal([ 132 | { source: 1, target: 3 }, 133 | { source: 2, target: 3 } 134 | ]); 135 | }); 136 | it('should order by sourceId ascending', () => { 137 | const graph = new DependencyGraph(); 138 | graph.addNode(1, { name: 'One' }); 139 | graph.addNode(2, { name: 'Two' }); 140 | graph.addNode(3, { name: 'Three' }); 141 | graph.addLink(1, 2); 142 | graph.addLink(2, 3); // adding out of order 143 | graph.addLink(1, 3); 144 | expect(graph.getLinksFrom(1)).to.deep.equal([ 145 | { source: 1, target: 2 }, 146 | { source: 1, target: 3 } 147 | ]); 148 | }); 149 | it('should work with no results', () => { 150 | const graph = makeSimpleGraph(); 151 | expect(graph.getLinksTo(6)).to.deep.equal([]); 152 | }); 153 | }); 154 | 155 | // OUTPUTS ------------------------------------------------------------------- 156 | 157 | describe('createSubgraph', () => { 158 | 159 | }); 160 | 161 | describe('calculateLevels', () => { 162 | 163 | }); 164 | 165 | }); 166 | -------------------------------------------------------------------------------- /packages/hekla-cli/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/lf/h2m6qtfd0psf0x88065n4x5r0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | setupFilesAfterEnv: [ 121 | "/test/helpers/setup.js" 122 | ], 123 | 124 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 125 | // snapshotSerializers: [], 126 | 127 | // The test environment that will be used for testing 128 | testEnvironment: "node", 129 | 130 | // Options that will be passed to the testEnvironment 131 | // testEnvironmentOptions: {}, 132 | 133 | // Adds a location field to test results 134 | // testLocationInResults: false, 135 | 136 | // The glob patterns Jest uses to detect test files 137 | testMatch: [ 138 | "/test/cases/**/testcase.js" 139 | ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /packages/hekla-webpack-plugin/jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/var/folders/lf/h2m6qtfd0psf0x88065n4x5r0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | setupFilesAfterEnv: [ 121 | "/test/helpers/setup.js" 122 | ], 123 | 124 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 125 | // snapshotSerializers: [], 126 | 127 | // The test environment that will be used for testing 128 | testEnvironment: "node", 129 | 130 | // Options that will be passed to the testEnvironment 131 | // testEnvironmentOptions: {}, 132 | 133 | // Adds a location field to test results 134 | // testLocationInResults: false, 135 | 136 | // The glob patterns Jest uses to detect test files 137 | testMatch: [ 138 | "/test/cases/**/testcase.js" 139 | ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "/node_modules/" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "http://localhost", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "/node_modules/" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | // watchman: true, 180 | }; 181 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const htmlparser = require('htmlparser2'); 5 | 6 | const astUtils = require('../../../utils/ast-utils'); 7 | const ngUtils = require('../../../utils/ng-utils'); 8 | const BaseParser = require('../BaseParser'); 9 | const ParserResult = require('../../../utils/parser-result'); 10 | 11 | module.exports = class AngularDirectiveParser extends BaseParser { 12 | constructor() { 13 | super(); 14 | } 15 | 16 | extractComponents(module) { 17 | return astUtils.parseAST(module.contents, module.path) 18 | .then(ast => analyzeAllInFile(ast, module)) 19 | .catch(err => { 20 | console.error(`Error parsing AST for ${module.path}: `, err.stack); 21 | return ParserResult.create([], err, module); 22 | }); 23 | } 24 | }; 25 | 26 | function analyzeAllInFile(ast, module) { 27 | return Promise.resolve(getDirectiveCallNodes(ast)) 28 | .then(directiveCallNodes => { 29 | return Promise.all(directiveCallNodes.map(node => getComponentDetails(node, module, ast))); 30 | }) 31 | .then(components => ParserResult.create(components)) 32 | .catch(err => ParserResult.create([], err, module)); 33 | } 34 | 35 | function getDirectiveCallNodes(ast) { 36 | return astUtils 37 | .getNodesByType(ast.program, 'CallExpression') 38 | .filter(node => (astUtils.getDeepProperty(node, 'callee.property.name') === 'directive')); 39 | } 40 | 41 | function getDirectiveDefinitionObject(directiveCallNode, ast) { 42 | 43 | const definitionFunction = ngUtils.getDefinitionFunction(directiveCallNode, ast); 44 | 45 | const returnStatement = definitionFunction.body.body 46 | .reduce((previous, statement) => (statement.type === 'ReturnStatement' ? statement : previous), null); 47 | 48 | if (returnStatement.argument.type === 'ObjectExpression') { 49 | const definitionObject = returnStatement.argument; 50 | return definitionObject; 51 | } else if (returnStatement.argument.type === 'FunctionExpression') { 52 | // This is just a link function, not a whole definition object. 53 | return null; 54 | } else { 55 | throw new Error('Cannot find directive definition object'); 56 | } 57 | } 58 | 59 | function getDefinitionProperty(propertyName, directiveDefinitionObject) { 60 | if (!directiveDefinitionObject) { 61 | return null; 62 | } 63 | 64 | return directiveDefinitionObject.properties 65 | .reduce((prev, property) => (property.key.type === 'Identifier' && property.key.name === propertyName ? property.value : prev), null); 66 | } 67 | 68 | function getComponentDetails(node, module, ast) { 69 | const directiveDefinitionObject = getDirectiveDefinitionObject(node, ast); 70 | const filePath = module.path; 71 | 72 | let templateInfo = {}; 73 | return getTemplateInfo(directiveDefinitionObject, filePath, ast) 74 | .then(info => { 75 | templateInfo = info; 76 | return info; 77 | }) 78 | .then(info => getDependencies(info)) 79 | .then(dependencies => { 80 | const componentName = ngUtils.getName(node); 81 | const kebabCaseName = ngUtils.getKebabCaseName(componentName); 82 | return { 83 | name: componentName, 84 | altNames: (kebabCaseName.indexOf('-') === -1 ? [] : [kebabCaseName]), 85 | type: 'angular-directive', 86 | path: filePath, 87 | templatePath: (templateInfo ? templateInfo.path : null), 88 | properties: { 89 | angularModule: ngUtils.getModuleName(node), 90 | scope: getScope(directiveDefinitionObject) 91 | }, 92 | dependencies: dependencies 93 | }; 94 | }); 95 | } 96 | 97 | function getScope(directiveDefinitionObject) { 98 | const scopeNode = getDefinitionProperty('scope', directiveDefinitionObject); 99 | 100 | if (!scopeNode) { 101 | return null; 102 | } else if (scopeNode.properties) { 103 | return scopeNode.properties 104 | .map(property => getScopeParam(property)); 105 | } else if (scopeNode.type === 'BooleanLiteral') { 106 | return scopeNode.value; 107 | } else { 108 | throw new error('Parser bug while parsing scope value'); 109 | } 110 | } 111 | 112 | function getScopeParam(propertyObject) { 113 | if (propertyObject.key.type === 'Identifier') return propertyObject.key.name; 114 | else if (propertyObject.key.type === 'StringLiteral') return propertyObject.key.value; 115 | else throw new Error('invalid node type for scope parameter: ' + propertyObject.key.type); 116 | } 117 | 118 | function getTemplateInfo(directiveDefinitionObject, filePath, ast) { 119 | const templateProperty = getDefinitionProperty('template', directiveDefinitionObject); 120 | const templateUrlProperty = getDefinitionProperty('templateUrl', directiveDefinitionObject); 121 | 122 | return ngUtils.getTemplateInfo(templateProperty, templateUrlProperty, filePath, ast); 123 | } 124 | 125 | function getDependencies(templateInfo) { 126 | if (!templateInfo) { 127 | return Promise.resolve([]); 128 | } 129 | 130 | // console.log('getting dependencies:', templateInfo); 131 | return Promise.resolve() 132 | .then(() => getDependenciesLegacy(templateInfo.contents, templateInfo.path)) 133 | .catch(err => { 134 | console.log('dependency error!', err); 135 | if (err.code === 'ENOENT') { 136 | // console.log(' template does not exist, bro'); 137 | return []; 138 | } else { 139 | return Promise.reject(err); 140 | } 141 | }); 142 | } 143 | 144 | function getDependenciesLegacy(fileContents, filePath) { 145 | const dependencies = []; 146 | 147 | var parser = new htmlparser.Parser({ 148 | 149 | onopentag: (name, attrs) => { 150 | if (isSpecialTag(name)) { 151 | dependencies.push(name); 152 | } 153 | 154 | // TODO: check special attributes 155 | // for (let key in attrs) { 156 | // } 157 | } 158 | 159 | }, {decodeEntities: true}); 160 | parser.write(fileContents); 161 | parser.end(); 162 | 163 | return cleanDependencyList(dependencies); 164 | } 165 | 166 | function isSpecialTag(tagName) { 167 | return (tagName.indexOf('-') !== -1); 168 | } 169 | 170 | function getDmComponentName(attrValue, filePath) { 171 | let name = attrValue; 172 | 173 | if (attrValue.indexOf('./') === 0) { 174 | const pathPieces = filePath.split('/'); 175 | const folderName = pathPieces[pathPieces.length - 2]; 176 | name = name.replace('./', folderName + '/'); 177 | } 178 | 179 | return name; 180 | } 181 | 182 | function cleanDependencyList(dependencies) { 183 | const uniqueDependencies = uniq(dependencies); 184 | return uniqueDependencies 185 | .sort(); 186 | } 187 | 188 | // Courtesy of http://stackoverflow.com/a/9229821/2418448 189 | function uniq(a) { 190 | var seen = {}; 191 | var out = []; 192 | var len = a.length; 193 | var j = 0; 194 | for(var i = 0; i < len; i++) { 195 | var item = a[i]; 196 | if(seen[item] !== 1) { 197 | seen[item] = 1; 198 | out[j++] = item; 199 | } 200 | } 201 | return out; 202 | } 203 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ast-utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const babelParser = require('@babel/parser'); 5 | const HtmlParser = require('htmlparser2').Parser; 6 | const DomHandler = require('domhandler'); 7 | const walk = require('tree-walk'); 8 | 9 | const ASTWrapper = require('./ASTWrapper'); 10 | const DOMWrapper = require('./DOMWrapper'); 11 | 12 | module.exports = { 13 | parseAST, 14 | parseHTML, 15 | ASTWrapper, 16 | DOMWrapper, 17 | getNodesByType, 18 | filterNodes, 19 | isPromiseCall, 20 | isASTNode, 21 | looksLike, 22 | getDeepProperty, 23 | reduceCallName, 24 | reduceMemberName, 25 | getFunctionDeclarationsByName, 26 | getVariableDeclarationsByName, 27 | resolveRequirePath, 28 | getImportInfo 29 | }; 30 | 31 | // TODO: make sync and async versions 32 | function parseAST(fileContents, filePath) { 33 | try { 34 | const ast = babelParser.parse(fileContents, { 35 | sourceType: 'module', 36 | plugins: [ 37 | 'objectRestSpread', 38 | 'dynamicImport', 39 | 'classProperties', 40 | 'jsx', 41 | 'typescript', 42 | 'optionalCatchBinding', 43 | ] 44 | }); 45 | return Promise.resolve(ast); 46 | } catch (err) { 47 | return Promise.reject(err); 48 | } 49 | } 50 | 51 | function parseHTML(source) { 52 | return new Promise((resolve, reject) => { 53 | try { 54 | const handler = new DomHandler((err, dom) => { 55 | if (err) { 56 | return reject(err); 57 | } else { 58 | return resolve(dom); 59 | } 60 | }); 61 | const options = {}; 62 | const parser = new HtmlParser(handler, options); 63 | parser.write(source); 64 | parser.end(); 65 | } catch (err) { 66 | reject(err); 67 | } 68 | }); 69 | } 70 | 71 | function getNodesByType(tree, nodeType) { 72 | return walk.filter(tree, walk.preorder, (value, key, parent) => isASTNode(value, key, parent) && value.type === nodeType); 73 | } 74 | 75 | function filterNodes(tree, filterFunction) { 76 | return walk.filter(tree, walk.preorder, (value, key, parent) => isASTNode(value, key, parent) && filterFunction(value, key, parent)); 77 | } 78 | 79 | function isPromiseCall(callExpNode) { 80 | return looksLike(callExpNode, { 81 | callee: { 82 | property: { 83 | type: 'Identifier', 84 | name: 'then' 85 | } 86 | } 87 | }); 88 | } 89 | 90 | function isASTNode(value, key, parent) { 91 | return (value && typeof value === 'object' && value.type); 92 | } 93 | 94 | function looksLike(a, b) { 95 | return ( 96 | a && 97 | b && 98 | Object.keys(b).every(bKey => { 99 | const bVal = b[bKey] 100 | const aVal = a[bKey] 101 | if (typeof bVal === 'function') { 102 | return bVal(aVal) 103 | } 104 | return isPrimitive(bVal) ? bVal === aVal : looksLike(aVal, bVal) 105 | }) 106 | ) 107 | } 108 | 109 | function isPrimitive(val) { 110 | return val == null || /^[sbn]/.test(typeof val) 111 | } 112 | 113 | /** 114 | * Gets a deep property from a node, if it exists. 115 | * If the property does not exist, undefined is returned 116 | * 117 | * Example: 118 | * getDeepProperty(myCallNode, 'callee.property.name') 119 | */ 120 | function getDeepProperty(node, deepProperty) { 121 | const pieces = deepProperty.split('.'); 122 | let currentChildNode = node; 123 | for (let i = 0; i < pieces.length; i++) { 124 | if (!currentChildNode.hasOwnProperty(pieces[i])) { 125 | // A property along the chain is missing 126 | return undefined; 127 | } 128 | 129 | if (i === pieces.length - 1) { 130 | // This is the deep property 131 | return currentChildNode[pieces[i]]; 132 | } 133 | 134 | // Go to the next node down in the chain 135 | currentChildNode = currentChildNode[pieces[i]]; 136 | } 137 | 138 | return undefined; 139 | } 140 | 141 | function reduceCallName(callExpressionNode) { 142 | const callee = callExpressionNode.callee; 143 | if (callee.type === 'Identifier') { 144 | return callee.name; 145 | } else if (callee.type === 'MemberExpression') { 146 | return reduceMemberName(callee); 147 | } else if (callee.type === 'CallExpression') { 148 | return '(CallExpression)'; 149 | } else { 150 | throw new Error(`callee type not handled: ${callee.type}`); 151 | } 152 | } 153 | 154 | function reduceMemberName(memberExpressionNode) { 155 | let objectName; 156 | if (memberExpressionNode.object.type === 'MemberExpression') { 157 | objectName = reduceMemberName(memberExpressionNode.object); 158 | } else if (memberExpressionNode.object.type === 'Identifier') { 159 | objectName = memberExpressionNode.object.name; 160 | } else if (memberExpressionNode.object.type === 'CallExpression') { 161 | objectName = '(CallExpressionInMember)'; 162 | } else if (memberExpressionNode.object.type === 'ArrayExpression') { 163 | objectName = '(ArrayExpressionInMember)'; 164 | } else { 165 | throw new Error(`node type not handled: ${memberExpressionNode.object.type}`); 166 | } 167 | 168 | let propertyName = memberExpressionNode.property.name; 169 | 170 | return `${objectName}.${propertyName}`; 171 | } 172 | 173 | function getFunctionDeclarationsByName(node, identifierName) { 174 | return getNodesByType(node, 'FunctionDeclaration') 175 | .filter(node => getDeepProperty(node, 'id.name') === identifierName); 176 | } 177 | 178 | function getVariableDeclarationsByName(node, identifierName) { 179 | const results = []; 180 | 181 | const declarationNodes = getNodesByType(node, 'VariableDeclaration'); 182 | declarationNodes.forEach(node => { 183 | node.declarations.forEach(declarator => { 184 | if (declarator.id.type === 'Identifier' && declarator.id.name === identifierName) { 185 | results.push(declarator.init); 186 | } 187 | }); 188 | }); 189 | 190 | return results; 191 | } 192 | 193 | function resolveRequirePath(requiredPathString, modulePath) { 194 | let cleanRequiredPath = requiredPathString; 195 | if (requiredPathString.indexOf('!') !== -1) { 196 | const pieces = requiredPathString.split('!'); 197 | cleanRequiredPath = pieces[pieces.length - 1]; 198 | } 199 | return path.resolve(modulePath, '..', cleanRequiredPath); 200 | } 201 | 202 | function getImportInfo(ast) { 203 | const es6Imports = getES6Imports(ast); 204 | const commonjsImports = getCommonJSImports(ast); 205 | return mergeArrays([es6Imports, commonjsImports]); 206 | } 207 | 208 | function getES6Imports(ast) { 209 | return getNodesByType(ast, 'ImportDeclaration') 210 | .filter(node => node.source.type === 'StringLiteral') 211 | .map(node => ({ 212 | type: 'ES6', 213 | value: node.source.value 214 | })); 215 | } 216 | 217 | function getCommonJSImports(ast) { 218 | return getNodesByType(ast, 'CallExpression') 219 | .filter(node => node.callee.type === 'Identifier' && node.callee.name === 'require') 220 | .map(node => ({ 221 | type: 'CommonJS', 222 | value: node.arguments[0].value 223 | })); 224 | } 225 | 226 | // TODO: copied from ParserResult... refactor to somewhere common 227 | function mergeArrays(arrays) { 228 | return [].concat.apply([], arrays); 229 | } 230 | 231 | function simplifyFunctionDeclarationNode(node) { 232 | return { 233 | id: node.id.name 234 | }; 235 | } 236 | 237 | function simplifyVariableDeclarationNode(node) { 238 | return { 239 | type: node.type, 240 | kind: node.kind, 241 | id: node.declarations[0].id.name 242 | }; 243 | } 244 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/ng-utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dashify = require('dashify'); 4 | 5 | const astUtils = require('../ast-utils'); 6 | const fsUtils = require('../fs-utils'); 7 | 8 | const { looksLike } = astUtils; 9 | 10 | module.exports = { 11 | getName, 12 | getKebabCaseName, 13 | getModuleName, 14 | getDefinitionFunction, 15 | getTemplateInfo 16 | }; 17 | 18 | // Info from definition call 19 | // e.g. angular.module('app').directive('myThing', () => {}); 20 | 21 | function getName(callNode) { 22 | const nameNode = callNode.arguments[0]; 23 | if (nameNode.type === 'StringLiteral') { 24 | return nameNode.value; 25 | } else { 26 | throw new Error('Name type not handled: ', nameNode.type); 27 | } 28 | } 29 | 30 | function getKebabCaseName(camelCaseName) { 31 | return dashify(camelCaseName); 32 | } 33 | 34 | function getModuleName(callNode) { 35 | if ( 36 | looksLike(callNode, { 37 | callee: { 38 | type: 'MemberExpression', 39 | object: { 40 | type: 'CallExpression', 41 | callee: { 42 | property: { 43 | name: 'module' 44 | } 45 | } 46 | } 47 | } 48 | }) 49 | ) { 50 | const ngModuleCallNode = callNode.callee.object; 51 | return ngModuleCallNode.arguments[0].value; 52 | } else { 53 | return null; 54 | } 55 | } 56 | 57 | // Injection functions 58 | 59 | function getDefinitionFunction(callNode, ast) { 60 | const secondArg = callNode.arguments[1]; 61 | 62 | let possibleDefinitionFunction; 63 | 64 | if (secondArg.type === 'Identifier') { 65 | // Function is saved in a variable - resolve it 66 | const variableName = secondArg.name; 67 | const declarations = [].concat.apply([], [ 68 | astUtils.getVariableDeclarationsByName(ast, variableName), 69 | astUtils.getFunctionDeclarationsByName(ast, variableName) 70 | ]); 71 | 72 | if (declarations.length === 1) { 73 | possibleDefinitionFunction = declarations[0]; 74 | } else { 75 | throw new Error(`Cannot resolve definition function from variable: ${variableName}`); 76 | } 77 | } else { 78 | possibleDefinitionFunction = secondArg; 79 | } 80 | 81 | // We may have found the function, but we may have found a DI array. 82 | // Reduce it. 83 | let definitionFunction; 84 | 85 | if ( 86 | looksLike(possibleDefinitionFunction, { 87 | type: 'ArrayExpression', 88 | elements: (arr) => looksLike(arr[arr.length - 1], { 89 | type: 'FunctionExpression' 90 | }) 91 | }) 92 | ) { 93 | // Angular DI syntax 94 | const elements = possibleDefinitionFunction.elements; 95 | return elements[elements.length - 1]; 96 | } else if (['FunctionExpression', 'FunctionDeclaration'].includes(possibleDefinitionFunction.type)) { 97 | // Standard function 98 | return possibleDefinitionFunction; 99 | } else { 100 | throw new Error('Cannot find definition function'); 101 | } 102 | } 103 | 104 | // Loading templates 105 | 106 | function getTemplateInfo(templateProperty, templateUrlProperty, filePath, ast) { 107 | if ( 108 | looksLike(templateProperty, { 109 | type: 'CallExpression', 110 | callee: { 111 | name: 'require' 112 | } 113 | }) 114 | ) { 115 | // Required template file 116 | const requiredPath = templateProperty.arguments[0].value; 117 | const templatePath = astUtils.resolveRequirePath(requiredPath, filePath); 118 | return getExternalTemplateContents(templatePath) 119 | .then(contents => { 120 | return { 121 | type: 'external', 122 | path: templatePath, 123 | contents 124 | }; 125 | }); 126 | } else if (templateProperty && templateProperty.type === 'StringLiteral') { 127 | // Simple inline template 128 | return Promise.resolve({ 129 | type: 'inline', 130 | path: null, 131 | contents: templateProperty.value 132 | }); 133 | } else if (templateProperty) { 134 | // Complex inline template 135 | return Promise.resolve({ 136 | type: 'inline', 137 | path: null, 138 | contents: reduceComplexTemplate(templateProperty, ast) 139 | }); 140 | } else if (templateUrlProperty && templateUrlProperty.type === 'StringLiteral') { 141 | // templateUrl string 142 | return resolveTemplatePath(templateUrlProperty.value, filePath) 143 | .then(templatePath => { 144 | if (templatePath) { 145 | return getExternalTemplateContents(templatePath) 146 | .then(contents => ({ 147 | type: 'external', 148 | path: templatePath, 149 | contents 150 | })); 151 | } else { 152 | return Promise.resolve({ 153 | type: 'external', 154 | path: null, 155 | contents: '' 156 | }); 157 | } 158 | }); 159 | } else if (templateUrlProperty) { 160 | // Weird templateUrl - can't parse 161 | return Promise.resolve({ 162 | type: 'external', 163 | path: null, 164 | contents: '' 165 | }); 166 | } else { 167 | // No template or templateUrl 168 | return Promise.resolve(null); 169 | } 170 | } 171 | 172 | function reduceComplexTemplate(templateProperty, ast) { 173 | if (templateProperty.type === 'BinaryExpression') { 174 | return reduceConcatenatedTemplate(templateProperty); 175 | } else if (templateProperty.type === 'CallExpression') { 176 | return reduceArrayJoinTemplate(templateProperty); 177 | } else if (templateProperty.type === 'Identifier') { 178 | return reduceTemplateFromVariable(templateProperty.name, ast); 179 | } else { 180 | throw new Error('invalid complex template'); 181 | } 182 | } 183 | 184 | function reduceTemplateFromVariable(variableName, ast) { 185 | const declarations = astUtils.getVariableDeclarationsByName(ast, variableName); 186 | if (declarations.length === 1) { 187 | return reduceComplexTemplate(declarations[0], ast); 188 | } else { 189 | throw new Error(`Cannot resolve template from variable: ${variableName}`); 190 | } 191 | } 192 | 193 | function reduceConcatenatedTemplate(binaryExpression) { 194 | const leftSide = (binaryExpression.left.type === 'BinaryExpression' ? reduceConcatenatedTemplate(binaryExpression.left) : binaryExpression.left.value); 195 | const rightSide = binaryExpression.right.value; 196 | return leftSide + rightSide; 197 | } 198 | 199 | function reduceArrayJoinTemplate(joinCallExpression) { 200 | const templateArray = joinCallExpression.callee.object; 201 | const pieces = templateArray.elements.map(element => element.value); 202 | const delimiter = joinCallExpression.arguments[0].value; 203 | return pieces.join(delimiter); 204 | } 205 | 206 | /** 207 | * Attempt to convert a templateUrl into an absolute path 208 | */ 209 | function resolveTemplatePath(templateUrl, componentPath) { 210 | const componentFilename = getFileName(componentPath); 211 | const templateFilename = getFileName(templateUrl); 212 | 213 | if (getDirectoryName(templateUrl) === getDirectoryName(componentPath)) { 214 | const templatePath = componentPath.replace(componentFilename, templateFilename); 215 | return fsUtils.getFileExists(templatePath) 216 | .then(fileExists => { 217 | if (fileExists) { 218 | // The template is in the same directory as the component. 219 | return templatePath; 220 | } else { 221 | // TODO: the file is missing so the naive replace didn't work... 222 | return null; 223 | } 224 | }); 225 | } else { 226 | // The template is in a different directory... 227 | // TODO: do tricky stuff here to reconcile the path and url. 228 | return Promise.resolve(null); 229 | } 230 | } 231 | 232 | function getDirectoryName(path) { 233 | const pieces = path.split('/'); 234 | return pieces[pieces.length - 2]; 235 | } 236 | 237 | function getFileName(path) { 238 | const pieces = path.split('/'); 239 | return pieces[pieces.length - 1]; 240 | } 241 | 242 | function getExternalTemplateContents(templatePath) { 243 | return fsUtils.getFileContents(templatePath) 244 | } 245 | -------------------------------------------------------------------------------- /packages/hekla-viewer/src/ContentPane/DependencyChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ComponentContextMenu from './ComponentContextMenu'; 3 | import ComponentBox from './ComponentBox'; 4 | import ComponentDependencyArrow from './ComponentDependencyArrow'; 5 | const DependencyGraph = require('hekla-core').DependencyGraph; 6 | import { 7 | OFFSET_X, 8 | OFFSET_Y, 9 | GRID_X, 10 | GRID_Y 11 | } from './constants'; 12 | 13 | import './DependencyChart.css'; 14 | 15 | 16 | export default class DependencyChart extends Component { 17 | constructor(props) { 18 | super(props); 19 | const { components, componentDependencies } = props; 20 | this._onSelect = this._onSelect.bind(this); 21 | this._onContextMenu = this._onContextMenu.bind(this); 22 | this._onClickBackground = this._onClickBackground.bind(this); 23 | this._onUpdateGraph = this._onUpdateGraph.bind(this); 24 | this._onUpdateSelectedComponent = this._onUpdateSelectedComponent.bind(this); 25 | this._onExpandDependants = this._onExpandDependants.bind(this); 26 | this._onExpandDependencies = this._onExpandDependencies.bind(this); 27 | this._onRemove = this._onRemove.bind(this); 28 | this._addComponent = this._addComponent.bind(this); 29 | this._nextY = 0; // TODO: make this smarter 30 | this.state = { 31 | projectGraph: createProjectGraph(components, componentDependencies), 32 | subgraph: new DependencyGraph(), 33 | contextMenuComponent: null, 34 | contextMenuCoordinates: { x: 0, y: 0 } 35 | }; 36 | } 37 | 38 | componentWillReceiveProps(newProps) { 39 | if (newProps.components !== this.props.components) { 40 | this._onUpdateGraph(newProps.components, newProps.componentDependencies); 41 | } 42 | 43 | if (newProps.selectedComponent !== this.props.selectedComponent) { 44 | this._onUpdateSelectedComponent(newProps.selectedComponent); 45 | } 46 | } 47 | 48 | _onSelect(component) { 49 | this.props.onSelect(component); 50 | } 51 | 52 | _onContextMenu(event, component) { 53 | const containerOffsets = this.refs.container.getBoundingClientRect(); 54 | const x = (event.clientX - containerOffsets.left); 55 | const y = (event.clientY - containerOffsets.top); 56 | event.preventDefault(); 57 | this.setState({ 58 | contextMenuComponent: component, 59 | contextMenuCoordinates: { x, y } 60 | }); 61 | } 62 | 63 | _onUpdateGraph(components, componentDependencies) { 64 | this.setState({ 65 | projectGraph: createProjectGraph(components, componentDependencies), 66 | subgraph: new DependencyGraph() 67 | }); 68 | } 69 | 70 | _onUpdateSelectedComponent(component) { 71 | const { subgraph } = this.state; 72 | if (!subgraph.hasNode(component.id)) { 73 | // Pick coordinates for the box 74 | // TODO: decide based on the projectGraph 75 | const boxX = 0; 76 | const boxY = this._nextY; 77 | this._nextY++; 78 | this._addComponent(component, boxX, boxY); 79 | 80 | // Re-render 81 | console.log('new subgraph:', subgraph); 82 | this.forceUpdate(); 83 | } 84 | } 85 | 86 | _onExpandDependants(component) { 87 | const { subgraph, projectGraph } = this.state; 88 | const dependantIds = projectGraph.getLinksTo(component.id) 89 | .map(link => link.source) 90 | .filter(id => !subgraph.hasNode(id)); 91 | // TODO: decide coordinates based on the projectGraph 92 | let boxX = 0; 93 | const boxY = this._nextY; 94 | dependantIds.forEach(id => { 95 | const dependantComponent = projectGraph.getNode(id); 96 | this._addComponent(dependantComponent, boxX, boxY); 97 | boxX++; 98 | }); 99 | 100 | // Re-render 101 | console.log('new subgraph:', subgraph); 102 | this.forceUpdate(); 103 | } 104 | 105 | _onExpandDependencies(component) { 106 | const { subgraph, projectGraph } = this.state; 107 | const dependencyIds = projectGraph.getLinksFrom(component.id) 108 | .map(link => link.target) 109 | .filter(id => !subgraph.hasNode(id)); 110 | // TODO: decide coordinates based on the projectGraph 111 | let boxX = 0; 112 | const boxY = this._nextY; 113 | dependencyIds.forEach(id => { 114 | const dependencyComponent = projectGraph.getNode(id); 115 | this._addComponent(dependencyComponent, boxX, boxY); 116 | boxX++; 117 | }); 118 | 119 | // Re-render 120 | console.log('new subgraph:', subgraph); 121 | this.forceUpdate(); 122 | } 123 | 124 | _onRemove(component) { 125 | this._removeComponent(component); 126 | } 127 | 128 | _addComponent(component, boxX, boxY) { 129 | const { subgraph } = this.state; 130 | // Add the node 131 | const node = makeNode(component, boxX, boxY); 132 | subgraph.addNode(component.id, node); 133 | 134 | // Add links to and from other nodes in the current subgraph 135 | this.state.projectGraph.getLinksFrom(component.id) 136 | .filter(link => subgraph.hasNode(link.target)) 137 | .forEach(link => subgraph.addLink(link.source, link.target)); 138 | this.state.projectGraph.getLinksTo(component.id) 139 | .filter(link => subgraph.hasNode(link.source)) 140 | .forEach(link => subgraph.addLink(link.source, link.target)); 141 | } 142 | 143 | _removeComponent(component) { 144 | const { subgraph } = this.state; 145 | subgraph.removeNode(component.id); 146 | this.forceUpdate(); 147 | } 148 | 149 | _onClickBackground() { 150 | // Close the context menu 151 | this.setState({ 152 | contextMenuComponent: null 153 | }); 154 | } 155 | 156 | render() { 157 | const { selectedComponent } = this.props; 158 | const { 159 | projectGraph, 160 | subgraph, 161 | contextMenuComponent, 162 | contextMenuCoordinates 163 | } = this.state; 164 | const subgraphNodes = subgraph.nodes.map(node => node.value); 165 | const subgraphLinks = subgraph.links; 166 | return ( 167 |
168 | {!contextMenuComponent ? null : ( 169 | this._onExpandDependants(contextMenuComponent)} 176 | onExpandDependencies={() => this._onExpandDependencies(contextMenuComponent)} 177 | onRemove={() => this._onRemove(contextMenuComponent)} 178 | /> 179 | )} 180 | 181 | 186 | 187 | 188 | {subgraphNodes.map(node => ( 189 | 198 | ))} 199 | {subgraphLinks.map(link => { 200 | const { source: sourceId, target: targetId } = link; 201 | const fromNode = subgraph.getNode(sourceId); 202 | const toNode = subgraph.getNode(targetId); 203 | const key = `${sourceId}-${targetId}`; 204 | return ( 205 | 210 | ); 211 | })} 212 | 213 |
214 | ); 215 | } 216 | }; 217 | 218 | /** 219 | * Rebuild the dependency graph that the analyzer created. 220 | * We will pull items out of this to create the visible subgraph. 221 | */ 222 | function createProjectGraph(components, componentDependencies) { 223 | const projectGraph = new DependencyGraph(); 224 | components.forEach(component => projectGraph.addNode(component.id, component)); 225 | componentDependencies.forEach(link => projectGraph.addLink(link.source, link.target)); 226 | return projectGraph; 227 | } 228 | 229 | function makeNode(component, gridX, gridY) { 230 | return { 231 | x: (OFFSET_X + (gridX * GRID_X)), 232 | y: (OFFSET_Y + (gridY * GRID_Y)), 233 | component 234 | }; 235 | } 236 | -------------------------------------------------------------------------------- /packages/hekla-core/src/utils/dependency-graph/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class DependencyGraph { 4 | constructor() { 5 | this.nodes = []; 6 | this.nodeMap = new Map(); 7 | this.links = []; 8 | this.fromLinks = new Map(); // Map> 9 | this.toLinks = new Map(); // Map> 10 | } 11 | 12 | // NODES --------------------------------------------------------------------- 13 | 14 | addNode(id, value) { 15 | this.nodes.push(createNode(id, value)); 16 | this.nodeMap.set(id, value); 17 | } 18 | 19 | hasNode(id) { 20 | return this.nodeMap.has(id); 21 | } 22 | 23 | getNode(id) { 24 | return this.nodeMap.get(id); 25 | } 26 | 27 | countNodes() { 28 | return this.nodes.length; 29 | } 30 | 31 | removeNode(id) { 32 | // Delete the node itself 33 | this.nodeMap.delete(id); 34 | this.nodes = this.nodes.filter(n => n.id !== id); 35 | 36 | // Delete the links to and from the node too 37 | this.links = this.links.filter(l => l.source !== id && l.target !== id); 38 | this.fromLinks.delete(id); 39 | this.fromLinks.forEach((fromSet) => { 40 | fromSet.forEach((link) => { 41 | if (link.target === id) { 42 | fromSet.delete(link); 43 | } 44 | }); 45 | }); 46 | this.toLinks.delete(id); 47 | this.toLinks.forEach((toSet) => { 48 | toSet.forEach((link) => { 49 | if (link.source === id) { 50 | toSet.delete(link); 51 | } 52 | }); 53 | }); 54 | } 55 | 56 | // LINKS --------------------------------------------------------------------- 57 | 58 | addLink(sourceId, targetId) { 59 | const foundDuplicate = this.hasLink(sourceId, targetId); 60 | if (foundDuplicate) { 61 | throw new Error('Cannot add the same link twice'); 62 | } 63 | 64 | // Store the link in a basic array. 65 | const link = createLink(sourceId, targetId); 66 | this.links.push(link); 67 | 68 | // Store the link in the fromLinks Map. 69 | if (!this.fromLinks.has(sourceId)) { 70 | this.fromLinks.set(sourceId, new Set()); 71 | } 72 | this.fromLinks.get(sourceId).add(link); 73 | 74 | // Store the link in the toLinks Map. 75 | if (!this.toLinks.has(targetId)) { 76 | this.toLinks.set(targetId, new Set()); 77 | } 78 | this.toLinks.get(targetId).add(link); 79 | } 80 | 81 | hasLink(sourceId, targetId) { 82 | const link = this.getLink(sourceId, targetId); 83 | return (!!link); 84 | } 85 | 86 | getLink(sourceId, targetId) { 87 | const toSet = this.fromLinks.get(sourceId); 88 | if (!toSet) return null; 89 | 90 | for (let link of toSet) { 91 | if (link.target === targetId) { 92 | return link; 93 | } 94 | } 95 | return null; 96 | } 97 | 98 | getLinksFrom(sourceId) { 99 | const toSet = this.fromLinks.get(sourceId); 100 | if (!toSet) return []; 101 | 102 | const results = []; 103 | for (let link of toSet) { 104 | results.push(link); 105 | } 106 | return results.sort(sortByTargetAsc); 107 | } 108 | 109 | getLinksTo(targetId) { 110 | const fromSet = this.toLinks.get(targetId); 111 | if (!fromSet) return []; 112 | 113 | const results = []; 114 | for (let link of fromSet) { 115 | results.push(link); 116 | } 117 | return results.sort(sortBySourceAsc); 118 | } 119 | 120 | countLinks() { 121 | return this.links.length; 122 | } 123 | 124 | // trimLinks() { 125 | // this.links = this.links.filter(link => this.nodeMap.has(link.source)); 126 | // } 127 | 128 | // OUTPUTS ------------------------------------------------------------------- 129 | 130 | serialize() { 131 | return { 132 | nodes: this.nodes, 133 | links: this.links 134 | }; 135 | } 136 | 137 | toString() { 138 | return `[DependencyGraph: ${this.nodes.length} nodes, ${this.links.length} links]`; 139 | } 140 | 141 | // createSubgraph(topLevelModuleId) { 142 | // const subgraph = new DependencyGraph(); 143 | // 144 | // // Keep a stack of modules that need to be visited 145 | // const moduleIdStack = []; 146 | // moduleIdStack.push(topLevelModuleId); 147 | // while (moduleIdStack.length > 0) { 148 | // const moduleId = moduleIdStack.pop(); 149 | // if (subgraph.hasModule(moduleId)) { 150 | // // We've already added this module 151 | // break; 152 | // } 153 | // 154 | // const module = this.moduleMap.get(moduleId); 155 | // // Add the module to the subgraph 156 | // subgraph.addModule(module); 157 | // 158 | // // Add the children of this module to the stack 159 | // const childIds = this.links 160 | // .filter(link => link.source === moduleId) 161 | // .map(link => link.target); 162 | // childIds 163 | // .filter(childId => !subgraph.hasModule(childId)) 164 | // .forEach(childId => { 165 | // moduleIdStack.push(childId) 166 | // }); 167 | // } 168 | // 169 | // subgraph.trimLinks(); 170 | // 171 | // return subgraph; 172 | // } 173 | 174 | /** 175 | * Modified Dijkstra's algorithm 176 | * https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm 177 | */ 178 | // calculateLevels(topLevelModuleId) { 179 | // // Create a set of unvisited nodes 180 | // const unvisitedModuleIds = new Set(); 181 | // this.modules.forEach(module => { 182 | // if (module.id === topLevelModuleId) { 183 | // module.level = 0; 184 | // } else { 185 | // module.level = Infinity; 186 | // unvisitedModuleIds.add(module.id); 187 | // } 188 | // }); 189 | // 190 | // let currentModule = this.moduleMap.get(topLevelModuleId); 191 | // let currentLevel = currentModule.level = 0; 192 | // while (currentModule && unvisitedModuleIds.size > 0) { 193 | // // console.log('visiting node: ', currentModule.name); 194 | // currentLevel = currentModule.level; 195 | // 196 | // // Mark tentative distances for the children of each node 197 | // const unvisitedChildren = getChildModules(this.moduleMap, this.links, currentModule) 198 | // .filter(childModule => unvisitedModuleIds.has(childModule.id)); 199 | // unvisitedChildren.forEach(child => { 200 | // // console.log('\tupdating child', child.id); 201 | // child.level = currentModule.level + 1; 202 | // }); 203 | // 204 | // unvisitedModuleIds.delete(currentModule.id); 205 | // 206 | // // Mark the actual level of the node 207 | // // TODO: Implement! 208 | // // const maxParentLevel = getParentModules(this.moduleMap, this.links, currentModule) 209 | // // .reduce((maxLevel, module) => (module.level > maxLevel ? module.level : maxLevel), 210 | // // currentModule.level - 1); 211 | // // currentModule.level = maxParentLevel + 1; 212 | // 213 | // // TODO: is this edge case real? 214 | // // if (currentModule.id === topLevelModuleId) { 215 | // // currentModule.level = 0; 216 | // // } 217 | // 218 | // // Pick the next currentModule 219 | // // console.log('unvisited modules:', this.modules 220 | // // .filter(module => unvisitedModuleIds.has(module.id)) 221 | // // .map(Utils.simplifyModule)); 222 | // 223 | // const nextModule = this.modules 224 | // .filter(module => unvisitedModuleIds.has(module.id)) 225 | // .reduce((lowestLevelModule, module) => { 226 | // if (!lowestLevelModule || module.level < lowestLevelModule.level) { 227 | // return module; 228 | // } else { 229 | // return lowestLevelModule; 230 | // } 231 | // }, null); 232 | // currentModule = nextModule; 233 | // 234 | // if (!currentModule) { 235 | // break; 236 | // } 237 | // console.log('next module: ', Utils.simplifyModule(currentModule)); 238 | // } 239 | // } 240 | 241 | }; // End class DependencyGraph 242 | 243 | // Utility functions 244 | 245 | function createNode(id, value) { 246 | return { 247 | id, 248 | value 249 | }; 250 | } 251 | 252 | function createLink(sourceId, targetId) { 253 | return { 254 | source: sourceId, 255 | target: targetId 256 | }; 257 | } 258 | 259 | function sortBySourceAsc(linkA, linkB) { 260 | return (linkA.source - linkB.source); 261 | } 262 | 263 | function sortByTargetAsc(linkA, linkB) { 264 | return (linkA.target - linkB.target); 265 | } 266 | 267 | // function getChildModules(moduleMap, links, currentModule) { 268 | // const childLinks = links.filter(link => link.source === currentModule.id); 269 | // const children = childLinks.map(childLink => moduleMap.get(childLink.target)); 270 | // return children; 271 | // } 272 | // 273 | // function getParentModules(moduleMap, links, currentModule) { 274 | // const parentLinks = links.filter(link => link.target === currentModule.id); 275 | // const parents = parentLinks.map(parentLink => moduleMap.get(parentLink.source)); 276 | // return parents; 277 | // } 278 | -------------------------------------------------------------------------------- /packages/hekla-core/src/legacy/parsers/AngularDirectiveParser/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const AngularDirectiveParser = require('./index'); 5 | 6 | function makeModule(filename) { 7 | const modulePath = path.resolve(__dirname, './test-examples/', filename); 8 | return { 9 | path: modulePath, 10 | contents: loadContents(modulePath) 11 | }; 12 | } 13 | 14 | function getComponentFromResults(results) { 15 | if (results.errors.length > 0) { 16 | console.error('Error while extracting components:'); 17 | console.error(results.errors[0].stack); 18 | } 19 | expect(results.components).have.length(1); 20 | expect(results.errors).to.have.length(0); 21 | return results.components[0]; 22 | } 23 | 24 | describe('AngularDirectiveParser', () => { 25 | let examples = {}; 26 | 27 | before(() => { 28 | examples = { 29 | basic: makeModule('basic.js'), 30 | injected: makeModule('injected.js'), 31 | noScope: makeModule('no-scope.js'), 32 | minimal: makeModule('minimal.js'), 33 | twoDirectives: makeModule('two-directives.js'), 34 | splitDefinitionInjected: makeModule('split-definition-injected.js'), 35 | inline: makeModule('inline.js'), 36 | inlineConcat: makeModule('inline-concat.js'), 37 | inlineArrayJoin: makeModule('inline-array-join.js'), 38 | inlineVariable: makeModule('inline-variable.js'), 39 | templateUrl: makeModule('template-url.js'), 40 | templateUrlFn: makeModule('template-url-function.js'), 41 | webpackLoaderTemplate: makeModule('webpack-loader-template.js') 42 | }; 43 | }); 44 | 45 | describe('extractComponents', () => { 46 | 47 | it('should extract a basic directive', (done) => { 48 | const parser = new AngularDirectiveParser(); 49 | parser.extractComponents(examples.basic) 50 | .then(getComponentFromResults) 51 | .then(component => { 52 | expect(component.name).to.equal('myPetShop'); 53 | expect(component.altNames).to.deep.equal(['my-pet-shop']); 54 | expect(component.type).to.equal('angular-directive'); 55 | expect(component.properties).to.deep.equal({ 56 | angularModule: 'app', 57 | scope: [ 58 | 'title', 59 | 'dogs', 60 | 'cats', 61 | 'quoted' 62 | ] 63 | }); 64 | done(); 65 | }) 66 | .catch(err => done(err)); 67 | }); 68 | 69 | it('should extract a directive with Angular DI syntax', (done) => { 70 | const parser = new AngularDirectiveParser(); 71 | parser.extractComponents(examples.injected) 72 | .then(getComponentFromResults) 73 | .then(component => { 74 | expect(component.name).to.equal('myPetShop'); 75 | done(); 76 | }) 77 | .catch(err => done(err)); 78 | }); 79 | 80 | it('should extract a directive with no scope set', (done) => { 81 | const parser = new AngularDirectiveParser(); 82 | parser.extractComponents(examples.noScope) 83 | .then(getComponentFromResults) 84 | .then(component => { 85 | expect(component.properties).to.deep.equal({ 86 | angularModule: 'app', 87 | scope: null 88 | }); 89 | done(); 90 | }) 91 | .catch(err => done(err)); 92 | }); 93 | 94 | it('should extract a directive with only a link function', (done) => { 95 | const parser = new AngularDirectiveParser(); 96 | parser.extractComponents(examples.minimal) 97 | .then(getComponentFromResults) 98 | .then(component => { 99 | expect(component.name).to.equal('myMouseHover'); 100 | expect(component.properties).to.deep.equal({ 101 | angularModule: 'app', 102 | scope: null 103 | }); 104 | done(); 105 | }) 106 | .catch(err => done(err)); 107 | }); 108 | 109 | it('should extract two directives from a single file', (done) => { 110 | const parser = new AngularDirectiveParser(); 111 | parser.extractComponents(examples.twoDirectives) 112 | .then(results => { 113 | if (results.errors.length > 0) { 114 | console.error('Error while extracting components:'); 115 | console.error(results.errors[0].stack); 116 | } 117 | expect(results.components).have.length(2); 118 | expect(results.errors).to.have.length(0); 119 | return results.components; 120 | }) 121 | .then(components => { 122 | const firstComponent = components[0]; 123 | expect(firstComponent.name).to.equal('myPetMenu'); 124 | expect(firstComponent.properties).to.deep.equal({ 125 | angularModule: 'app', 126 | scope: [ 127 | 'pets' 128 | ] 129 | }); 130 | expect(firstComponent.dependencies).to.deep.equal([ 131 | 'my-pet-menu-item' 132 | ]); 133 | const secondComponent = components[1]; 134 | expect(secondComponent.name).to.equal('myPetMenuItem'); 135 | expect(secondComponent.properties).to.deep.equal({ 136 | angularModule: 'app', 137 | scope: [ 138 | 'name' 139 | ] 140 | }); 141 | done(); 142 | }) 143 | .catch(err => done(err)); 144 | }); 145 | 146 | it('should extract a directive with required template file', (done) => { 147 | const parser = new AngularDirectiveParser(); 148 | parser.extractComponents(examples.basic) 149 | .then(getComponentFromResults) 150 | .then(component => { 151 | expect(component.dependencies).to.deep.equal([ 152 | 'my-pet-title' 153 | ]); 154 | done(); 155 | }) 156 | .catch(err => done(err)); 157 | }); 158 | 159 | it('should extract a directive with a simple inline template', (done) => { 160 | const parser = new AngularDirectiveParser(); 161 | parser.extractComponents(examples.inline) 162 | .then(getComponentFromResults) 163 | .then(component => { 164 | expect(component.name).to.equal('myPetShop'); 165 | expect(component.type).to.equal('angular-directive'); 166 | expect(component.dependencies).to.deep.equal([ 167 | 'my-pet-title' 168 | ]); 169 | done(); 170 | }) 171 | .catch(err => done(err)); 172 | }); 173 | 174 | it('should extract a directive with a contatenated inline template', (done) => { 175 | const parser = new AngularDirectiveParser(); 176 | parser.extractComponents(examples.inlineConcat) 177 | .then(getComponentFromResults) 178 | .then(component => { 179 | expect(component.dependencies).to.deep.equal([ 180 | 'my-pet-title' 181 | ]); 182 | done(); 183 | }) 184 | .catch(err => done(err)); 185 | }); 186 | 187 | it('should extract a directive with an inline template as a joined array', (done) => { 188 | const parser = new AngularDirectiveParser(); 189 | parser.extractComponents(examples.inlineArrayJoin) 190 | .then(getComponentFromResults) 191 | .then(component => { 192 | expect(component.dependencies).to.deep.equal([ 193 | 'my-pet-title' 194 | ]); 195 | done(); 196 | }) 197 | .catch(err => done(err)); 198 | }); 199 | 200 | it('should extract a directive with an inline template from a variable', (done) => { 201 | const parser = new AngularDirectiveParser(); 202 | parser.extractComponents(examples.inlineVariable) 203 | .then(getComponentFromResults) 204 | .then(component => { 205 | expect(component.dependencies).to.deep.equal([ 206 | 'my-pet-title' 207 | ]); 208 | done(); 209 | }) 210 | .catch(err => done(err)); 211 | }); 212 | 213 | it('should extract a directive with a templateUrl', (done) => { 214 | const parser = new AngularDirectiveParser(); 215 | parser.extractComponents(examples.templateUrl) 216 | .then(getComponentFromResults) 217 | .then(component => { 218 | expect(component.dependencies).to.deep.equal([ 219 | 'my-pet-title' 220 | ]); 221 | done(); 222 | }) 223 | .catch(err => done(err)); 224 | }); 225 | 226 | it('should extract a directive with a required template including a webpack loader prefix', (done) => { 227 | const parser = new AngularDirectiveParser(); 228 | parser.extractComponents(examples.webpackLoaderTemplate) 229 | .then(getComponentFromResults) 230 | .then(component => { 231 | expect(component.name).to.equal('myPetShop'); 232 | expect(component.dependencies).to.deep.equal([ 233 | 'my-pet-title' 234 | ]); 235 | done(); 236 | }) 237 | .catch(err => done(err)); 238 | }); 239 | 240 | it('should extract a directive with a definition function saved in a variable', (done) => { 241 | const parser = new AngularDirectiveParser(); 242 | parser.extractComponents(examples.splitDefinitionInjected) 243 | .then(getComponentFromResults) 244 | .then(component => { 245 | expect(component.name).to.equal('myPetShop'); 246 | done(); 247 | }) 248 | .catch(err => done(err)); 249 | }); 250 | 251 | it('should gracefully fail with an evaluated templateUrl', (done) => { 252 | const parser = new AngularDirectiveParser(); 253 | parser.extractComponents(examples.templateUrlFn) 254 | .then(getComponentFromResults) 255 | .then(component => { 256 | expect(component.dependencies).to.deep.equal([]); 257 | done(); 258 | }) 259 | .catch(err => done(err)); 260 | }); 261 | 262 | }); 263 | 264 | }); 265 | -------------------------------------------------------------------------------- /packages/hekla-core/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hekla-core", 3 | "version": "0.3.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/parser": { 8 | "version": "7.1.0", 9 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.0.tgz", 10 | "integrity": "sha512-SmjnXCuPAlai75AFtzv+KCBcJ3sDDWbIn+WytKw1k+wAtEy6phqI2RqKh/zAnw53i1NR8su3Ep/UoqaKcimuLg==" 11 | }, 12 | "@babel/types": { 13 | "version": "7.1.2", 14 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.2.tgz", 15 | "integrity": "sha512-pb1I05sZEKiSlMUV9UReaqsCPUpgbHHHu2n1piRm7JkuBkm6QxcaIzKu6FMnMtCbih/cEYTR+RGYYC96Yk9HAg==", 16 | "requires": { 17 | "esutils": "^2.0.2", 18 | "lodash": "^4.17.10", 19 | "to-fast-properties": "^2.0.0" 20 | } 21 | }, 22 | "ansi-styles": { 23 | "version": "3.2.1", 24 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 25 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 26 | "requires": { 27 | "color-convert": "^1.9.0" 28 | } 29 | }, 30 | "assertion-error": { 31 | "version": "1.1.0", 32 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 33 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 34 | "dev": true 35 | }, 36 | "async": { 37 | "version": "2.6.1", 38 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 39 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 40 | "requires": { 41 | "lodash": "^4.17.10" 42 | } 43 | }, 44 | "balanced-match": { 45 | "version": "1.0.0", 46 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 47 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 48 | }, 49 | "brace-expansion": { 50 | "version": "1.1.11", 51 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 52 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 53 | "requires": { 54 | "balanced-match": "^1.0.0", 55 | "concat-map": "0.0.1" 56 | } 57 | }, 58 | "browser-stdout": { 59 | "version": "1.3.1", 60 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 61 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 62 | "dev": true 63 | }, 64 | "camel-case": { 65 | "version": "3.0.0", 66 | "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", 67 | "integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=", 68 | "requires": { 69 | "no-case": "^2.2.0", 70 | "upper-case": "^1.1.1" 71 | } 72 | }, 73 | "chai": { 74 | "version": "4.1.2", 75 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 76 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 77 | "dev": true, 78 | "requires": { 79 | "assertion-error": "^1.0.1", 80 | "check-error": "^1.0.1", 81 | "deep-eql": "^3.0.0", 82 | "get-func-name": "^2.0.0", 83 | "pathval": "^1.0.0", 84 | "type-detect": "^4.0.0" 85 | } 86 | }, 87 | "chalk": { 88 | "version": "2.4.2", 89 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 90 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 91 | "requires": { 92 | "ansi-styles": "^3.2.1", 93 | "escape-string-regexp": "^1.0.5", 94 | "supports-color": "^5.3.0" 95 | } 96 | }, 97 | "check-error": { 98 | "version": "1.0.2", 99 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 100 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 101 | "dev": true 102 | }, 103 | "color-convert": { 104 | "version": "1.9.3", 105 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 106 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 107 | "requires": { 108 | "color-name": "1.1.3" 109 | } 110 | }, 111 | "color-name": { 112 | "version": "1.1.3", 113 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 114 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 115 | }, 116 | "commander": { 117 | "version": "2.15.1", 118 | "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 119 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 120 | "dev": true 121 | }, 122 | "concat-map": { 123 | "version": "0.0.1", 124 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 125 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 126 | }, 127 | "core-util-is": { 128 | "version": "1.0.2", 129 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 130 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 131 | }, 132 | "dashify": { 133 | "version": "0.2.2", 134 | "resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz", 135 | "integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4=" 136 | }, 137 | "debug": { 138 | "version": "3.1.0", 139 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 140 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 141 | "dev": true, 142 | "requires": { 143 | "ms": "2.0.0" 144 | } 145 | }, 146 | "deep-eql": { 147 | "version": "3.0.1", 148 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 149 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 150 | "dev": true, 151 | "requires": { 152 | "type-detect": "^4.0.0" 153 | } 154 | }, 155 | "diff": { 156 | "version": "3.5.0", 157 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 158 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 159 | "dev": true 160 | }, 161 | "dom-serializer": { 162 | "version": "0.1.0", 163 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", 164 | "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", 165 | "requires": { 166 | "domelementtype": "~1.1.1", 167 | "entities": "~1.1.1" 168 | }, 169 | "dependencies": { 170 | "domelementtype": { 171 | "version": "1.1.3", 172 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", 173 | "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" 174 | } 175 | } 176 | }, 177 | "domelementtype": { 178 | "version": "1.3.0", 179 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", 180 | "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=" 181 | }, 182 | "domhandler": { 183 | "version": "2.4.2", 184 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", 185 | "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", 186 | "requires": { 187 | "domelementtype": "1" 188 | } 189 | }, 190 | "domutils": { 191 | "version": "1.7.0", 192 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", 193 | "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", 194 | "requires": { 195 | "dom-serializer": "0", 196 | "domelementtype": "1" 197 | } 198 | }, 199 | "entities": { 200 | "version": "1.1.1", 201 | "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", 202 | "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" 203 | }, 204 | "escape-string-regexp": { 205 | "version": "1.0.5", 206 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 207 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 208 | }, 209 | "esutils": { 210 | "version": "2.0.2", 211 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 212 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" 213 | }, 214 | "fs.realpath": { 215 | "version": "1.0.0", 216 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 217 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 218 | }, 219 | "get-func-name": { 220 | "version": "2.0.0", 221 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 222 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 223 | "dev": true 224 | }, 225 | "glob": { 226 | "version": "7.1.4", 227 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 228 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 229 | "requires": { 230 | "fs.realpath": "^1.0.0", 231 | "inflight": "^1.0.4", 232 | "inherits": "2", 233 | "minimatch": "^3.0.4", 234 | "once": "^1.3.0", 235 | "path-is-absolute": "^1.0.0" 236 | } 237 | }, 238 | "growl": { 239 | "version": "1.10.5", 240 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 241 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 242 | "dev": true 243 | }, 244 | "has-flag": { 245 | "version": "3.0.0", 246 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 247 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 248 | }, 249 | "he": { 250 | "version": "1.1.1", 251 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 252 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 253 | "dev": true 254 | }, 255 | "htmlparser2": { 256 | "version": "3.9.2", 257 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", 258 | "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", 259 | "requires": { 260 | "domelementtype": "^1.3.0", 261 | "domhandler": "^2.3.0", 262 | "domutils": "^1.5.1", 263 | "entities": "^1.1.1", 264 | "inherits": "^2.0.1", 265 | "readable-stream": "^2.0.2" 266 | } 267 | }, 268 | "inflight": { 269 | "version": "1.0.6", 270 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 271 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 272 | "requires": { 273 | "once": "^1.3.0", 274 | "wrappy": "1" 275 | } 276 | }, 277 | "inherits": { 278 | "version": "2.0.3", 279 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 280 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 281 | }, 282 | "isarray": { 283 | "version": "1.0.0", 284 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 285 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 286 | }, 287 | "lodash": { 288 | "version": "4.17.15", 289 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", 290 | "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" 291 | }, 292 | "lower-case": { 293 | "version": "1.1.4", 294 | "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", 295 | "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=" 296 | }, 297 | "minimatch": { 298 | "version": "3.0.4", 299 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 300 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 301 | "requires": { 302 | "brace-expansion": "^1.1.7" 303 | } 304 | }, 305 | "minimist": { 306 | "version": "0.0.8", 307 | "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 308 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 309 | "dev": true 310 | }, 311 | "mkdirp": { 312 | "version": "0.5.1", 313 | "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 314 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 315 | "dev": true, 316 | "requires": { 317 | "minimist": "0.0.8" 318 | } 319 | }, 320 | "mocha": { 321 | "version": "5.2.0", 322 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 323 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 324 | "dev": true, 325 | "requires": { 326 | "browser-stdout": "1.3.1", 327 | "commander": "2.15.1", 328 | "debug": "3.1.0", 329 | "diff": "3.5.0", 330 | "escape-string-regexp": "1.0.5", 331 | "glob": "7.1.2", 332 | "growl": "1.10.5", 333 | "he": "1.1.1", 334 | "minimatch": "3.0.4", 335 | "mkdirp": "0.5.1", 336 | "supports-color": "5.4.0" 337 | }, 338 | "dependencies": { 339 | "glob": { 340 | "version": "7.1.2", 341 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 342 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 343 | "dev": true, 344 | "requires": { 345 | "fs.realpath": "^1.0.0", 346 | "inflight": "^1.0.4", 347 | "inherits": "2", 348 | "minimatch": "^3.0.4", 349 | "once": "^1.3.0", 350 | "path-is-absolute": "^1.0.0" 351 | } 352 | } 353 | } 354 | }, 355 | "ms": { 356 | "version": "2.0.0", 357 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 358 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 359 | "dev": true 360 | }, 361 | "no-case": { 362 | "version": "2.3.2", 363 | "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", 364 | "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", 365 | "requires": { 366 | "lower-case": "^1.1.1" 367 | } 368 | }, 369 | "once": { 370 | "version": "1.4.0", 371 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 372 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 373 | "requires": { 374 | "wrappy": "1" 375 | } 376 | }, 377 | "path-is-absolute": { 378 | "version": "1.0.1", 379 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 380 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 381 | }, 382 | "pathval": { 383 | "version": "1.1.0", 384 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 385 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 386 | "dev": true 387 | }, 388 | "process-nextick-args": { 389 | "version": "2.0.0", 390 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 391 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 392 | }, 393 | "readable-stream": { 394 | "version": "2.3.6", 395 | "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 396 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 397 | "requires": { 398 | "core-util-is": "~1.0.0", 399 | "inherits": "~2.0.3", 400 | "isarray": "~1.0.0", 401 | "process-nextick-args": "~2.0.0", 402 | "safe-buffer": "~5.1.1", 403 | "string_decoder": "~1.1.1", 404 | "util-deprecate": "~1.0.1" 405 | } 406 | }, 407 | "safe-buffer": { 408 | "version": "5.1.2", 409 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 410 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 411 | }, 412 | "sticky-terminal-display": { 413 | "version": "0.1.0", 414 | "resolved": "https://registry.npmjs.org/sticky-terminal-display/-/sticky-terminal-display-0.1.0.tgz", 415 | "integrity": "sha512-y8+CjknYKj2F8uIls26KJCsqxDF5l8cD4Jx0H2rVkeEJe5AmF5xbaL34lrZdugMBzJJEjX3waRTawP7MDuA73Q==" 416 | }, 417 | "string_decoder": { 418 | "version": "1.1.1", 419 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 420 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 421 | "requires": { 422 | "safe-buffer": "~5.1.0" 423 | } 424 | }, 425 | "supports-color": { 426 | "version": "5.4.0", 427 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 428 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 429 | "requires": { 430 | "has-flag": "^3.0.0" 431 | } 432 | }, 433 | "tapable": { 434 | "version": "1.1.0", 435 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz", 436 | "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==" 437 | }, 438 | "to-fast-properties": { 439 | "version": "2.0.0", 440 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 441 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" 442 | }, 443 | "tree-walk": { 444 | "version": "0.4.0", 445 | "resolved": "https://registry.npmjs.org/tree-walk/-/tree-walk-0.4.0.tgz", 446 | "integrity": "sha1-6s4Rm9+fOjlFJ16Dj5hi5pxfBks=", 447 | "requires": { 448 | "util-extend": "^1.0.1" 449 | } 450 | }, 451 | "type-detect": { 452 | "version": "4.0.8", 453 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 454 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 455 | "dev": true 456 | }, 457 | "upper-case": { 458 | "version": "1.1.3", 459 | "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", 460 | "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=" 461 | }, 462 | "util-deprecate": { 463 | "version": "1.0.2", 464 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 465 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 466 | }, 467 | "util-extend": { 468 | "version": "1.0.3", 469 | "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", 470 | "integrity": "sha1-p8IW0mdUUWljeztu3GypEZ4v+T8=" 471 | }, 472 | "wrappy": { 473 | "version": "1.0.2", 474 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 475 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 476 | } 477 | } 478 | } 479 | --------------------------------------------------------------------------------