├── .nvmrc ├── packages ├── recon-config │ ├── src │ │ ├── __tests__ │ │ │ ├── __fixtures__ │ │ │ │ ├── empty │ │ │ │ │ └── .gitignore │ │ │ │ └── all │ │ │ │ │ └── .reconrc │ │ │ ├── index.test.js │ │ │ ├── configFromWebpack.test.js │ │ │ ├── getConfig.test.js │ │ │ └── createConfig.test.js │ │ ├── shared.js │ │ ├── index.js │ │ ├── createConfig.js │ │ ├── getConfig.js │ │ └── configFromWebpack.js │ ├── package.json │ └── README.md ├── recon-engine │ ├── src │ │ ├── engine │ │ │ ├── __tests__ │ │ │ │ ├── __fixtures__ │ │ │ │ │ └── basic-app │ │ │ │ │ │ ├── query.graphql │ │ │ │ │ │ └── src │ │ │ │ │ │ ├── avatar.js │ │ │ │ │ │ ├── button.js │ │ │ │ │ │ ├── list.js │ │ │ │ │ │ ├── user-list.js │ │ │ │ │ │ ├── app.js │ │ │ │ │ │ └── notes.js │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── createEngine.test.js.snap │ │ │ │ └── createEngine.test.js │ │ │ └── createEngine.js │ │ ├── index.js │ │ ├── parse │ │ │ ├── __tests__ │ │ │ │ ├── __fixtures__ │ │ │ │ │ ├── empty │ │ │ │ │ │ └── src.js │ │ │ │ │ ├── re-exports │ │ │ │ │ │ └── src.js │ │ │ │ │ ├── no-components │ │ │ │ │ │ └── src.js │ │ │ │ │ ├── dynamic-components │ │ │ │ │ │ └── src.js │ │ │ │ │ ├── basic-components │ │ │ │ │ │ └── src.js │ │ │ │ │ ├── enhanced-components │ │ │ │ │ │ └── src.js │ │ │ │ │ └── real-world-lystable │ │ │ │ │ │ └── src.js │ │ │ │ └── parseModule.test.js │ │ │ └── parseModule.js │ │ ├── __tests__ │ │ │ └── index.test.js │ │ ├── utils │ │ │ ├── isReactComponent.js │ │ │ └── __tests__ │ │ │ │ └── isReactComponent.test.js │ │ └── query │ │ │ └── createSchema.js │ ├── README.md │ ├── package.json │ └── docs │ │ └── dev-guide.md ├── recon-stats │ ├── src │ │ ├── index.js │ │ ├── __tests__ │ │ │ ├── index.test.js │ │ │ ├── makeStats.test.js │ │ │ └── pullStats.test.js │ │ ├── pullStats.js │ │ ├── query.graphql │ │ └── makeStats.js │ ├── README.md │ └── package.json ├── recon-server │ ├── src │ │ ├── index.js │ │ ├── __tests__ │ │ │ └── index.test.js │ │ └── createServer.js │ ├── README.md │ └── package.json └── recon-cli │ ├── README.md │ ├── package.json │ └── src │ └── index.js ├── .travis.yml ├── .eslintignore ├── .gitignore ├── lerna.json ├── .flowconfig ├── .eslintrc ├── wallaby.js ├── LICENSE.md ├── docs └── dev-guide.md ├── CONTRIBUTING.md ├── package.json ├── flow ├── lodash.js.flow └── babel.js.flow └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v7.7.3 2 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/__fixtures__/empty/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/query.graphql: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/recon-engine/src/index.js: -------------------------------------------------------------------------------- 1 | exports.createEngine = require('./engine/createEngine'); 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | before_script: 5 | - npm run bootstrap 6 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/empty/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // EMPTY 4 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/__fixtures__/all/.reconrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": "**/*.js", 3 | "resolve": {} 4 | } 5 | -------------------------------------------------------------------------------- /packages/recon-stats/src/index.js: -------------------------------------------------------------------------------- 1 | const pullStats = require('./pullStats'); 2 | 3 | exports.pullStats = pullStats; 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/**/*.js 2 | **/__fixtures__/**/*.js 3 | **/dist/**/*.js 4 | **/lib/**/*.js 5 | **/*.js.flow 6 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/re-exports/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export {a as b} from 'src'; 3 | -------------------------------------------------------------------------------- /packages/recon-server/src/index.js: -------------------------------------------------------------------------------- 1 | const createServer = require('./createServer'); 2 | 3 | exports.createServer = createServer; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | dist/ 4 | .idea/ 5 | npm-debug.log 6 | lerna-debug.log 7 | __local__/ 8 | *.__local__.* 9 | .tmp/ 10 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.38", 3 | "version": "0.0.7", 4 | "hoist": true, 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/recon-config/src/shared.js: -------------------------------------------------------------------------------- 1 | /** File name to look for configuration */ 2 | const CONFIG_FILE_NAME = '.reconrc'; 3 | 4 | exports.CONFIG_FILE_NAME = CONFIG_FILE_NAME; 5 | -------------------------------------------------------------------------------- /packages/recon-stats/README.md: -------------------------------------------------------------------------------- 1 | recon-stats 2 | =========== 3 | 4 | Generate stats for your application 5 | 6 | *Part of [Recon: Code Intelligence for React](https://github.com/lystable/recon)* 7 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/react\(-dom\)?/.* 3 | .*/.*\.config\.js 4 | .*/Gulpfile\.js 5 | .*/json5/test/.* 6 | 7 | [include] 8 | 9 | [libs] 10 | flow/ 11 | 12 | [options] 13 | -------------------------------------------------------------------------------- /packages/recon-engine/README.md: -------------------------------------------------------------------------------- 1 | recon-engine 2 | ============ 3 | 4 | Engine runtime for Recon intelligence. 5 | 6 | *Part of [Recon: Code Intelligence for React](https://github.com/lystable/recon)* 7 | -------------------------------------------------------------------------------- /packages/recon-server/README.md: -------------------------------------------------------------------------------- 1 | recon-server 2 | ============ 3 | 4 | GraphQL server in front of a Recon Engine. 5 | 6 | *Part of [Recon: Code Intelligence for React](https://github.com/lystable/recon)* 7 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/avatar.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | 4 | export default function Avatar({src}) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /packages/recon-engine/src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const api = require('../index'); 3 | 4 | it('should provide expected api', () => { 5 | expect(api.createEngine).toBeInstanceOf(Function); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/recon-server/src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const api = require('../index'); 3 | 4 | it('should provide expected api', () => { 5 | expect(api.createServer).toBeInstanceOf(Function); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/recon-stats/src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const api = require('../index'); 3 | 4 | it('should provide expected api', () => { 5 | expect(api.pullStats).toBeInstanceOf(Function); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/recon-stats/src/__tests__/makeStats.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | 3 | // const makeStats = require('../makeStats'); 4 | it('should generate stats from data', () => { 5 | expect({}).toMatchObject({}); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/no-components/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import imported from './imported'; 3 | 4 | export class MyClass {} 5 | 6 | const myValue = 5; 7 | 8 | export default myValue; 9 | -------------------------------------------------------------------------------- /packages/recon-stats/src/__tests__/pullStats.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | // const pullStats = require('../pullStats'); 3 | 4 | it('should generate stats given an engine to query', () => { 5 | expect({}).toMatchObject({}); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/button.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | 4 | export default function Button({theme, children}) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const api = require('../index'); 3 | 4 | it('should provide expected api', () => { 5 | expect(api.getConfig).toBeInstanceOf(Function); 6 | expect(api.createConfig).toBeInstanceOf(Function); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/recon-cli/README.md: -------------------------------------------------------------------------------- 1 | recon-cli 2 | ========= 3 | 4 | Command line interface for wielding the power of Recon. 5 | 6 | *Part of [Recon: Code Intelligence for React](https://github.com/lystable/recon)* 7 | 8 | Once you've run `recon` the world of power should just be a `help` command away! 9 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/dynamic-components/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | 4 | function makeComponent(staticProps) { 5 | return props =>
; 6 | } 7 | 8 | export const Flex = makeComponent({style: {display: 'flex'}}); 9 | -------------------------------------------------------------------------------- /packages/recon-config/src/index.js: -------------------------------------------------------------------------------- 1 | const getConfig = require('./getConfig'); 2 | const createConfig = require('./createConfig'); 3 | const configFromWebpack = require('./configFromWebpack'); 4 | 5 | exports.getConfig = getConfig; 6 | exports.createConfig = createConfig; 7 | exports.configFromWebpack = configFromWebpack; 8 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__snapshots__/createEngine.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`should respond to query as expected: basic-app 1`] = ` 4 | Object { 5 | "errors": Array [ 6 | [GraphQLError: Syntax Error GraphQL request (1:2) Expected Name, found } 7 | 8 | 1: {} 9 | ^ 10 | 2: 11 | ], 12 | ], 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /packages/recon-stats/src/pullStats.js: -------------------------------------------------------------------------------- 1 | const Jetpack = require('fs-jetpack'); 2 | const makeStats = require('./makeStats'); 3 | 4 | /** Given a recon-engine instance pull stats */ 5 | function pullStats(engine) { 6 | const query = Jetpack.cwd(__dirname).read('query.graphql', 'utf8'); 7 | return engine.runQuery(query).then(makeStats); 8 | } 9 | 10 | module.exports = pullStats; 11 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/list.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | 4 | export function ListItem({type, children}) { 5 | return ( 6 |
  • 7 | {children} 8 |
  • 9 | ); 10 | } 11 | 12 | export default function List({children, type}) { 13 | return ( 14 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "flowtype" 4 | ], 5 | "extends": [ 6 | "lystable", 7 | "prettier", 8 | "plugin:flowtype/recommended", 9 | "prettier", 10 | "prettier/flowtype" 11 | ], 12 | "env": { 13 | "node": true 14 | }, 15 | "rules": { 16 | "max-len": [2, 140], 17 | "spaced-comment": 0, 18 | "arrow-body-style": 0, 19 | "no-confusing-arrow": 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/basic-components/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | 4 | export function FunctionalComponent() { 5 | return
    Hello world!
    ; 6 | } 7 | 8 | export const ArrowFunctionalComponent = () =>
    ; 9 | 10 | export default class ClassComponent { 11 | render() { 12 | return
    Hello world! Click Here!
    ; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | testFramework: 'jest', 4 | files: ['packages/**/*', {pattern: '**/*.test.js', ignore: true}], 5 | tests: [ 6 | 'packages/**/src/**/__tests__/**/*.test.js', 7 | {pattern: 'packages/**/node_modules/**/__tests__/**', ignore: true}, 8 | ], 9 | env: { 10 | type: 'node', 11 | runner: 'node', 12 | }, 13 | workers: { 14 | recycle: true, 15 | }, 16 | filesWithNoCoverageCalculated: ['**/node_modules/**'], 17 | debug: true, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2016 Lystable Industries 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/configFromWebpack.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const configFromWebpack = require('../configFromWebpack'); 3 | 4 | it('should return expected configuration', () => { 5 | const cwd = '/root/test'; 6 | const webpackConfig = { 7 | context: '/root/test/src', 8 | resolve: { 9 | roots: ['/root/test/src/core'], 10 | extensions: ['.test', ''], 11 | }, 12 | }; 13 | const reconConfig = { 14 | context: 'src', 15 | resolve: { 16 | roots: ['core'], 17 | extensions: ['.test'], 18 | }, 19 | }; 20 | 21 | expect(configFromWebpack(webpackConfig, {cwd})).toMatchObject(reconConfig); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/recon-server/src/createServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const graphqlHTTP = require('express-graphql'); 3 | const http = require('http'); 4 | 5 | /** Given a recon engine create a http-server for querying */ 6 | function createServer(engine, {port = 4000} = {}) { 7 | const app = express(); 8 | 9 | app.use( 10 | '/graphql', 11 | graphqlHTTP({ 12 | schema: engine.schema, 13 | graphiql: true, 14 | }) 15 | ); 16 | 17 | const server = http.createServer(app); 18 | 19 | return new Promise(accept => { 20 | server.listen(port, () => { 21 | accept(server); 22 | }); 23 | }); 24 | } 25 | 26 | module.exports = createServer; 27 | -------------------------------------------------------------------------------- /packages/recon-config/src/createConfig.js: -------------------------------------------------------------------------------- 1 | const Jetpack = require('fs-jetpack'); 2 | const Path = require('path'); 3 | const {CONFIG_FILE_NAME} = require('./shared'); 4 | 5 | /** 6 | * Create a new config file with user defined configuration 7 | */ 8 | function createConfig(userConfig, {cwd = process.cwd()} = {}) { 9 | // TODO: Search for definition within package.json 10 | const rc = Jetpack.cwd(cwd).exists(CONFIG_FILE_NAME); 11 | if (rc) { 12 | throw new Error('Oops! Looks like you already have a .reconrc file!'); 13 | } 14 | Jetpack.cwd(cwd).write(CONFIG_FILE_NAME, userConfig); 15 | return Path.resolve(cwd, CONFIG_FILE_NAME); 16 | } 17 | 18 | module.exports = createConfig; 19 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/user-list.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import Button from './button'; 4 | import List, {ListItem} from './list'; 5 | 6 | function UserItem({user}) { 7 | return ( 8 | 9 | 10 | {user.name} 11 | 12 | 13 | ); 14 | } 15 | 16 | export default function UserList({users}) { 17 | return ( 18 | 19 | {users.map(user => { 20 | return ; 21 | })} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | import Button from './button'; 6 | import Notes from './notes'; 7 | import UserList from './user-list'; 8 | 9 | const users = []; 10 | 11 | const App = ({users}) => ( 12 |
    13 |

    My Application

    14 |

    15 | Users 16 | 17 |

    18 | 19 | 20 |
    21 | ); 22 | 23 | function renderApp() { 24 | ReactDOM.render(, document.getElementById('app')); 25 | } 26 | 27 | renderApp(); 28 | -------------------------------------------------------------------------------- /packages/recon-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recon-config", 3 | "version": "0.0.7", 4 | "description": "An api for discovering and reading config for recon.", 5 | "main": "src/index.js", 6 | "keywords": [ 7 | "intellisense", 8 | "components", 9 | "react" 10 | ], 11 | "author": "Lystable Industries ", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lystable/recon.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lystable/recon/issues" 19 | }, 20 | "homepage": "https://github.com/lystable/recon", 21 | "dependencies": { 22 | "fs-jetpack": "^0.13.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/getConfig.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const Path = require('path'); 3 | 4 | const getConfig = require('../getConfig'); 5 | 6 | it('should read a config file with no user config', () => { 7 | const cwd = Path.resolve(__dirname, '__fixtures__/all'); 8 | const uc = undefined; 9 | expect(getConfig(uc, {cwd})).toMatchObject({ 10 | files: '**/*.js', 11 | resolve: {}, 12 | }); 13 | }); 14 | 15 | it('should read a config file with user config', () => { 16 | const cwd = Path.resolve(__dirname, '__fixtures__/all'); 17 | const uc = {files: 'test.js'}; 18 | expect(getConfig(uc, {cwd})).toMatchObject({ 19 | files: 'test.js', 20 | resolve: {}, 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/recon-stats/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recon-stats", 3 | "version": "0.0.7", 4 | "description": "Generate stats from recon intelligence", 5 | "main": "src/index.js", 6 | "keywords": [ 7 | "intellisense", 8 | "components", 9 | "react" 10 | ], 11 | "author": "Lystable Industries ", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lystable/recon.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lystable/recon/issues" 19 | }, 20 | "homepage": "https://github.com/lystable/recon", 21 | "dependencies": { 22 | "fs-jetpack": "^0.13.1", 23 | "lodash": "^4.15.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/recon-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recon-server", 3 | "version": "0.0.7", 4 | "description": "A GraphQL server for querying the Recon Engine.", 5 | "main": "src/index.js", 6 | "keywords": [ 7 | "intellisense", 8 | "components", 9 | "react" 10 | ], 11 | "author": "Lystable Industries ", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lystable/recon.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lystable/recon/issues" 19 | }, 20 | "homepage": "https://github.com/lystable/recon", 21 | "dependencies": { 22 | "express": "^4.14.0", 23 | "express-graphql": "^0.6.3", 24 | "graphql": "^0.9.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/recon-stats/src/query.graphql: -------------------------------------------------------------------------------- 1 | { 2 | modules { 3 | path 4 | } 5 | 6 | components { 7 | id, 8 | name, 9 | module { 10 | path 11 | }, 12 | dependencies { 13 | name, 14 | component { 15 | id, 16 | pathEnhancements { 17 | type 18 | } 19 | module { 20 | path 21 | } 22 | }, 23 | usages { 24 | props { 25 | name, 26 | valueType 27 | } 28 | } 29 | }, 30 | dependants { 31 | name, 32 | component { 33 | id, 34 | name, 35 | module { 36 | path 37 | } 38 | } 39 | usages { 40 | props { 41 | name, 42 | valueType 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/recon-config/src/getConfig.js: -------------------------------------------------------------------------------- 1 | const Jetpack = require('fs-jetpack'); 2 | const {CONFIG_FILE_NAME} = require('./shared'); 3 | 4 | /** 5 | * Generate a full config for a working directory and any user config 6 | * - Will manage any sensible merging of configs as complexity grows 7 | */ 8 | function getConfig(uc, {cwd = process.cwd()} = {}) { 9 | // TODO: Search for definition within package.json 10 | const rc = Jetpack.cwd(cwd).read(CONFIG_FILE_NAME, 'json'); 11 | if (!rc) { 12 | throw new Error( 13 | "Oops! Doesn't look like there is a valid .reconrc file" + 14 | 'defined in your project root. See: https://github' + 15 | '.com/lystable/recon/tree/master/packages/recon-config for info.' 16 | ); // eslint-disable-line max-len 17 | } 18 | return Object.assign({}, rc, uc); 19 | } 20 | 21 | module.exports = getConfig; 22 | -------------------------------------------------------------------------------- /packages/recon-config/src/configFromWebpack.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | 3 | /** Given webpack configuration object return recon config */ 4 | function configFromWebpack(webpackConfig, {cwd = process.cwd()} = {}) { 5 | const config = {}; 6 | 7 | if (webpackConfig.context) { 8 | config.context = Path.relative(cwd, webpackConfig.context); 9 | } 10 | 11 | if (webpackConfig.resolve) { 12 | const resolve = {}; 13 | 14 | if (webpackConfig.resolve.extensions) { 15 | resolve.extensions = webpackConfig.resolve.extensions.filter(x => !!x); 16 | } 17 | 18 | const roots = webpackConfig.resolve.root || webpackConfig.resolve.roots; // support v1 & v2 19 | if (roots) { 20 | resolve.roots = roots.map(p => 21 | Path.relative(webpackConfig.context || cwd, p)); 22 | } 23 | 24 | config.resolve = resolve; 25 | } 26 | 27 | return config; 28 | } 29 | 30 | module.exports = configFromWebpack; 31 | -------------------------------------------------------------------------------- /packages/recon-config/src/__tests__/createConfig.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const Path = require('path'); 3 | const Jetpack = require('fs-jetpack'); 4 | 5 | const createConfig = require('../createConfig'); 6 | 7 | it('should throw if a config already exists', () => { 8 | const cwd = Path.resolve(__dirname, '__fixtures__/all'); 9 | expect(() => createConfig({}, {cwd})).toThrow(); 10 | }); 11 | 12 | it('should create a new configuration file', () => { 13 | const uc = { 14 | resolve: { 15 | extensions: ['.jsx', '.js'], 16 | }, 17 | }; 18 | const cwd = Path.resolve(__dirname, '__fixtures__/empty'); 19 | const file = createConfig(uc, {cwd}); 20 | expect(Jetpack.cwd(cwd).exists('.reconrc')).toBe('file'); 21 | expect(Jetpack.cwd(cwd).read('.reconrc', 'json')).toMatchObject(uc); 22 | expect(file).toMatch(Path.resolve(cwd, '.reconrc')); 23 | // finally, clean up test 24 | Jetpack.cwd(cwd).remove('.reconrc'); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/recon-engine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recon-engine", 3 | "version": "0.0.7", 4 | "description": "The engine runtime for Recon.", 5 | "main": "src/index.js", 6 | "keywords": [ 7 | "intellisense", 8 | "components", 9 | "react" 10 | ], 11 | "author": "Lystable Industries ", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lystable/recon.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lystable/recon/issues" 19 | }, 20 | "homepage": "https://github.com/lystable/recon", 21 | "dependencies": { 22 | "babel-traverse": "^6.23.1", 23 | "babel-types": "^6.23.0", 24 | "babylon": "^6.16.1", 25 | "fs-jetpack": "^0.13.1", 26 | "glob": "^7.0.5", 27 | "lodash": "^4.15.0" 28 | }, 29 | "devDependencies": { 30 | "graphql": "^0.11.7" 31 | }, 32 | "peerDependencies": { 33 | "graphql": ">=0.11.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/recon-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "recon-cli", 3 | "version": "0.0.7", 4 | "description": "A commandline interface for wielding the power of Recon.", 5 | "main": "src/index.js", 6 | "keywords": [ 7 | "intellisense", 8 | "components", 9 | "react" 10 | ], 11 | "author": "Lystable Industries ", 12 | "license": "Apache-2.0", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/lystable/recon.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/lystable/recon/issues" 19 | }, 20 | "homepage": "https://github.com/lystable/recon", 21 | "dependencies": { 22 | "dedent": "^0.7.0", 23 | "fs-jetpack": "^0.13.1", 24 | "lodash": "^4.15.0", 25 | "progress": "^1.1.8", 26 | "recon-config": "^0.0.7", 27 | "recon-engine": "^0.0.7", 28 | "recon-server": "^0.0.7", 29 | "recon-stats": "^0.0.7", 30 | "vorpal": "^1.11.4" 31 | }, 32 | "bin": { 33 | "recon": "./src/index.js" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/createEngine.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const Path = require('path'); 3 | const FS = require('fs'); 4 | 5 | const createEngine = require('../createEngine'); 6 | 7 | function run(path) { 8 | const absPath = Path.resolve(__dirname, '__fixtures__', path); 9 | const query = FS.readFileSync(Path.resolve(absPath, 'query.graphql'), { 10 | encoding: 'utf8', 11 | }); 12 | 13 | const engine = createEngine({ 14 | files: '**/*.js', 15 | context: Path.resolve(absPath, 'src'), 16 | }); 17 | 18 | return new Promise((accept, reject) => { 19 | engine.subscribe(stats => { 20 | if (stats.numModules && stats.canQuery) { 21 | engine.runQuery(query).then( 22 | result => { 23 | // TODO: snapshot tests 24 | expect(result).toMatchSnapshot(); 25 | accept(); 26 | }, 27 | err => reject(err) 28 | ); 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | it('should respond to query as expected: basic-app', () => run('basic-app')); 35 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/__tests__/__fixtures__/basic-app/src/notes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import List, {ListItem} from './list'; 4 | import {createContainer, query as q} from 'api/recall'; 5 | import {Note} from 'api/resources'; 6 | 7 | function NoteItem({note}) { 8 | return ( 9 | 10 | 11 |

    {note.content}

    12 |
    13 | ); 14 | } 15 | 16 | export function Notes({notes}) { 17 | return ( 18 |
    19 |

    Notes

    20 | 21 | {notes.map(user => { 22 | return ; 23 | })} 24 | 25 |
    26 | ); 27 | } 28 | 29 | const contain = createContainer({ 30 | queries: { 31 | invoices: q.many(Note, { 32 | params: vars => ({ 33 | filter: { 34 | parent: vars.parent, 35 | }, 36 | }), 37 | fields: { 38 | content: true, 39 | created_by: { 40 | img: true, 41 | }, 42 | }, 43 | }), 44 | }, 45 | }); 46 | 47 | export default contain(Notes); 48 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/enhanced-components/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import {withState, compose} from 'recompose'; 4 | import {createContainer} from 'recall'; 5 | 6 | export function FunctionalComponent() { 7 | return
    Hello world!
    ; 8 | } 9 | 10 | // Note we currently do not detect this as a "directly enhanced" component 11 | // but rather it can be seen as an "enhancement path" for the 12 | // referenced `FunctionalComponent`. Ie. if `EnhancedFunctionalComponent` 13 | // was referenced as a dep from another component we could trace it back 14 | // to `FunctionalComponent` but mark this as a "enhancement path". 15 | // In the future we may want to discover all these within a module at parse time. 16 | export const EnhancedFunctionalComponent = withState()(FunctionalComponent); 17 | 18 | export const ArrowFunctionalComponent = () =>
    ; 19 | 20 | const enhance = compose(createContainer(), withState()); 21 | 22 | export const EnhancedArrowFunctionalComponent = enhance(() =>
    ); 23 | 24 | export default class ClassComponent { 25 | render() { 26 | return
    Hello world! Click Here!
    ; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/dev-guide.md: -------------------------------------------------------------------------------- 1 | Recon Developer Guide 2 | ===================== 3 | 4 | The first thing to get to grips with is that Recon is structured as a monorepo 5 | and contains several packages. These are: 6 | 7 | - [recon-engine](../packages/recon-engine) - The powerhouse behind Recon. Parses, structures and provides a query interface. 8 | - [recon-server](../packages/recon-server) - A simple http server which spawns the engine and provides a http graphql interface. 9 | - [recon-cli](../packages/recon-cli) - Command line interface for quickly working with Recon as opposed to using the programmatic api's. 10 | - [recon-config](../packages/recon-config) - Recon configuration management (reads .rc file etc.) 11 | - [recon-stats](../packages/recon-stats) - Generate useful and interesting stats about an application 12 | 13 | ## The code 14 | 15 | - `npm test` will tell you what's up (type checking, linter ([eslint-config-lystable](https://github.com/lystable/guidelines/tree/master/styleguides/eslint-config-lystable)) and unit tests) 16 | 17 | ## Let's get going! 18 | 19 | Depending on where you're wanting to start you may be interested in the more targeted 20 | developer guides: 21 | 22 | - [recon-engine Developer Guide](../packages/recon-engine/docs/dev-guide.md) 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing Guidelines 2 | ======================= 3 | 4 | ## Got a bug when using Recon against your application source code? 5 | 6 | The nature of Recon and flexibility of Javascript means it is very hard to gather 7 | all the edge cases for usage of React. Therefore the best way for us to debug 8 | and resolve issues is to have a reproducible breaking test case which demonstrates 9 | your usage. 10 | 11 | We therefore ask if you have an issue with the parsing or output of Recon to please 12 | submit a pull request with the following: 13 | 14 | - A test case which demonstrates the code which breaks/doesn't do what you expect. 15 | - In most cases this will be as simple as copying out a fixture and hooking up a new test 16 | which will most likely live either [here](../packages/recon-engine/src/engine/__tests__) or [here](../packages/recon-engine/src/parse/__tests__) 17 | - Title prefixed with `[BUG]` 18 | 19 | ## Got a feature idea? 20 | 21 | We'd love to discuss it! Feel free to submit an issue. Just try to be as descriptive as you can 22 | and preferably include code/output examples since that can really help. 23 | 24 | ## Developing Recon 25 | 26 | Read more about contributing to the codebase in our [Developer Guide](./docs/dev-guide.md) 27 | 28 | At an absolute minimum make sure `npm test` runs! :) 29 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/parseModule.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | const parseModule = require('../parseModule'); 3 | const FS = require('fs'); 4 | const Path = require('path'); 5 | 6 | describe('react-engine::parse/parseModule', () => { 7 | describe('::parseModule (default)', () => { 8 | function run(path) { 9 | const absPath = Path.resolve(__dirname, '__fixtures__', path); 10 | const src = FS.readFileSync(`${absPath}/src.js`, {encoding: 'utf8'}); 11 | const module = {src, path, id: path}; 12 | const parsed = parseModule(module); 13 | expect(parsed).toMatchSnapshot(); 14 | } 15 | 16 | it('should parse module information as expected: empty', () => 17 | run('empty')); 18 | it('should parse module information as expected: no-components', () => 19 | run('no-components')); 20 | it('should parse module information as expected: basic-components', () => 21 | run('basic-components')); 22 | it('should parse module information as expected: enhanced-components', () => 23 | run('enhanced-components')); 24 | it('should parse module information as expected: real-world-lystable', () => 25 | run('real-world-lystable')); 26 | it('should parse module information as expected: re-exports', () => 27 | run('re-exports')); 28 | it('should parse module information as expected: dynamic-components', () => 29 | run('dynamic-components')); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/recon-engine/src/utils/isReactComponent.js: -------------------------------------------------------------------------------- 1 | const T = require('babel-types'); 2 | const {find} = require('lodash'); 3 | const traverse = require('babel-traverse').default; 4 | 5 | /** Does a given function path contain JSX? */ 6 | function containsJSX(node) { 7 | if (T.isJSXElement(node)) { 8 | return true; 9 | } 10 | 11 | let doesContainJSX = false; 12 | const visitor = { 13 | ReturnStatement(jsxPath) { 14 | if (T.isJSXElement(jsxPath.node.argument)) { 15 | doesContainJSX = true; 16 | jsxPath.stop(); 17 | } 18 | }, 19 | 20 | noScope: true, 21 | }; 22 | 23 | traverse(node, visitor); 24 | return doesContainJSX; 25 | } 26 | 27 | /** Is given path a react component declaration? */ 28 | function isReactComponent(node) { 29 | // TODO: Is there a stronger way of determining a "react component"? 30 | // TODO: Accept React.createClass() (unless there is plans to deprecate in *near* future?) 31 | 32 | if (T.isClassDeclaration(node)) { 33 | return !!find( 34 | node.body.body, 35 | bNode => T.isClassMethod(bNode) && bNode.key.name === 'render' 36 | ); 37 | } 38 | 39 | if (T.isFunctionDeclaration(node)) { 40 | return containsJSX(node.body); 41 | } 42 | 43 | if (T.isFunctionExpression(node)) { 44 | return containsJSX(node.body); 45 | } 46 | 47 | if (T.isArrowFunctionExpression(node)) { 48 | return containsJSX(node.body); 49 | } 50 | 51 | return false; 52 | } 53 | 54 | module.exports = isReactComponent; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "recon", 4 | "scripts": { 5 | "test:lint": "eslint **/*.js --cache --cache-location .tmp/eslint/", 6 | "test:type": "flow", 7 | "test:unit": "jest --forceExit", 8 | "test": "npm run test:lint && npm run test:type && npm run test:unit", 9 | "pub": "npm run test && lerna publish", 10 | "bootstrap": "lerna bootstrap", 11 | "precommit": "lint-staged", 12 | "postinstall": "npm run bootstrap", 13 | "format": "prettier --single-quote --trailing-comma es5 --no-bracket-spacing --write" 14 | }, 15 | "author": "Lystable Industries ", 16 | "license": "Apache-2.0", 17 | "devDependencies": { 18 | "babel-eslint": "7.1.1", 19 | "eslint": "3.18.0", 20 | "eslint-config-airbnb": "14.1.0", 21 | "eslint-config-lystable": "6.0.0", 22 | "eslint-config-prettier": "1.5.0", 23 | "eslint-plugin-flowtype": "2.30.3", 24 | "eslint-plugin-import": "2.2.0", 25 | "eslint-plugin-jsx-a11y": "4.0.0", 26 | "eslint-plugin-react": "6.10.0", 27 | "flow-bin": "0.42.0", 28 | "husky": "^0.13.2", 29 | "jest": "^19.0.2", 30 | "lerna": "2.0.0-beta.38", 31 | "lint-staged": "^3.4.0", 32 | "prettier": "^0.22.0" 33 | }, 34 | "jest": { 35 | "coverageDirectory": ".tmp/coverage/", 36 | "collectCoverage": true, 37 | "testEnvironment": "node", 38 | "testMatch": [ 39 | "**/__tests__/**/(*.)(test).js?(x)" 40 | ] 41 | }, 42 | "lint-staged": { 43 | "*.js": [ 44 | "npm run format", 45 | "git add", 46 | "npm run test:lint" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/recon-config/README.md: -------------------------------------------------------------------------------- 1 | recon-config 2 | ============ 3 | 4 | Recon configuration management. 5 | 6 | *Part of [Recon: Code Intelligence for React](https://github.com/lystable/recon)* 7 | 8 | ### Configuration 9 | 10 | All projects hoping to use Recon should have a `.reconrc` file at their root. 11 | 12 | - `context` - Where should we search for files (default `process.cwd()`) 13 | - `files` *(required)* - [Glob](https://en.wikipedia.org/wiki/Glob_(programming)) pattern telling which files 14 | recon should parse. This should be pretty much all js files in your application which are likely to 15 | be used by or use a component. It is recommended to exclude any meta files such as tests. 16 | - `resolve` - Namespace to help recon resolve require/import paths correctly 17 | - `roots` - An array of paths (relative to working directory) from which to resolve paths. 18 | Eg. `require('components/button')` and `roots: [core]` would look in `core/components/button` 19 | - `extensions` - An array of extensions to include when resolving paths without an explicit extension. 20 | Default: `['.js', '.jsx']` 21 | - `ignore` - Regex to exclude paths from being parsed (default `/node_modules/`) 22 | - Note: atm we don't support flags, pr's welcome! 23 | 24 | ##### Example configuration 25 | 26 | ```json 27 | { 28 | "context": "src", 29 | "files": "**/!(*-test|*-tests|*.manifest).js*", 30 | "resolve": { 31 | "roots": [ 32 | "core", 33 | "" 34 | ] 35 | }, 36 | "ignore": "/node_modules/" 37 | } 38 | ``` 39 | 40 | > Note: If you make changes to your project config file you must restart your `recon` cli! 41 | -------------------------------------------------------------------------------- /flow/lodash.js.flow: -------------------------------------------------------------------------------- 1 | declare module 'lodash' { 2 | declare function find(list: T[], predicate: (val: T)=>boolean): ?T; 3 | declare function findWhere(list: Array, properties: {[key:string]: any}): ?T; 4 | declare function clone(obj: T): T; 5 | 6 | declare function isEqual(a: any, b: any): boolean; 7 | declare function range(a: number, b: number): Array; 8 | declare function extend(o1: S, o2: T): S & T; 9 | 10 | declare function zip(a1: S[], a2: T[]): Array<[S, T]>; 11 | 12 | declare function flatten(a: S[][]): S[]; 13 | 14 | declare function any(list: Array, pred: (el: T)=>boolean): boolean; 15 | 16 | declare function each(o: {[key:string]: T}, iteratee: (val: T, key: string)=>void): void; 17 | declare function each(a: T[], iteratee: (val: T, key: string)=>void): void; 18 | 19 | declare function map(a: T[], iteratee: (val: T, n?: number)=>U): U[]; 20 | declare function map(a: {[key:K]: T}, iteratee: (val: T, k?: K)=>U): U[]; 21 | 22 | declare function object(a: Array<[string, T]>): {[key:string]: T}; 23 | 24 | declare function every(a: Array, pred: (val: T)=>boolean): boolean; 25 | 26 | declare function initial(a: Array, n?: number): Array; 27 | declare function rest(a: Array, index?: number): Array; 28 | 29 | declare function sortBy(a: T[], iteratee: (val: T)=>any): T[]; 30 | 31 | declare function filter(o: {[key:string]: T}, pred: (val: T, k: string)=>boolean): T[]; 32 | 33 | declare function isEmpty(o: any): boolean; 34 | 35 | declare function groupBy(a: Array, iteratee: (val: T, index: number)=>any): {[key:string]: T[]}; 36 | 37 | declare function min(a: Array|{[key:any]: T}): T; 38 | declare function max(a: Array|{[key:any]: T}): T; 39 | 40 | declare function values(o: {[key: any]: T}): T[]; 41 | declare function flatten(a: Array): Array; 42 | 43 | // TODO: improve this 44 | declare function chain(obj: S): any; 45 | } 46 | -------------------------------------------------------------------------------- /packages/recon-engine/src/utils/__tests__/isReactComponent.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, jest */ 2 | const Babylon = require('babylon'); 3 | const traverse = require('babel-traverse').default; 4 | 5 | const isReactComponent = require('../isReactComponent'); 6 | 7 | function parse(type, code) { 8 | let found; 9 | 10 | const ast = Babylon.parse(code, { 11 | plugins: ['jsx', 'flow', 'objectRestSpread'], 12 | }); 13 | 14 | traverse(ast, { 15 | [type]: function find(path) { 16 | found = path.node; 17 | path.stop(); 18 | }, 19 | }); 20 | 21 | return found; 22 | } 23 | 24 | describe('utils/react/isReactComponent', () => { 25 | it('should identify class components', () => { 26 | const node = parse( 27 | 'ClassDeclaration', 28 | `class MyComponent { 29 | render() {} 30 | }` 31 | ); 32 | expect(isReactComponent(node)).toBe(true); 33 | }); 34 | 35 | it('should identify function declarations', () => { 36 | const node = parse( 37 | 'FunctionDeclaration', 38 | `function MyComponent() { 39 | return
    Test
    ; 40 | }` 41 | ); 42 | expect(isReactComponent(node)).toBe(true); 43 | }); 44 | 45 | it('should identify function expressions', () => { 46 | const node = parse( 47 | 'FunctionExpression', 48 | `const MyComponent = function() { 49 | return
    Test
    ; 50 | }` 51 | ); 52 | expect(isReactComponent(node)).toBe(true); 53 | }); 54 | 55 | it('should identify arrow function expressions', () => { 56 | const node = parse( 57 | 'ArrowFunctionExpression', 58 | `const MyComponent = () =>
    Test
    ;` 59 | ); 60 | expect(isReactComponent(node)).toBe(true); 61 | }); 62 | 63 | it('should NOT identify arrow function expressions without JSX', () => { 64 | const node = parse( 65 | 'ArrowFunctionExpression', 66 | `const MyComponent = () => true;` 67 | ); 68 | expect(isReactComponent(node)).toBe(false); 69 | }); 70 | 71 | it('should NOT identify function declarations without JSX', () => { 72 | const node = parse( 73 | 'FunctionDeclaration', 74 | `function MyComponent() { 75 | return null; 76 | }` 77 | ); 78 | expect(isReactComponent(node)).toBe(false); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/recon-engine/docs/dev-guide.md: -------------------------------------------------------------------------------- 1 | recon-engine Developer Guide 2 | ============================ 3 | 4 | As much help as we can cram into a md file to get you going developing Recon. 5 | 6 | Please also refer to our top-level [Developer Guide](../../../docs/dev-guide.md) if you haven't already! 7 | 8 | ## How do we come up with our data? 9 | 10 | 1. Discover 11 | 2. Parse and extract 12 | 2. Query and resolve 13 | 14 | #### 1. Discover 15 | 16 | The simplest way to build up a dependency and usage graph is to find and parse all 17 | modules in a codebase. The user can direct on where in their file system they want 18 | us to search. 19 | 20 | #### 2. Parse and extract 21 | 22 | We then, on an individual module basis, traverse and extract 23 | any information we can find to be potentially useful for querying later. This is 24 | basically taking the user's source code, identifying the pieces we want to query and 25 | then structuring it in the optimal way for search/traversal. 26 | 27 | i. Parse each module individually looking for *top level* symbols. Resolve each symbol 28 | to it's local definition. 29 | ii. Are any of these symbols components? Let's extract *whatever* useful info we can 30 | (and resolve references upto the module boundary). 31 | iii. Return "module" (containing root symbol table and corresponding component info) 32 | for it to be stored in global list. 33 | 34 | #### 3. Query and resolve 35 | 36 | i. Queries (Graphql) come in and we can traverse our parsed modules to fulfil. 37 | ii. As we are resolving references (mainly component usage) we can collect any useful 38 | information along the way (eg. "enhancements") 39 | 40 | > Note: Immutability helps this to be heavily optimised (memoizing) 41 | 42 | ## So what does that mean if there's something I want to fix? 43 | 44 | Well... 45 | 46 | - If Recon is struggling to extract component definitions in your module look in `src/parse` 47 | - If Recon is failing to connect the dots when you query components look in `src/query` 48 | 49 | ## Useful resources 50 | 51 | - [ASTExplorer](http://astexplorer.net) - Wonderful tool for easily inspecting your code's AST 52 | - [babel-handbook](https://github.com/thejameskyle/babel-handbook) (the plugin section) - 53 | If you want to touch `src/parse` this will get you up to speed with how to efficiently 54 | traverse the ast generated by babel. 55 | 56 | -------------------------------------------------------------------------------- /packages/recon-engine/src/engine/createEngine.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const _glob = require('glob'); 3 | const Path = require('path'); 4 | const Jetpack = require('fs-jetpack'); 5 | const {graphql} = require('graphql'); 6 | const { 7 | pull, 8 | forEach, 9 | values, 10 | filter, 11 | map, 12 | memoize, 13 | join, 14 | flatten, 15 | cloneDeepWith, 16 | trim, 17 | } = require('lodash'); 18 | 19 | const createSchema = require('../query/createSchema'); 20 | const parseModule = require('../parse/parseModule'); 21 | 22 | /** Promisified glob */ 23 | function glob(pattern, opts) { 24 | return new Promise((resolve, reject) => { 25 | _glob(pattern, opts, (err, files) => { 26 | return err ? reject(err) : resolve(files); 27 | }); 28 | }); 29 | } 30 | 31 | /** Resolve modules based on configuration (allows for some level of rewriting module paths */ 32 | function createResolver( 33 | cwd, 34 | { 35 | roots: _roots = [cwd], 36 | extensions = ['.js', '.jsx'], 37 | } = {} 38 | ) { 39 | // TODO: Support "aliases" (eg. webpack aliases) 40 | const roots = _roots.map(r => Path.resolve(cwd, r)); 41 | return memoize( 42 | (context, target) => { 43 | const resolveFromPaths = [Path.dirname(context), ...roots]; 44 | const resolvedPaths = resolveFromPaths.map(path => 45 | Path.resolve(path, target)); 46 | const finalPaths = /\.[a-zA-Z0-9]$/.test(target) // has extension 47 | ? resolvedPaths 48 | : flatten( 49 | resolvedPaths.map(p => [ 50 | ...extensions.map(ext => `${p}${ext}`), 51 | Path.resolve(p, 'index.js'), 52 | ]) 53 | ); 54 | 55 | return finalPaths; 56 | }, 57 | join 58 | ); 59 | } 60 | 61 | /** Create a new engine instance */ 62 | function createEngine( 63 | { 64 | files, 65 | context: _rawContext = '', 66 | cwd = process.cwd(), 67 | resolve, 68 | exclude = '/node_modules/', 69 | } 70 | ) { 71 | const subscriptions = []; 72 | const modules = {}; 73 | let hasDiscovered = false; 74 | const excludeRegexp = new RegExp(trim(exclude, '/')); 75 | const context = Path.resolve(cwd, _rawContext); 76 | 77 | const resolveModulePaths = createResolver(context, resolve); 78 | 79 | // TODO: Cache parsed modules 80 | // TODO: Add persisted/watching support 81 | 82 | glob(files, {cwd: context}).then(rawFoundFiles => { 83 | hasDiscovered = true; 84 | 85 | const foundFiles = filter(rawFoundFiles, path => !excludeRegexp.test(path)); 86 | forEach(foundFiles, file => { 87 | const path = Path.resolve(context, file); 88 | const module = {ready: false, file, path}; 89 | modules[file] = module; 90 | 91 | Jetpack.readAsync(path, 'utf8').then( 92 | src => { 93 | module.ready = true; 94 | module.parsed = parseModule({src, path, id: file}); 95 | module.error = module.parsed.error; 96 | send(); 97 | }, 98 | error => { 99 | module.ready = true; 100 | module.error = error; 101 | send(); 102 | } 103 | ); 104 | }); 105 | 106 | send(); 107 | }); 108 | 109 | /** Get stats about the current state */ 110 | function getStats() { 111 | const allModules = values(modules); 112 | const readyModules = filter(allModules, m => m.ready); 113 | const moduleErrors = map(filter(allModules, m => m.error), m => ({ 114 | path: m.path, 115 | error: m.error, 116 | })); 117 | return { 118 | numModules: allModules.length, 119 | numReadyModules: readyModules.length, 120 | numErroredModules: moduleErrors.length, 121 | moduleErrors, 122 | hasDiscovered, 123 | canQuery: hasDiscovered && allModules.length === readyModules.length, 124 | }; 125 | } 126 | 127 | // TODO: The memoizing inside query/resolve doesn't support changing data yet :( 128 | const schema = createSchema(modules, {resolveModulePaths}); 129 | 130 | /* Run a graphql query against our store */ 131 | function runQuery(query) { 132 | return graphql(schema, query); 133 | } 134 | 135 | /** 136 | * Push changes to subscribers 137 | */ 138 | function send() { 139 | forEach(subscriptions, func => func(getStats())); 140 | } 141 | 142 | /** Subscribe to changes */ 143 | function subscribe(func) { 144 | subscriptions.push(func); 145 | // return an unsubscribe function 146 | return () => { 147 | pull(subscriptions, [func]); 148 | }; 149 | } 150 | 151 | /** get debug information */ 152 | function _debug({raw = true}) { 153 | if (raw) { 154 | return {modules}; 155 | } 156 | 157 | // exclude ast nodes from debug output (just track source location) 158 | const ignoreAstNodes = (v, k) => k === '__node' ? v.loc : undefined; 159 | 160 | const strippedModules = cloneDeepWith(modules, ignoreAstNodes); 161 | return {modules: strippedModules}; 162 | } 163 | 164 | return {runQuery, subscribe, schema, _debug}; 165 | } 166 | 167 | module.exports = createEngine; 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

     

    2 |

    3 | 4 |

    5 |

     

    6 | 7 | Recon 8 | ===== 9 | 10 | Code Intelligence for React Applications. 11 | 12 | [![Build Status](https://travis-ci.org/lystable/recon.svg?branch=master)](https://travis-ci.org/lystable/recon) 13 | 14 | ### What? 15 | 16 | Recon provides a new level of insight into your codebase allowing you to understand 17 | how your components are being used, easily grep their complexity, draw dependency graphs, 18 | and isolate points for optimisation. 19 | 20 | On top of this the raw information Recon provides gives a strong base for creating tools 21 | (such as living styleguides) which may need to plug in to component meta data. 22 | 23 | ### How? 24 | 25 | The core of Recon revolves around `recon-engine`. This engine parses your application pulling out 26 | any data which may be useful (eg. Props, component dependencies, enhancements etc.). Then a 27 | graphql query interface is exposed allowing you to explore your applications in an incredibly 28 | intuitive manner! 29 | 30 | Checking out our [test fixtures](./packages/recon-engine/src/engine/__fixtures__/) is a 31 | great place to an example of this. 32 | 33 | Once this data is consolidated the possibility of tools to be built on top are *endless*! 34 | 35 | Getting Started 36 | --------------- 37 | 38 | Prerequisites: `Node >v6`. Also ensure using `import/export` syntax, using JSX (see roadmap for full list) 39 | 40 | ### Install and configuration 41 | 42 | The quickest way to get going with Recon for your project is to use our CLI application. 43 | 44 | Firstly install with 45 | 46 | ``` 47 | $ npm install -g recon-cli 48 | ``` 49 | 50 | Now, within your application working directory, simply run 51 | 52 | ``` 53 | $ recon 54 | ``` 55 | 56 | You are now *inside* Recon! :O 57 | 58 | From this point forwards the entire power of Recon should be just a `help` command away. 59 | 60 | ``` 61 | recon$ help 62 | ``` 63 | 64 | The first thing you're going to want to do from here is create a new config file `.reconrc` in the working directory 65 | of your project. 66 | 67 | You can do this interactively by running 68 | 69 | ``` 70 | recon$ init 71 | ``` 72 | 73 | *To view all configuration possibilities you can view the docs [here](./packages/recon-config/README.md).* 74 | 75 | ### Show me the power! 76 | 77 | Why not start off by trying `stats`. This will analyse your application and dump out a bunch of objective 78 | statements and statistics. 79 | 80 | ``` 81 | recon$ stats 82 | ``` 83 | 84 | Then if you're feeling *extra* adventurous give `server` a go. This will spawn a new `graphql` server which will 85 | allow you to query your application meta data freely. 86 | 87 | ``` 88 | recon$ server 89 | ``` 90 | 91 | For everything else see what is available via the `help` command! 92 | 93 | ### I want to integrate Recon into x 94 | 95 | Documentation is going to be a little skimpy here for a while since we are planning on getting 96 | the internals of `recon-engine` to be as powerful as possible and stabilising the api as much as 97 | possible. 98 | 99 | Most likely you'll want to look at using `recon-engine` and `recon-server` (their tests are a decent 100 | place to start looking). 101 | 102 | > Disclaimer: This is a very early preview of Recon and you should expect breaking changes within the ({ 44 | name: c.name, 45 | usages: c.dependants 46 | .map(d => d.usages.length) 47 | .reduce((a, b) => a + b, 0), 48 | })) 49 | .sort((a, b) => a.usages > b.usages ? -1 : 1) 50 | .map(c => [c.name, c.usages]), 51 | }, 52 | 53 | { 54 | title: 'Fattest components', 55 | description: 'Which components render the most elements?', 56 | headers: ['Component Name', 'Rendered elements'], 57 | data: data.components 58 | .map(c => ({ 59 | name: c.name, 60 | elements: sum(c.dependencies.map(d => d.usages.length)), 61 | })) 62 | .sort((a, b) => a.elements > b.elements ? -1 : 1) 63 | .map(c => [c.name, c.elements]), 64 | }, 65 | 66 | { 67 | title: 'Most externally complex components', 68 | description: 'Which components require the most interface?', 69 | headers: ['Component Name', 'Average Props', 'Component Usages'], 70 | data: data.components 71 | .map(c => ({ 72 | name: c.name, 73 | avgProps: round( 74 | mean( 75 | flatten(c.dependants.map(d => d.usages.map(u => u.props.length))) 76 | ), 77 | 2 78 | ), 79 | usages: sum(c.dependants.map(d => d.usages.length)), 80 | })) 81 | .sort((a, b) => a.avgProps > b.avgProps ? -1 : 1) 82 | .map(c => [c.name, c.avgProps, c.usages]), 83 | }, 84 | 85 | { 86 | title: 'Most internally complex components', 87 | description: 'Which components deal with the most amount of unique dependencies?', 88 | headers: ['Component Name', 'Unique Dependencies'], 89 | data: data.components 90 | .map(c => ({ 91 | name: c.name, 92 | uniqueDeps: c.dependencies.length, 93 | })) 94 | .sort((a, b) => a.uniqueDeps > b.uniqueDeps ? -1 : 1) 95 | .map(c => [c.name, c.uniqueDeps]), 96 | }, 97 | 98 | { 99 | title: 'Dead components', 100 | description: 'Which components are never referenced?', 101 | headers: ['Component Name'], 102 | data: data.components 103 | .filter(c => !c.dependants.length && c.name) 104 | .map(c => [c.name]), 105 | }, 106 | 107 | { 108 | title: 'One trick ponies (internal)', 109 | description: 'Which components are only ever used once?', 110 | headers: ['Component Name'], 111 | data: data.components 112 | .filter( 113 | c => 114 | c.dependants.length === 1 && 115 | c.module.path === c.dependants[0].component.module.path 116 | ) 117 | .map(c => [c.name]), 118 | }, 119 | 120 | { 121 | title: 'One trick ponies (external)', 122 | description: 'Which components are only ever used once and imported from an external module?', 123 | headers: ['Component Name'], 124 | data: data.components 125 | .filter( 126 | c => 127 | c.dependants.length === 1 && 128 | c.module.path !== c.dependants[0].component.module.path 129 | ) 130 | .map(c => [c.name]), 131 | }, 132 | 133 | { 134 | title: 'Favourite prop names', 135 | description: 'Which prop names are most popular in usage?', 136 | headers: ['Prop name', 'Usages'], 137 | data: toPairs( 138 | groupBy( 139 | flattenDeep( 140 | data.components.map(c => 141 | c.dependencies.map(d => 142 | d.usages.map(u => u.props.map(p => p.name)))) 143 | ), 144 | identity 145 | ) 146 | ) 147 | .map(([name, u]) => ({name, usages: u.length})) 148 | .sort((a, b) => a.usages > b.usages ? -1 : 1) 149 | .map(c => [c.name, c.usages]), 150 | }, 151 | 152 | { 153 | debug: true, // only show in debug mode 154 | title: 'Unresolved dependencies', 155 | description: 'Usages where the component definition was not found', 156 | headers: ['Parent Component', 'Dependency Name'], 157 | data: flatMap(data.components, c => 158 | c.dependencies.filter(dep => !dep.component).map(dep => ({ 159 | depName: dep.name, 160 | parent: c.id, 161 | }))).map(c => [c.parent, c.depName]), 162 | }, 163 | ]; 164 | } 165 | 166 | module.exports = makeStats; 167 | -------------------------------------------------------------------------------- /packages/recon-cli/src/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const vorpal = require('vorpal')(); 3 | const { 4 | forEach, 5 | padEnd, 6 | isArray, 7 | map, 8 | join, 9 | take, 10 | max, 11 | memoize, 12 | has, 13 | } = require('lodash'); 14 | const ProgressBar = require('progress'); 15 | const dedent = require('dedent'); 16 | const jetpack = require('fs-jetpack'); 17 | const Path = require('path'); 18 | 19 | const { 20 | getConfig: _getConfig, 21 | createConfig: _createConfig, 22 | configFromWebpack: _configFromWebpack, 23 | } = require('recon-config'); 24 | const {createEngine} = require('recon-engine'); 25 | const {pullStats: _pullStats} = require('recon-stats'); 26 | const {createServer} = require('recon-server'); 27 | 28 | const chalk = vorpal.chalk; 29 | 30 | // helpful urls 31 | const CONFIG_HELP_URL = 'https://github.com/lystable/recon/tree/master/packages/recon-config'; 32 | 33 | // ---------------------------------------------------------------------------- 34 | // Configuration 35 | // ---------------------------------------------------------------------------- 36 | 37 | /** Determine whether the user may be using webpack or not? */ 38 | const detectWebpack = () => { 39 | const pkg = jetpack.cwd(process.cwd()).read('package.json', 'json'); 40 | return has(pkg.dependencies, 'webpack') || 41 | has(pkg.devDependencies, 'webpack'); 42 | }; 43 | 44 | /** Given path to webpack config file lets gen recon config */ 45 | const configFromWebpack = path => { 46 | const configPath = Path.resolve(process.cwd(), path); 47 | const webpackConfig = require(configPath); // eslint-disable-line global-require, import/no-dynamic-require 48 | return _configFromWebpack(webpackConfig); 49 | }; 50 | 51 | /** Current project config */ 52 | const getConfig = uc => 53 | new Promise(accept => accept(_getConfig(uc))).catch(() => { 54 | // Looks like we need a config file... 55 | const act = vorpal.activeCommand; 56 | act.log(chalk.red('Oops! Looks like we failed to load any configuration.')); 57 | 58 | /* eslint-disable no-use-before-define */ 59 | return makeConfig().catch( 60 | /* eslint-enable no-use-before-define */ 61 | () => { 62 | throw new Error('We need a configuration file to continue! :('); 63 | } 64 | ); 65 | }); 66 | 67 | const makeConfig = () => { 68 | const act = vorpal.activeCommand; 69 | 70 | return act 71 | .prompt([ 72 | { 73 | type: 'confirm', 74 | name: 'create', 75 | message: 'Would you like us to create a config file for you?', 76 | }, 77 | { 78 | type: 'confirm', 79 | name: 'webpack', 80 | message: "We detected you're using webpack. Would you like us to try and generate resolve config from there?", 81 | when: ({create}) => create && detectWebpack(), 82 | }, 83 | { 84 | type: 'input', 85 | name: 'webpackConfig', 86 | message: 'What webpack config file should we use for your configuration? (relative to cwd)', 87 | default: './webpack.config.js', 88 | when: ({webpack}) => webpack, 89 | validate: path => 90 | jetpack.exists(path) === 'file' || 'Could not find that file.', 91 | }, 92 | { 93 | type: 'input', 94 | name: 'files', 95 | message: 'What modules should we parse in your application? (glob pattern, relative to context)', 96 | default: '**/!(*.test|*.manifest).js*', 97 | when: ({create}) => create, 98 | }, 99 | ]) 100 | .then(result => { 101 | const {create, files, webpack, webpackConfig} = result; 102 | 103 | if (create) { 104 | const config = Object.assign( 105 | {files}, 106 | webpack ? configFromWebpack(webpackConfig) : {} 107 | ); 108 | const file = _createConfig(config); 109 | act.log(chalk.green(`Configuration file created! ${file}`)); 110 | act.log( 111 | chalk.dim( 112 | `Read more about .reconrc configuration here: ${CONFIG_HELP_URL}` 113 | ) 114 | ); 115 | // anddd, try again... 116 | return getConfig(); 117 | } 118 | 119 | return null; 120 | }); 121 | }; 122 | 123 | vorpal 124 | .command('init', 'Initialise recon for this project. Creates config etc.') 125 | .action(makeConfig); 126 | 127 | // ---------------------------------------------------------------------------- 128 | // Engine Management 129 | // ---------------------------------------------------------------------------- 130 | 131 | /** Get a (persisted) recon engine for current project */ 132 | const getEngine = memoize(() => 133 | getConfig().then( 134 | config => 135 | new Promise(accept => { 136 | const act = vorpal.activeCommand; 137 | act.log('Starting Recon Engine...'); 138 | const createdEngine = createEngine(config); 139 | let hasLoggedDiscovered = false; 140 | let bar; 141 | 142 | createdEngine.subscribe(stats => { 143 | if (!hasLoggedDiscovered && stats.hasDiscovered) { 144 | act.log(`Discovered ${stats.numModules} modules. Parsing...`); 145 | hasLoggedDiscovered = true; 146 | // TODO: right way to do progress bars in vorpal? https://github.com/dthree/vorpal/issues/176 147 | bar = new ProgressBar('parsing [:bar] :percent :etas', { 148 | complete: '=', 149 | incomplete: ' ', 150 | width: 30, 151 | total: stats.numModules, 152 | clear: true, 153 | }); 154 | } 155 | if (bar) { 156 | bar.update( 157 | stats.numReadyModules 158 | ? stats.numReadyModules / stats.numModules 159 | : 0 160 | ); 161 | } 162 | if (stats.canQuery) { 163 | act.log(`Parsed ${stats.numModules} modules.`); 164 | if (stats.numErroredModules) { 165 | act.log(''); 166 | act.log( 167 | chalk.bold( 168 | `Saw ${stats.numErroredModules} errors while parsing:` 169 | ) 170 | ); 171 | map(stats.moduleErrors, (m, i) => 172 | act.log(chalk.red(`${i + 1}. ${m.error.message} <${m.path}>`))); 173 | act.log(''); 174 | } 175 | accept(createdEngine); 176 | } 177 | }); 178 | }) 179 | )); 180 | 181 | // ---------------------------------------------------------------------------- 182 | // Statistics 183 | // ---------------------------------------------------------------------------- 184 | 185 | /** Given recon stats, dump them to the user */ 186 | function logStats(stats, {numRows = 20, debug = false} = {}) { 187 | const act = vorpal.activeCommand; 188 | const SEP = ' | '; 189 | act.log(''); 190 | forEach(stats.filter(s => debug ? true : !s.debug), stat => { 191 | act.log(chalk.bold(`${stat.debug ? '[DEBUG] ' : ''}${stat.title}`)); 192 | act.log(chalk.italic.dim(stat.description)); 193 | act.log(''); 194 | const displayRows = isArray(stat.data) 195 | ? take(stat.data, parseInt(numRows, 10)) 196 | : []; 197 | const colWidths = stat.headers 198 | ? stat.headers.map((h, i) => 199 | max([h.length, ...map(displayRows, l => `${l[i]}`.length)])) 200 | : []; 201 | if (stat.headers) { 202 | act.log( 203 | join( 204 | map(stat.headers, (h, i) => chalk.bold(padEnd(h, colWidths[i]))), 205 | SEP 206 | ) 207 | ); 208 | } 209 | if (isArray(stat.data)) { 210 | forEach(displayRows, l => 211 | act.log(join(map(l, (v, i) => padEnd(`${v}`, colWidths[i])), SEP))); 212 | const numHiddenLines = stat.data.length - displayRows.length; 213 | if (numHiddenLines > 0) { 214 | act.log(''); 215 | act.log(`& ${numHiddenLines} more rows ...`); 216 | } 217 | } else { 218 | act.log(stat.data); 219 | } 220 | act.log(''); 221 | act.log('---'); 222 | act.log(''); 223 | }); 224 | } 225 | 226 | /** Given a "ready to query" engine lets pull some stats */ 227 | function pullStats(engine) { 228 | vorpal.activeCommand.log('Querying and pulling stats...'); 229 | return _pullStats(engine); 230 | } 231 | 232 | vorpal 233 | .command('stats', 'Prints statistics about your React application') 234 | .option('--numRows', 'Max number of rows to display within printed stats') 235 | .option('--debug', 'Output debug stats') 236 | .action(args => 237 | getEngine().then(pullStats).then(s => logStats(s, args.options))); 238 | 239 | // ---------------------------------------------------------------------------- 240 | // Server 241 | // ---------------------------------------------------------------------------- 242 | 243 | /** Current running server */ 244 | let runningServer = null; 245 | 246 | /** Create a new server */ 247 | function spawnServer(engine, opts) { 248 | if (runningServer) { 249 | return runningServer; 250 | } 251 | const act = vorpal.activeCommand; 252 | act.log('Spawning server...'); 253 | return createServer(engine, opts).then(server => { 254 | runningServer = server; 255 | const port = server.address().port; 256 | act.log(''); 257 | act.log( 258 | dedent` 259 | Recon server listening on port ${port}! 260 | Visit ${chalk.bold(`http://localhost:${port}/graphql`)} to play with your data! 261 | ` 262 | ); 263 | act.log(''); 264 | return server; 265 | }); 266 | } 267 | 268 | /** Create a new server */ 269 | function killServer() { 270 | if (!runningServer) { 271 | return; 272 | } 273 | runningServer.close(); 274 | vorpal.activeCommand.log('Server stopped'); 275 | runningServer = null; 276 | } 277 | 278 | vorpal 279 | .command('server start', 'Spawn a server which accepts graphql queries') 280 | .alias('server') 281 | .option('-p --port', 'Port to run the server on') 282 | .action(args => getEngine().then(e => spawnServer(e, args.options))); 283 | 284 | vorpal 285 | .command('server stop', 'Kill the current recon server') 286 | .alias('server kill') 287 | .action(() => killServer()); 288 | 289 | // ---------------------------------------------------------------------------- 290 | // Debug 291 | // ---------------------------------------------------------------------------- 292 | 293 | /** Dump data from engine to disk */ 294 | function dumpDebug(engine, {file = 'recon-dump.json'}) { 295 | return _pullStats(engine).then(stats => { 296 | vorpal.activeCommand.log('Dumping debug information...'); 297 | const data = {stats, store: engine._debug({raw: false})}; 298 | jetpack.write(file, data, {jsonIndent: 0}); 299 | const path = Path.resolve(process.cwd(), file); 300 | vorpal.activeCommand.log(`Dumped successfully to ${path}`); 301 | }); 302 | } 303 | 304 | vorpal 305 | .command('dump', 'Dump debug information') 306 | .option('-f --file', 'File to dump to') 307 | .action(args => getEngine().then(e => dumpDebug(e, args.options))); 308 | 309 | // final setup - either user wants interactive mode or is just running a command 310 | const parsedArgs = vorpal.parse(process.argv, {use: 'minimist'}); 311 | if (!parsedArgs._) { 312 | // TODO: display working project? Ie. recon:my-app$ 313 | vorpal.delimiter('recon$').show(); 314 | } else { 315 | vorpal.parse(parsedArgs._); 316 | } 317 | -------------------------------------------------------------------------------- /packages/recon-engine/src/query/createSchema.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | const { 3 | GraphQLSchema, 4 | GraphQLObjectType, 5 | GraphQLString, 6 | GraphQLList, 7 | } = require('graphql'); 8 | 9 | // TODO: Roll in graphql-relay to make handling connections nicer 10 | 11 | const { 12 | flatten, 13 | map, 14 | groupBy, 15 | values, 16 | find, 17 | last, 18 | memoize, 19 | join, 20 | filter, 21 | flatMap, 22 | isString, 23 | } = require('lodash'); 24 | 25 | const makeDOMComponent = memoize(name => { 26 | // TODO: We should *probably* have resolved these within parse when we see a dom reference? 27 | return { 28 | id: `__REACT_DOM::${name}`, 29 | name, 30 | node: null, 31 | enhancements: [], 32 | props: [], // TODO: Probably a standard definition somewhere of dom attributes? 33 | deps: [], 34 | definedIn: null, 35 | }; 36 | }); 37 | 38 | function createSchema(dataSource, {resolveModulePaths}) { 39 | // TODO: Need to invalidate memoizing as modules are re-parsed (ie. support persisted engine) 40 | 41 | // RESOLUTION --------------------------------------------------------------- 42 | 43 | /** Get data for query/resolution stage */ 44 | const getModules = memoize(() => { 45 | return map(filter(values(dataSource), m => m.ready), m => m.parsed); 46 | }); 47 | 48 | const allComponents = memoize(() => { 49 | return filter(flatten(map(getModules(), m => m.data.components)), c => !!c); 50 | }); 51 | 52 | const getModule = memoize( 53 | paths => { 54 | return find(getModules(), m => find(paths, path => path === m.path)); 55 | }, 56 | p => join(p) 57 | ); 58 | 59 | const resolveSymbol = memoize( 60 | (name, module) => { 61 | const localSymbol = module.data.symbols.find(s => s.name === name); 62 | 63 | if (!localSymbol) { 64 | return { 65 | name, 66 | module, 67 | notFound: true, 68 | }; 69 | } 70 | 71 | if (localSymbol.type.type === 'Identifier') { 72 | return resolveSymbol(localSymbol.type.__node.name, module); 73 | } 74 | 75 | if (localSymbol.type.type === 'ImportSpecifier') { 76 | const nextModule = getModule( 77 | resolveModulePaths(module.path, localSymbol.type.source) 78 | ); 79 | 80 | if (!nextModule) { 81 | return { 82 | name, 83 | module, 84 | notFound: true, 85 | }; 86 | } 87 | 88 | return resolveSymbol( 89 | `export::${localSymbol.type.sourceName}`, 90 | nextModule 91 | ); 92 | } 93 | 94 | if (localSymbol.type.type === 'ImportDefaultSpecifier') { 95 | const nextModule = getModule( 96 | resolveModulePaths(module.path, localSymbol.type.source) 97 | ); 98 | 99 | if (!nextModule) { 100 | return { 101 | name, 102 | module, 103 | notFound: true, 104 | }; 105 | } 106 | 107 | return resolveSymbol('export::default', nextModule); 108 | } 109 | 110 | if (localSymbol.type.type === 'ExportSpecifier') { 111 | const nextModule = getModule( 112 | resolveModulePaths(module.path, localSymbol.type.source) 113 | ); 114 | 115 | if (!nextModule) { 116 | return { 117 | name, 118 | module, 119 | notFound: true, 120 | }; 121 | } 122 | 123 | return resolveSymbol( 124 | `export::${localSymbol.type.sourceName}`, 125 | nextModule 126 | ); 127 | } 128 | 129 | return { 130 | name, 131 | module, 132 | }; 133 | }, 134 | (n, m) => n + m.path 135 | ); 136 | 137 | const getComponentFromResolvedSymbol = memoize( 138 | resolvedSymbol => { 139 | const component = find( 140 | resolvedSymbol.module.data.components, 141 | c => c.name === resolvedSymbol.name 142 | ); 143 | 144 | if (component) { 145 | return component; 146 | } 147 | 148 | const componentPath = find( 149 | resolvedSymbol.module.data.potentialComponentPaths, 150 | cp => cp.name === resolvedSymbol.name 151 | ); 152 | 153 | // absolutely no paths :( 154 | if (!componentPath) { 155 | return null; 156 | } 157 | 158 | // Only taking the *last* potential component here. Really we should be 159 | // able to offer all of them as potential components Ie. branching 160 | const target = last(componentPath.targets); 161 | const resolvedComponent = resolveComponentByName( 162 | target.name, 163 | resolvedSymbol.module 164 | ); 165 | 166 | // Does this break things by being path specific return value? (ie. due to aggressive memoizing) 167 | // Maybe not since within this module the given symbol would always have the same enhancement path. 168 | return Object.assign({}, resolvedComponent, { 169 | pathEnhancements: componentPath.enhancements, 170 | }); 171 | }, 172 | s => s.name + s.module.path 173 | ); 174 | 175 | const resolveComponentByName = memoize( 176 | (name, module) => { 177 | // JSX Convention says if the identifier begins lowercase it is 178 | // a dom node rather than a custom component 179 | if (/^[a-z][a-z0-9]*/.test(name)) { 180 | return makeDOMComponent(name); 181 | } 182 | 183 | const symbol = resolveSymbol(name, module); 184 | 185 | if (symbol.notFound) { 186 | return null; 187 | } 188 | 189 | return getComponentFromResolvedSymbol(symbol) || null; 190 | }, 191 | (n, m) => n + m.path 192 | ); 193 | 194 | const resolveComponent = memoize( 195 | (component, module) => { 196 | // TODO: Need to track/resolve enhancement paths via usage 197 | 198 | const resolvedDeps = map( 199 | values(groupBy(component.deps, 'name')), 200 | usages => { 201 | const resolvedComponent = resolveComponentByName( 202 | usages[0].name, 203 | module 204 | ); 205 | 206 | return { 207 | name: usages[0].name, 208 | component: resolvedComponent, 209 | byComponent: component, 210 | usages: map(usages, u => 211 | Object.assign({}, u, { 212 | component: resolvedComponent, 213 | byComponent: component, 214 | })), 215 | }; 216 | } 217 | ); 218 | 219 | return Object.assign({}, component, { 220 | resolvedDeps, 221 | }); 222 | }, 223 | (c, m) => c.id + m.path 224 | ); 225 | 226 | const allResolvedComponents = memoize(() => { 227 | return flatten( 228 | getModules().map(module => 229 | module.data.components.map(component => 230 | resolveComponent(component, module))) 231 | ); 232 | }); 233 | 234 | const resolveComponentDependants = memoize( 235 | component => { 236 | const all = allResolvedComponents(); 237 | 238 | return flatten( 239 | all 240 | .filter(c => 241 | c.resolvedDeps.find( 242 | depC => depC.component && depC.component.id === component.id 243 | )) 244 | .map(c => 245 | c.resolvedDeps 246 | .filter( 247 | depC => depC.component && depC.component.id === component.id 248 | ) 249 | .map(depC => Object.assign({}, depC, {component: c}))) 250 | ); 251 | }, 252 | c => c.id 253 | ); 254 | 255 | // SCHEMA ------------------------------------------------------------------- 256 | 257 | const moduleType = new GraphQLObjectType({ 258 | name: 'ModuleType', 259 | fields: () => ({ 260 | path: {type: GraphQLString}, 261 | }), 262 | }); 263 | 264 | const propUsageType = new GraphQLObjectType({ 265 | name: 'PropUsageType', 266 | fields: () => ({ 267 | name: {type: GraphQLString}, 268 | valueType: { 269 | type: GraphQLString, 270 | resolve: prop => prop.type.type, 271 | }, 272 | }), 273 | }); 274 | 275 | const componentUsageType = new GraphQLObjectType({ 276 | name: 'ComponentUsageType', 277 | fields: () => ({ 278 | name: {type: GraphQLString}, 279 | component: {type: componentType}, 280 | byComponent: {type: componentType}, 281 | props: {type: new GraphQLList(propUsageType)}, 282 | }), 283 | }); 284 | 285 | const componentDependencyType = new GraphQLObjectType({ 286 | name: 'ComponentDependencyType', 287 | fields: () => ({ 288 | name: {type: GraphQLString}, 289 | component: {type: componentType}, 290 | usages: {type: new GraphQLList(componentUsageType)}, 291 | }), 292 | }); 293 | 294 | const componentEnhancementType = new GraphQLObjectType({ 295 | // TODO: Enhancements need a lot of work in general. need to decide what useful information we can provide 296 | name: 'ComponentEnhancementType', 297 | fields: () => ({ 298 | type: {type: GraphQLString}, 299 | callee: { 300 | type: new GraphQLObjectType({ 301 | name: 'EnhanceCallee', 302 | fields: () => ({ 303 | type: {type: GraphQLString}, 304 | name: {type: GraphQLString}, 305 | }), 306 | }), 307 | }, 308 | }), 309 | }); 310 | 311 | const componentType = new GraphQLObjectType({ 312 | name: 'ComponentType', 313 | fields: () => ({ 314 | id: {type: GraphQLString}, 315 | name: {type: GraphQLString}, 316 | dependencies: { 317 | type: new GraphQLList(componentDependencyType), 318 | resolve: component => { 319 | const all = allResolvedComponents(); 320 | 321 | return find(all, c => c.id === component.id).resolvedDeps; 322 | }, 323 | }, 324 | dependants: { 325 | type: new GraphQLList(componentDependencyType), 326 | resolve: resolveComponentDependants, 327 | }, 328 | usages: { 329 | type: new GraphQLList(componentUsageType), 330 | resolve: component => { 331 | return flatMap( 332 | resolveComponentDependants(component), 333 | dependant => dependant.usages 334 | ); 335 | }, 336 | }, 337 | enhancements: { 338 | type: new GraphQLList(componentEnhancementType), 339 | }, 340 | pathEnhancements: { 341 | description: 'Contextual enhancements', 342 | type: new GraphQLList(componentEnhancementType), 343 | }, 344 | module: { 345 | type: moduleType, 346 | resolve: component => 347 | getModules().find(m => m.path === component.definedIn), 348 | }, 349 | }), 350 | }); 351 | 352 | const schemaType = new GraphQLSchema({ 353 | query: new GraphQLObjectType({ 354 | name: 'RootQueryType', 355 | fields: () => ({ 356 | components: { 357 | type: new GraphQLList(componentType), 358 | args: { 359 | search: { 360 | type: GraphQLString, 361 | }, 362 | }, 363 | resolve(root, {search}) { 364 | const all = allComponents(); 365 | 366 | if (search) { 367 | return all.filter( 368 | c => isString(c.name) && c.name.indexOf(search) > -1 369 | ); 370 | } 371 | 372 | return all; 373 | }, 374 | }, 375 | component: { 376 | type: componentType, 377 | args: { 378 | name: { 379 | type: GraphQLString, 380 | }, 381 | id: { 382 | type: GraphQLString, 383 | }, 384 | }, 385 | resolve(root, {name, id}) { 386 | const all = allComponents(); 387 | 388 | if (id) { 389 | return all.find(c => c.id === id) || null; 390 | } 391 | 392 | return all.find(c => c.name === name) || null; 393 | }, 394 | }, 395 | modules: { 396 | type: new GraphQLList(moduleType), 397 | resolve() { 398 | return getModules(); 399 | }, 400 | }, 401 | module: { 402 | type: moduleType, 403 | args: { 404 | path: { 405 | type: GraphQLString, 406 | }, 407 | }, 408 | resolve(root, {path}) { 409 | const all = getModules(); 410 | 411 | return all.find(c => c.path === path) || null; 412 | }, 413 | }, 414 | numComponents: { 415 | // TODO: move into better "components" shape 416 | type: GraphQLString, 417 | resolve() { 418 | return allComponents().length; 419 | }, 420 | }, 421 | }), 422 | }), 423 | }); 424 | 425 | return schemaType; 426 | } 427 | 428 | module.exports = createSchema; 429 | -------------------------------------------------------------------------------- /packages/recon-engine/src/parse/__tests__/__fixtures__/real-world-lystable/src.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react'; 3 | import PropTypes from 'utils/prop-types'; 4 | import {compose, withReducer, branch} from 'recompose'; 5 | import {identity} from 'lodash'; 6 | import {isRecruitmentManager} from 'utils/talent'; 7 | import {TALENT_REQUEST_STATES} from 'constants/talent'; 8 | import {TalentRequest, Agency, AgencyTalentRequest} from 'api/records'; 9 | import {createContainer, query as q} from 'api/recall'; 10 | import DateRange from 'moment-range'; 11 | import {PRIVACY_MODES} from 'constants/notes'; 12 | import {matchRecord} from 'utils/record'; 13 | import {Record} from 'immutable'; 14 | import withStaticProperties from 'decorators/with-static-properties'; 15 | import notFound from 'decorators/not-found'; 16 | 17 | import {Box, Flex} from 'components/layout'; 18 | import Paper from 'components/paper'; 19 | import LoadingSpinner from 'components/loading-spinner'; 20 | import SectionHeader from 'components/section-header'; 21 | import CardHeader from 'components/card-header'; 22 | import Link from 'components/link'; 23 | import Text from 'components/text'; 24 | import Icon from 'components/icon'; 25 | import CandidateList from 'components/candidate-list'; 26 | import MenuChecklist from 'components/menu-checklist'; 27 | import ContextualMenuButton from 'components/contextual-menu-button'; 28 | import MenuItemWithConfirm from 'components/menu-item-with-confirm'; 29 | import TalentRequestInformation from 'components/talent-request-information'; 30 | import NotesCard from 'components/notes-card'; 31 | import Avatar from 'components/avatar'; 32 | import Button from 'components/button'; 33 | import Modal from 'components/modal'; 34 | import TalentRequestForm from 'hera/components/talent-request-form'; 35 | 36 | import withViewer from 'decorators/with-viewer'; 37 | import withActions from 'decorators/with-actions'; 38 | import { 39 | updateTalentRequest, 40 | cancelTalentRequest, 41 | setFinalCandidate, 42 | removeCandidate, 43 | createAgencyTalentRequest, 44 | saveTalentRequest, 45 | } from 'actions/talent-request-actions'; 46 | 47 | // State 48 | 49 | export const State = new Record({ 50 | editing: false, 51 | }); 52 | 53 | // Actions 54 | 55 | const TOGGLE_EDIT_FORM = 'TOGGLE_EDIT_FORM'; 56 | const CLOSE_EDIT_FORM = 'CLOSE_EDIT_FORM'; 57 | 58 | export const reducer = (state, action) => { 59 | switch (action.type) { 60 | case TOGGLE_EDIT_FORM: 61 | return state.merge({editing: !state.editing}); 62 | 63 | case CLOSE_EDIT_FORM: 64 | return state.merge({editing: false}); 65 | 66 | default: 67 | return state; 68 | } 69 | }; 70 | 71 | // View 72 | 73 | /* Render talent reqiest updates */ 74 | export function UpdatesCard( 75 | { 76 | talentRequest, 77 | ...otherProps 78 | } 79 | ) { 80 | return ( 81 | 82 | 83 | 84 | 85 | {!!talentRequest.final_candidate 86 | ? 87 | 88 | 92 | 93 | 94 | Selected Candidate 95 | {talentRequest.final_candidate.supplier.name} 96 | 97 | 98 | : null} 99 | 100 | 101 | 102 | 103 | {talentRequest.candidates.size} 104 | 105 | 106 | Candidates 107 | 108 | 109 | 110 | 111 | 112 | 113 | {talentRequest.agencies.size} 114 | 115 | 116 | Agencies 117 | 118 | 119 | 120 | 121 | ); 122 | } 123 | 124 | export function AgencyChecklist( 125 | { 126 | talentRequest, 127 | agencies, 128 | onCreated, 129 | createAgencyTalentRequestAction, 130 | } 131 | ) { 132 | const items = agencies.map(agency => { 133 | // Check if the agency is already attached 134 | const existingAgency = talentRequest.agencies.find( 135 | _agency => !!_agency && matchRecord(agency, _agency) 136 | ); 137 | 138 | return { 139 | checked: !!existingAgency, 140 | label: agency.name, 141 | checkAction: () => { 142 | createAgencyTalentRequestAction( 143 | new AgencyTalentRequest({ 144 | talent_request: talentRequest, 145 | agency, 146 | }) 147 | ).then(() => { 148 | onCreated(); 149 | }); 150 | }, 151 | }; 152 | }); 153 | 154 | return ( 155 | 160 | 161 | Add to Talent Request 162 | 163 | 164 | ); 165 | } 166 | 167 | const Z_INDEX = { 168 | MODAL: 200, 169 | }; 170 | 171 | /** 172 | * Display a talent request in detail 173 | */ 174 | export const TalentRequestsDetailPage = withStaticProperties({ 175 | propTypes: { 176 | talentRequest: PropTypes.record(TalentRequest), 177 | agencies: PropTypes.iterableOf(PropTypes.record(Agency)), 178 | recall: PropTypes.shape({ 179 | markAsStale: PropTypes.func, 180 | }).isRequired, 181 | queries: PropTypes.shape({ 182 | agencies: PropTypes.shape({ 183 | id: PropTypes.string, 184 | ready: PropTypes.bool, 185 | }), 186 | talentRequest: PropTypes.shape({ 187 | id: PropTypes.string, 188 | ready: PropTypes.bool, 189 | }), 190 | }).isRequired, 191 | viewer: PropTypes.viewer.isRequired, 192 | updateTalentRequest: PropTypes.func.isRequired, 193 | cancelTalentRequest: PropTypes.func.isRequired, 194 | setFinalCandidate: PropTypes.func.isRequired, 195 | removeCandidate: PropTypes.func.isRequired, 196 | createAgencyTalentRequest: PropTypes.func.isRequired, 197 | saveTalentRequest: PropTypes.func.isRequired, 198 | state: PropTypes.shape({ 199 | editing: PropTypes.bool.isRequired, 200 | }).isRequired, 201 | dispatch: PropTypes.func.isRequired, 202 | }, 203 | })(function TalentRequestsDetailPage(props) { 204 | const { 205 | talentRequest, 206 | agencies, 207 | queries, 208 | viewer, 209 | // withReducer 210 | state: {editing}, 211 | dispatch, 212 | } = props; 213 | 214 | const handleContainerRefresh = () => { 215 | props.recall.markAsStale([ 216 | props.queries.agencies.id, 217 | props.queries.talentRequest.id, 218 | ]); 219 | }; 220 | 221 | const fire = action => () => dispatch(action); 222 | 223 | const status = !!talentRequest 224 | ? TALENT_REQUEST_STATES[talentRequest.status] 225 | : null; 226 | 227 | const searchRange = !!talentRequest && 228 | !!talentRequest.start_date && 229 | !!talentRequest.end_date 230 | ? new DateRange(talentRequest.start_date, talentRequest.end_date) 231 | : null; 232 | 233 | const heading = ( 234 |
    235 | {isRecruitmentManager(viewer) 236 | ? 237 | Talent Requests 238 | 239 | : 240 | My Requests 241 | } 242 | chevron_right 243 | {!!talentRequest ? talentRequest.job_title : 'Loading'} 244 |
    245 | ); 246 | 247 | const menu = ( 248 | 249 | 255 | props.cancelTalentRequest(talentRequest)} 259 | key="cancel" 260 | > 261 | Cancel Request 262 | 263 | 264 | ); 265 | 266 | const editTalentRequest = [ 267 |