├── .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 |
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 | [](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 | [](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 |
18 |
19 |
20 | {JSON.stringify(component, null, 2)}
21 |
22 |
23 |
24 | );
25 | } else {
26 | return (
27 |
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 | [](https://travis-ci.org/andrewjensen/hekla)
17 |
18 | 
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 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------