├── extension ├── chrome │ ├── extension │ │ ├── panel.js │ │ ├── background.js │ │ ├── reselect-tools-app.css │ │ ├── page-api.js │ │ ├── devtools.js │ │ └── reselect-tools-app.js │ ├── assets │ │ └── img │ │ │ ├── icon-128.png │ │ │ ├── icon-16.png │ │ │ ├── icon-32.png │ │ │ ├── icon-48.png │ │ │ ├── icon-128-disabled.png │ │ │ ├── icon-16-disabled.png │ │ │ └── icon-48-disabled.png │ ├── views │ │ ├── background.pug │ │ ├── panel.pug │ │ └── devtools.pug │ ├── manifest.prod.json │ └── manifest.dev.json ├── test │ ├── mocha.opts │ ├── .eslintrc │ ├── setup-app.js │ ├── func.js │ ├── app │ │ ├── actions │ │ │ └── todos.spec.js │ │ ├── components │ │ │ ├── Header.spec.js │ │ │ ├── TodoTextInput.spec.js │ │ │ ├── Footer.spec.js │ │ │ ├── TodoItem.spec.js │ │ │ └── MainSection.spec.js │ │ └── reducers │ │ │ └── todos.spec.js │ └── e2e │ │ ├── injectpage.js │ │ └── todoapp.js ├── webpack │ ├── postcss.config.js │ ├── .eslintrc │ ├── test.config.js │ ├── customPublicPath.js │ ├── prod.config.js │ ├── replace │ │ ├── JsonpMainTemplate.runtime.js │ │ └── process-update.js │ └── dev.config.js ├── .gitignore ├── Screen Shot 2017-11-15 at 3.34.11 AM.png ├── .babelrc ├── app │ ├── reducers │ │ ├── index.js │ │ └── graph.js │ ├── constants │ │ ├── TodoFilters.js │ │ └── ActionTypes.js │ ├── store │ │ ├── configureStore.js │ │ ├── configureStore.prod.js │ │ └── configureStore.dev.js │ ├── components │ │ ├── SelectorState.css │ │ ├── TodoTextInput.css │ │ ├── SelectorSearch.css │ │ ├── StateTree.js │ │ ├── TodoTextInput.js │ │ ├── Footer.css │ │ ├── SelectorState.js │ │ ├── Dock.js │ │ ├── TodoItem.js │ │ ├── SelectorInspector.js │ │ ├── TodoItem.css │ │ ├── MainSection.js │ │ ├── Footer.js │ │ ├── SelectorSearch.js │ │ ├── Header.js │ │ ├── MainSection.css │ │ └── SelectorGraph.js │ ├── containers │ │ ├── Root.js │ │ └── App.js │ ├── utils │ │ ├── storage.js │ │ ├── rpc.js │ │ └── apiMiddleware.js │ └── actions │ │ └── graph.js ├── index.html ├── scripts │ ├── .eslintrc │ ├── build.js │ ├── dev.js │ ├── tasks.js │ └── compress.js ├── appveyor.yml ├── .eslintrc ├── .travis.yml ├── LICENSE ├── TODO.md ├── package.json └── README.md ├── examples ├── graph.png ├── extension.png ├── demo.html ├── your-app.html ├── demo-app.js ├── your-app.js └── simple-graph.js ├── CREDITS.md ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── .babelrc ├── .eslintrc ├── LICENSE ├── package.json ├── src └── index.js ├── lib └── index.js ├── README.md └── test └── test.js /extension/chrome/extension/panel.js: -------------------------------------------------------------------------------- 1 | import './reselect-tools-app'; 2 | 3 | -------------------------------------------------------------------------------- /extension/test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | --recursive 3 | -------------------------------------------------------------------------------- /extension/webpack/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | ] 4 | }; 5 | -------------------------------------------------------------------------------- /examples/graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/examples/graph.png -------------------------------------------------------------------------------- /examples/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/examples/extension.png -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # CREDITS 2 | 3 | * Thanks to all commenters on the proposal [here](https://github.com/reactjs/reselect/issues/279) 4 | -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-128.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-16.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-32.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-48.png -------------------------------------------------------------------------------- /extension/webpack/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | 5 | build/ 6 | dev/ 7 | 8 | *.zip 9 | *.crx 10 | *.pem 11 | update.xml 12 | -------------------------------------------------------------------------------- /extension/Screen Shot 2017-11-15 at 3.34.11 AM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/Screen Shot 2017-11-15 at 3.34.11 AM.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-128-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-128-disabled.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-16-disabled.png -------------------------------------------------------------------------------- /extension/chrome/assets/img/icon-48-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skortchmark9/reselect-tools/HEAD/extension/chrome/assets/img/icon-48-disabled.png -------------------------------------------------------------------------------- /extension/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["add-module-exports", "transform-decorators-legacy", "transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /extension/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import graph from './graph'; 3 | 4 | export default combineReducers({ 5 | graph 6 | }); 7 | -------------------------------------------------------------------------------- /extension/app/constants/TodoFilters.js: -------------------------------------------------------------------------------- 1 | export const SHOW_ALL = 'show_all'; 2 | export const SHOW_COMPLETED = 'show_completed'; 3 | export const SHOW_ACTIVE = 'show_active'; 4 | -------------------------------------------------------------------------------- /extension/chrome/extension/background.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction.onClicked.addListener(() => { 2 | window.open('https://github.com/skortchmark9/reselect-devtools-extension'); 3 | }); 4 | -------------------------------------------------------------------------------- /extension/chrome/views/background.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | script(src=env == 'prod' ? '/js/background.bundle.js' : 'http://localhost:3000/js/background.bundle.js') 6 | -------------------------------------------------------------------------------- /extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /extension/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "shelljs": true 4 | }, 5 | "rules": { 6 | "no-console": 0, 7 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /extension/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": 0, 7 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /extension/app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | // Can't do dynamic imports with es6 import 2 | if (process.env.NODE_ENV === 'production') { 3 | module.exports = require('./configureStore.prod'); 4 | } else { 5 | module.exports = require('./configureStore.dev'); 6 | } 7 | -------------------------------------------------------------------------------- /extension/webpack/test.config.js: -------------------------------------------------------------------------------- 1 | // for babel-plugin-webpack-loaders 2 | const config = require('./prod.config'); 3 | 4 | module.exports = { 5 | output: { 6 | libraryTarget: 'commonjs2' 7 | }, 8 | module: { 9 | loaders: config.module.loaders.slice(1) // remove babel-loader 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | dist 5 | es 6 | .vscode 7 | .idea 8 | typescript_test/should_compile/index.js 9 | typescript_test/should_not_compile/index.js 10 | typescript_test/common.js 11 | flow_test/should_fail/flow-typed/index.js.flow 12 | flow_test/should_pass/flow-typed/index.js.flow 13 | .DS_STORE 14 | -------------------------------------------------------------------------------- /extension/scripts/build.js: -------------------------------------------------------------------------------- 1 | const tasks = require('./tasks'); 2 | 3 | tasks.replaceWebpack(); 4 | console.log('[Copy assets]'); 5 | console.log('-'.repeat(80)); 6 | tasks.copyAssets('build'); 7 | 8 | console.log('[Webpack Build]'); 9 | console.log('-'.repeat(80)); 10 | exec('webpack --config webpack/prod.config.js --progress --profile --colors'); 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [v0.0.7] - 2018/11/04 7 | 8 | ### New Features 9 | 10 | Removed need for createSelectorWithDependencies! (#6) 11 | Exception handling in the selectorGraph() (#10) 12 | -------------------------------------------------------------------------------- /extension/app/components/SelectorState.css: -------------------------------------------------------------------------------- 1 | td { 2 | vertical-align: baseline; 3 | padding: 5px; 4 | padding-left: 0; 5 | border-bottom: 1px solid rgb(79, 90, 101); 6 | } 7 | 8 | tr { 9 | min-height: 20px; 10 | } 11 | 12 | section tr + tr { 13 | border-bottom: 1px solid rgb(79, 90, 101); 14 | } 15 | 16 | section h5 { 17 | border-bottom: 1px solid white; 18 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "5" 5 | - "6" 6 | - "7" 7 | script: 8 | - npm run lint 9 | - npm test 10 | - npm run test:cov 11 | - npm run compile 12 | 13 | after_success: 14 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 15 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 16 | -------------------------------------------------------------------------------- /extension/chrome/extension/reselect-tools-app.css: -------------------------------------------------------------------------------- 1 | html, body, body > div { 2 | height: 100%; 3 | width: 100%; 4 | } 5 | body { 6 | overflow: hidden; 7 | min-width: 350px; 8 | margin: 0; 9 | padding: 0; 10 | font-family: "Helvetica Neue", "Lucida Grande", sans-serif; 11 | font-size: 11px; 12 | background-color: rgb(0, 43, 54); 13 | color: #fff; 14 | } 15 | a { 16 | color: #fff; 17 | } 18 | -------------------------------------------------------------------------------- /extension/chrome/views/panel.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | meta(charset='UTF-8') 6 | title Redux TodoMVC Example (Panel) 7 | body 8 | #root 9 | if env !== 'prod' 10 | script(src='chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.js') 11 | script(src=env == 'prod' ? '/js/panel.bundle.js' : 'http://localhost:3000/js/panel.bundle.js') 12 | -------------------------------------------------------------------------------- /extension/app/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | 6 | export default function (initialState, ...middlewares) { 7 | const enhancer = compose( 8 | applyMiddleware(thunk, ...middlewares), 9 | ); 10 | 11 | return createStore(rootReducer, initialState, enhancer); 12 | } 13 | -------------------------------------------------------------------------------- /extension/appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '6' 4 | - nodejs_version: '7' 5 | 6 | cache: 7 | - "%LOCALAPPDATA%/Yarn" 8 | - node_modules 9 | 10 | install: 11 | - ps: Install-Product node $env:nodejs_version 12 | - yarn install 13 | 14 | test_script: 15 | - node --version 16 | - yarn --version 17 | - yarn run lint 18 | - yarn test 19 | - yarn run build 20 | 21 | build: off 22 | -------------------------------------------------------------------------------- /extension/test/setup-app.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom'; 2 | import hook from 'css-modules-require-hook'; 3 | import postCSSConfig from '../webpack/postcss.config'; 4 | 5 | global.document = jsdom(''); 6 | global.window = document.defaultView; 7 | global.navigator = global.window.navigator; 8 | 9 | hook({ 10 | generateScopedName: '[name]__[local]___[hash:base64:5]', 11 | prepend: postCSSConfig.plugins, 12 | }); 13 | -------------------------------------------------------------------------------- /extension/app/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import App from './App'; 4 | 5 | export default class Root extends Component { 6 | 7 | static propTypes = { 8 | store: PropTypes.object.isRequired 9 | }; 10 | 11 | render() { 12 | const { store } = this.props; 13 | return ( 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /extension/chrome/views/devtools.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | meta(charset='UTF-8') 6 | title Reselect Devtools 7 | style. 8 | html, body { 9 | background-color: rgba(0,0,0,0) 10 | } 11 | body 12 | #root 13 | if env !== 'prod' 14 | script(src='chrome-extension://lmhkpmbekcpmknklioeibfkpmmfibljd/js/redux-devtools-extension.js') 15 | script(src=env == 'prod' ? '/js/devtools.bundle.js' : 'http://localhost:3000/js/devtools.bundle.js') 16 | -------------------------------------------------------------------------------- /extension/app/components/TodoTextInput.css: -------------------------------------------------------------------------------- 1 | .new, 2 | .edit { 3 | position: relative; 4 | margin: 0; 5 | width: 100%; 6 | font-size: 24px; 7 | font-family: inherit; 8 | font-weight: inherit; 9 | line-height: 1.4em; 10 | border: 0; 11 | outline: none; 12 | color: inherit; 13 | padding: 6px; 14 | border: 1px solid #999; 15 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 16 | box-sizing: border-box; 17 | font-smoothing: antialiased; 18 | } 19 | 20 | .new { 21 | padding: 16px 16px 16px 60px; 22 | border: none; 23 | background: rgba(0, 0, 0, 0.003); 24 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 25 | } 26 | -------------------------------------------------------------------------------- /extension/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "globals": { 5 | "chrome": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "rules": { 12 | "react/forbid-prop-types": 0, 13 | "react/prefer-stateless-function": 0, 14 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 15 | "jsx-a11y/no-static-element-interactions": 0, 16 | "jsx-a11y/label-has-for": 0, 17 | "consistent-return": 0, 18 | "comma-dangle": 0, 19 | "spaced-comment": 0, 20 | "global-require": 0 21 | }, 22 | "plugins": [ 23 | "react" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | ], 5 | "env": { 6 | "commonjs": { 7 | "plugins": [ 8 | ["transform-es2015-modules-commonjs", { "loose": true }] 9 | ] 10 | }, 11 | "umd": { 12 | "plugins": [ 13 | ["transform-es2015-modules-umd", 14 | { 15 | "loose": true, 16 | "globals": { 17 | 'reselect': 'Reselect' 18 | } 19 | }] 20 | ], 21 | "moduleId": "ReselectTools" 22 | }, 23 | "test": { 24 | "plugins": [ 25 | ["transform-es2015-modules-commonjs", { "loose": true }], 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /extension/webpack/customPublicPath.js: -------------------------------------------------------------------------------- 1 | /* global __webpack_public_path__ __HOST__ __PORT__ */ 2 | /* eslint no-global-assign: 0 camelcase: 0 */ 3 | 4 | if (process.env.NODE_ENV === 'production') { 5 | __webpack_public_path__ = chrome.extension.getURL('/js/'); 6 | } else { 7 | // In development mode, 8 | // the iframe of injectpage cannot get correct path, 9 | // it need to get parent page protocol. 10 | const path = `//${__HOST__}:${__PORT__}/js/`; 11 | if (location.protocol === 'https:' || location.search.indexOf('protocol=https') !== -1) { 12 | __webpack_public_path__ = `https:${path}`; 13 | } else { 14 | __webpack_public_path__ = `http:${path}`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extension/app/utils/storage.js: -------------------------------------------------------------------------------- 1 | function saveState(state) { 2 | chrome.storage.local.set({ state: JSON.stringify(state) }); 3 | } 4 | 5 | // todos unmarked count 6 | function setBadge(todos) { 7 | if (chrome.browserAction) { 8 | const count = todos.filter(todo => !todo.marked).length; 9 | chrome.browserAction.setBadgeText({ text: count > 0 ? count.toString() : '' }); 10 | } 11 | } 12 | 13 | export default function () { 14 | return next => (reducer, initialState) => { 15 | const store = next(reducer, initialState); 16 | store.subscribe(() => { 17 | const state = store.getState(); 18 | saveState(state); 19 | setBadge(state.todos); 20 | }); 21 | return store; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /extension/scripts/dev.js: -------------------------------------------------------------------------------- 1 | const tasks = require('./tasks'); 2 | const createWebpackServer = require('webpack-httpolyglot-server'); 3 | const devConfig = require('../webpack/dev.config'); 4 | 5 | tasks.replaceWebpack(); 6 | console.log('[Copy assets]'); 7 | console.log('-'.repeat(80)); 8 | tasks.copyAssets('dev'); 9 | 10 | console.log('[Webpack Dev]'); 11 | console.log('-'.repeat(80)); 12 | console.log('If you\'re developing Inject page,'); 13 | console.log('please allow `https://localhost:3000` connections in Google Chrome,'); 14 | console.log('and load unpacked extensions with `./dev` folder. (see https://developer.chrome.com/extensions/getstarted#unpacked)\n'); 15 | createWebpackServer(devConfig, { 16 | host: 'localhost', 17 | port: 3000 18 | }); 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "mocha": true, 6 | "es6": true, 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "array-bracket-spacing": [2, "always"], 14 | "eol-last": 2, 15 | "indent": [2, 2, { 16 | "SwitchCase": 1 17 | }], 18 | "no-multiple-empty-lines": 2, 19 | "object-curly-spacing": [2, "always"], 20 | "quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 21 | "semi": [2, "never"], 22 | "strict": 0, 23 | "space-before-blocks": [2, "always"], 24 | "space-before-function-paren": [2, {"anonymous":"always","named":"never"}] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /extension/app/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_TODO = 'ADD_TODO'; 2 | export const DELETE_TODO = 'DELETE_TODO'; 3 | export const EDIT_TODO = 'EDIT_TODO'; 4 | export const COMPLETE_TODO = 'COMPLETE_TODO'; 5 | export const COMPLETE_ALL = 'COMPLETE_ALL'; 6 | export const CLEAR_COMPLETED = 'CLEAR_COMPLETED'; 7 | 8 | export const CHECK_SELECTOR = 'CHECK_SELECTOR'; 9 | export const CHECK_SELECTOR_FAILED = 'CHECK_SELECTOR_FAILED'; 10 | export const CHECK_SELECTOR_SUCCESS = 'CHECK_SELECTOR_SUCCESS'; 11 | export const UNCHECK_SELECTOR = 'UNCHECK_SELECTOR'; 12 | 13 | 14 | export const GET_SELECTOR_GRAPH = 'GET_SELECTOR_GRAPH'; 15 | export const GET_SELECTOR_GRAPH_FAILED = 'GET_SELECTOR_GRAPH_FAILED'; 16 | export const GET_SELECTOR_GRAPH_SUCCESS = 'GET_SELECTOR_GRAPH_SUCCESS'; 17 | -------------------------------------------------------------------------------- /extension/scripts/tasks.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | exports.replaceWebpack = () => { 4 | const replaceTasks = [{ 5 | from: 'webpack/replace/JsonpMainTemplate.runtime.js', 6 | to: 'node_modules/webpack/lib/JsonpMainTemplate.runtime.js' 7 | }, { 8 | from: 'webpack/replace/process-update.js', 9 | to: 'node_modules/webpack-hot-middleware/process-update.js' 10 | }]; 11 | 12 | replaceTasks.forEach(task => cp(task.from, task.to)); 13 | }; 14 | 15 | exports.copyAssets = (type) => { 16 | const env = type === 'build' ? 'prod' : type; 17 | rm('-rf', type); 18 | mkdir(type); 19 | cp(`chrome/manifest.${env}.json`, `${type}/manifest.json`); 20 | cp('-R', 'chrome/assets/*', type); 21 | exec(`pug -O "{ env: '${env}' }" -o ${type} chrome/views/`); 22 | }; 23 | -------------------------------------------------------------------------------- /extension/app/components/SelectorSearch.css: -------------------------------------------------------------------------------- 1 | .searchContainer { 2 | flex-shrink: 0; 3 | display: flex; 4 | align-items: center; 5 | flex-grow: 1; 6 | max-width: 800px; 7 | margin-left: auto; 8 | justify-content: flex-end; 9 | } 10 | 11 | .searchContainer > a { 12 | flex-grow: 0 !important; 13 | margin-left: 1em !important; 14 | } 15 | 16 | .searchContainer .autocomplete { 17 | flex-grow: 1; 18 | display: flex; 19 | z-index: 4; 20 | } 21 | 22 | .searchContainer .autocomplete input { 23 | min-width: 200px; 24 | flex-grow: 1; 25 | border: 0; 26 | padding: 0.3em; 27 | outline: none; 28 | background: none; 29 | color: white; 30 | border-bottom: 2px solid rgb(79, 90, 101); 31 | } 32 | 33 | .searchContainer .autocomplete input:focus { 34 | border-color: #e8eaf7 35 | } -------------------------------------------------------------------------------- /extension/app/components/StateTree.js: -------------------------------------------------------------------------------- 1 | import JSONTree from 'react-json-tree'; 2 | import React, { PropTypes } from 'react'; 3 | 4 | const shouldExpandNode = (keyName, data, level) => false; 5 | 6 | const isObject = o => typeof o === 'object'; 7 | 8 | const valueStyle = { 9 | marginLeft: '0.875em', 10 | paddingLeft: '1.25em', 11 | paddingTop: '0.25em', 12 | }; 13 | 14 | const StateTree = ({ data, style = {} }) => ( 15 |
16 | { isObject(data) ? 17 | :
{ "" + data }
21 | } 22 |
23 | ); 24 | 25 | StateTree.propTypes = { 26 | data: PropTypes.any, 27 | style: PropTypes.object, 28 | }; 29 | 30 | export default StateTree; 31 | -------------------------------------------------------------------------------- /extension/test/func.js: -------------------------------------------------------------------------------- 1 | import chromedriver from 'chromedriver'; 2 | import webdriver from 'selenium-webdriver'; 3 | 4 | export function delay(time) { 5 | return new Promise(resolve => setTimeout(resolve, time)); 6 | } 7 | 8 | let crdvIsStarted = false; 9 | export function startChromeDriver() { 10 | if (crdvIsStarted) return Promise.resolve(); 11 | chromedriver.start(); 12 | process.on('exit', chromedriver.stop); 13 | crdvIsStarted = true; 14 | return delay(1000); 15 | } 16 | 17 | export function buildWebDriver(extPath) { 18 | return new webdriver.Builder() 19 | .usingServer('http://localhost:9515') 20 | .withCapabilities({ 21 | chromeOptions: { 22 | args: [`load-extension=${extPath}`] 23 | } 24 | }) 25 | .forBrowser('chrome') 26 | .build(); 27 | } 28 | -------------------------------------------------------------------------------- /examples/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A Demo App Example 4 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/your-app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example Graph: (just add json) 4 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /extension/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - "6" 6 | - "7" 7 | cache: 8 | directories: 9 | - $HOME/.yarn-cache 10 | - node_modules 11 | env: 12 | - CXX=g++-4.8 13 | addons: 14 | apt: 15 | sources: 16 | - google-chrome 17 | - ubuntu-toolchain-r-test 18 | packages: 19 | - google-chrome-stable 20 | - g++-4.8 21 | 22 | install: 23 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 24 | - npm install -g yarn 25 | - yarn install 26 | 27 | before_script: 28 | - export DISPLAY=:99.0 29 | - sh -e /etc/init.d/xvfb start & 30 | - sleep 3 31 | 32 | script: 33 | - yarn run lint 34 | - yarn test 35 | - yarn run build 36 | - yarn run test-e2e 37 | -------------------------------------------------------------------------------- /extension/chrome/extension/page-api.js: -------------------------------------------------------------------------------- 1 | function evalPromise(str) { 2 | return new Promise((resolve, reject) => { 3 | chrome.devtools.inspectedWindow.eval(str, (resultStr, err) => { 4 | const result = JSON.parse(resultStr); 5 | if (err && err.isException) { 6 | console.error(err.value); 7 | reject(err.value); 8 | } else { 9 | resolve(result); 10 | } 11 | }); 12 | }); 13 | } 14 | 15 | export function checkSelector(id) { 16 | const str = `(function() { 17 | const __reselect_last_check = window.__RESELECT_TOOLS__.checkSelector('${id}'); 18 | console.log(__reselect_last_check); 19 | return JSON.stringify(__reselect_last_check); 20 | })()`; 21 | return evalPromise(str); 22 | } 23 | 24 | export function selectorGraph() { 25 | const str = 'JSON.stringify(window.__RESELECT_TOOLS__.selectorGraph())'; 26 | return evalPromise(str); 27 | } 28 | -------------------------------------------------------------------------------- /extension/chrome/manifest.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "page": "background.html" 4 | }, 5 | "browser_action": { 6 | "default_icon": { 7 | "128": "img/icon-128.png", 8 | "16": "img/icon-16.png", 9 | "32": "img/icon-32.png", 10 | "48": "img/icon-48.png" 11 | }, 12 | "default_title": "Reselect Devtools" 13 | }, 14 | "content_security_policy": "default-src 'self'; script-src 'self'; style-src * 'unsafe-inline'; img-src 'self' data:;", 15 | "description": "Reselect Devtools", 16 | "devtools_page": "devtools.html", 17 | "icons": { 18 | "128": "img/icon-128.png", 19 | "16": "img/icon-16.png", 20 | "32": "img/icon-32.png", 21 | "48": "img/icon-48.png" 22 | }, 23 | "manifest_version": 2, 24 | "name": "Reselect Devtools", 25 | "permissions": [ 26 | "tabs", 27 | "storage", 28 | "" 29 | ], 30 | "version": "0.0.2" 31 | } -------------------------------------------------------------------------------- /extension/app/actions/graph.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/ActionTypes'; 2 | 3 | export function uncheckSelector() { 4 | return { type: types.UNCHECK_SELECTOR }; 5 | } 6 | 7 | export function checkSelectorFailed(selector) { 8 | return { type: types.CHECK_SELECTOR_FAILED, payload: { selector } }; 9 | } 10 | 11 | export function checkSelectorSuccess(selector) { 12 | return { type: types.CHECK_SELECTOR_SUCCESS, payload: { selector } }; 13 | } 14 | 15 | export function checkSelector(selector) { 16 | return { type: types.CHECK_SELECTOR, payload: { selector } }; 17 | } 18 | 19 | 20 | export function getSelectorGraphFailed() { 21 | return { type: types.GET_SELECTOR_GRAPH_FAILED }; 22 | } 23 | 24 | export function getSelectorGraphSuccess(graph) { 25 | return { type: types.GET_SELECTOR_GRAPH_SUCCESS, payload: { graph } }; 26 | } 27 | 28 | export function getSelectorGraph() { 29 | return { type: types.GET_SELECTOR_GRAPH }; 30 | } 31 | -------------------------------------------------------------------------------- /extension/app/utils/rpc.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 2 | const where = sender.tab ? 'a content script' : 'the extension'; 3 | const message = `extension received a message from ${where}`; 4 | console.log(message); 5 | sendResponse({ k: true }); 6 | }); 7 | 8 | 9 | function sendMessage(data) { 10 | console.log(chrome.windows.getCurrent((x) => console.log(x))); 11 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 12 | console.log('sending', data, tabs); 13 | chrome.tabs.sendMessage(tabs[0].id, data, function(response) { 14 | console.log(response); 15 | }); 16 | }); 17 | } 18 | 19 | export default (store) => (next) => (action) => { 20 | switch (action.type) { 21 | case 'ADD_TODO': 22 | sendMessage(action.text); 23 | default: 24 | break; 25 | 26 | } 27 | return next(action); 28 | }; 29 | 30 | 31 | -------------------------------------------------------------------------------- /extension/chrome/extension/devtools.js: -------------------------------------------------------------------------------- 1 | let panelCreated = false; 2 | let loadCheckInterval; 3 | 4 | const checkForDevtools = cb => chrome.devtools.inspectedWindow.eval('!!(Object.keys(window.__RESELECT_TOOLS__ || {}).length)', cb); 5 | 6 | 7 | function onCheck(pageHasDevtools) { 8 | if (!pageHasDevtools || panelCreated) { 9 | return; 10 | } 11 | 12 | clearInterval(loadCheckInterval); 13 | panelCreated = true; 14 | chrome.devtools.panels.create('Reselect', '', 'panel.html'); 15 | } 16 | 17 | function createPanelIfDevtoolsLoaded() { 18 | if (panelCreated) return; 19 | checkForDevtools(onCheck); 20 | } 21 | 22 | chrome.devtools.network.onNavigated.addListener(createPanelIfDevtoolsLoaded); 23 | 24 | // Check to see if Reselect Tools have loaded once per second in case Reselect tools were added 25 | // after page load 26 | loadCheckInterval = setInterval(createPanelIfDevtoolsLoaded, 1000); 27 | 28 | createPanelIfDevtoolsLoaded(); 29 | -------------------------------------------------------------------------------- /extension/scripts/compress.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const ChromeExtension = require('crx'); 3 | /* eslint import/no-unresolved: 0 */ 4 | const name = require('../build/manifest.json').name; 5 | const argv = require('minimist')(process.argv.slice(2)); 6 | 7 | const keyPath = argv.key || 'key.pem'; 8 | const existsKey = fs.existsSync(keyPath); 9 | const crx = new ChromeExtension({ 10 | appId: argv['app-id'], 11 | codebase: argv.codebase, 12 | privateKey: existsKey ? fs.readFileSync(keyPath) : null 13 | }); 14 | 15 | crx.load('build') 16 | .then(() => crx.loadContents()) 17 | .then((archiveBuffer) => { 18 | fs.writeFile(`${name}.zip`, archiveBuffer); 19 | 20 | if (!argv.codebase || !existsKey) return; 21 | crx.pack(archiveBuffer).then((crxBuffer) => { 22 | const updateXML = crx.generateUpdateXML(); 23 | 24 | fs.writeFile('update.xml', updateXML); 25 | fs.writeFile(`${name}.crx`, crxBuffer); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /extension/chrome/manifest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "page": "background.html" 4 | }, 5 | "browser_action": { 6 | "default_icon": { 7 | "128": "img/icon-128.png", 8 | "16": "img/icon-16.png", 9 | "48": "img/icon-48.png" 10 | }, 11 | "default_title": "Reselect Devtools" 12 | }, 13 | "content_security_policy": "default-src 'self'; script-src 'self' http://localhost:3000 https://localhost:3000 'unsafe-eval'; connect-src http://localhost:3000 https://localhost:3000; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' data:;", 14 | "description": "Reselect Devtools", 15 | "devtools_page": "devtools.html", 16 | "icons": { 17 | "128": "img/icon-128.png", 18 | "16": "img/icon-16.png", 19 | "48": "img/icon-48.png" 20 | }, 21 | "manifest_version": 2, 22 | "name": "Reselect Devtools", 23 | "permissions": [ 24 | "management", 25 | "tabs", 26 | "storage", 27 | "" 28 | ], 29 | "version": "0.0.2" 30 | } -------------------------------------------------------------------------------- /extension/app/utils/apiMiddleware.js: -------------------------------------------------------------------------------- 1 | import * as types from '../../app/constants/ActionTypes'; 2 | import { 3 | checkSelectorSuccess, 4 | checkSelectorFailed, 5 | getSelectorGraphSuccess, 6 | getSelectorGraphFailed, 7 | } from '../../app/actions/graph'; 8 | 9 | export default api => store => next => async (action) => { 10 | const result = next(action); 11 | if (action.type === types.CHECK_SELECTOR) { 12 | const { selector } = action.payload; 13 | const { id } = selector; 14 | try { 15 | const checked = await api.checkSelector(id); 16 | store.dispatch(checkSelectorSuccess({ ...checked, id })); 17 | } catch (e) { 18 | store.dispatch(checkSelectorFailed(selector)); 19 | } 20 | return result; 21 | } 22 | 23 | if (action.type === types.GET_SELECTOR_GRAPH) { 24 | try { 25 | const graph = await api.selectorGraph(); 26 | store.dispatch(getSelectorGraphSuccess(graph)); 27 | } catch (e) { 28 | store.dispatch(getSelectorGraphFailed()); 29 | } 30 | } 31 | 32 | return result; 33 | }; 34 | -------------------------------------------------------------------------------- /extension/app/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | // import storage from '../utils/storage'; 5 | 6 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 7 | /* eslint-disable no-underscore-dangle */ 8 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 9 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 10 | // Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html 11 | }) : 12 | compose; 13 | /* eslint-enable no-underscore-dangle */ 14 | 15 | 16 | export default function (initialState, ...middlewares) { 17 | const enhancer = composeEnhancers( 18 | applyMiddleware(thunk, ...middlewares), 19 | // storage(), 20 | ); 21 | 22 | const store = createStore(rootReducer, initialState, enhancer); 23 | 24 | if (module.hot) { 25 | module.hot.accept('../reducers', () => { 26 | const nextRootReducer = require('../reducers'); 27 | 28 | store.replaceReducer(nextRootReducer); 29 | }); 30 | } 31 | return store; 32 | } 33 | -------------------------------------------------------------------------------- /extension/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jhen-Jie Hong 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Reselect Contributors 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 | -------------------------------------------------------------------------------- /extension/TODO.md: -------------------------------------------------------------------------------- 1 | Features: 2 | - [X] refresh button? 3 | - [X] make the graph pretty 4 | - [X] highlight dependencies through graph 5 | - [X] display number of recomputations (might need a refresh button) 6 | - [X] deal with long labels 7 | - [X] deal w/weird flexbox layout issues 8 | - [X] change N/A/ everywhere 9 | - [X] should we be able to get the value of selectors in the extension that we haven't added. - no 10 | - [X] pageApi can run into problems because we attempt to pass in an unregistered func with quotes in it. 11 | - [X] inject id field into graph nodes 12 | - [X] search selectors 13 | - [X] lock drawer open or closed 14 | - [X] find nodes depend on a given node 15 | - [X] highlight most recomputed nodes 16 | - [X] improve checkSelector rendering - zip inputs and dependencies 17 | - [ ] highlight unregistered nodes 18 | - [ ] show page action only when we are on a page w/devtools on it 19 | - [ ] Enable / disable depending on whether or not reselect tools have been installed 20 | 21 | 22 | Platforms: 23 | - [ ] Allow remote debugging with an RPC interface a la redux devtools 24 | 25 | Productionize 26 | - [X] Create icon 27 | - [X] Remove todoapp references 28 | - [X] Remove console logs 29 | - [X] Remove unnecessary pug templates 30 | - [X] Handle bad loads better 31 | - [X] Decide if we need redux 32 | - [ ] Remove all references to boilerplate 33 | - [ ] Set up linting, at least 34 | - [ ] At least look at the tests 35 | -------------------------------------------------------------------------------- /examples/demo-app.js: -------------------------------------------------------------------------------- 1 | var { selectorGraph } = ReselectTools; 2 | var { createSelector } = Reselect; 3 | ReselectTools.getStateWith(() => STORE); 4 | 5 | var STORE = { 6 | data: { 7 | users: { 8 | '1': { 9 | id: '1', 10 | name: 'bob', 11 | pets: ['a', 'b'], 12 | }, 13 | '2': { 14 | id: '2', 15 | name: 'alice', 16 | pets: ['a'], 17 | } 18 | }, 19 | pets: { 20 | 'a': { 21 | name: 'fluffy', 22 | }, 23 | 'b': { 24 | name: 'paws', 25 | } 26 | } 27 | }, 28 | ui: { 29 | currentUser: '1', 30 | } 31 | }; 32 | 33 | const data$ = (state) => state.data; 34 | const ui$ = (state) => state.ui; 35 | var users$ = createSelector(data$, (data) => data.users); 36 | var pets$ = createSelector(data$, ({ pets }) => pets); 37 | var currentUser$ = createSelector(ui$, users$, (ui, users) => users[ui.currentUser]); 38 | 39 | var currentUserPets$ = createSelector(currentUser$, pets$, (currentUser, pets) => currentUser.pets.map((petId) => pets[petId])); 40 | 41 | const random$ = (state) => 1; 42 | const thingy$ = createSelector(random$, (number) => number + 1); 43 | 44 | const selectors = { 45 | data$, 46 | ui$, 47 | users$, 48 | pets$, 49 | currentUser$, 50 | currentUserPets$, 51 | random$, 52 | thingy$, 53 | }; 54 | 55 | ReselectTools.registerSelectors(selectors); 56 | 57 | 58 | drawCytoscapeGraph(selectorGraph()); -------------------------------------------------------------------------------- /extension/test/app/actions/todos.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as types from '../../../app/constants/ActionTypes'; 3 | import * as actions from '../../../app/actions/todos'; 4 | 5 | describe('todoapp todo actions', () => { 6 | it('addTodo should create ADD_TODO action', () => { 7 | expect(actions.addTodo('Use Redux')).to.eql({ 8 | type: types.ADD_TODO, 9 | text: 'Use Redux' 10 | }); 11 | }); 12 | 13 | it('deleteTodo should create DELETE_TODO action', () => { 14 | expect(actions.deleteTodo(1)).to.eql({ 15 | type: types.DELETE_TODO, 16 | id: 1 17 | }); 18 | }); 19 | 20 | it('editTodo should create EDIT_TODO action', () => { 21 | expect(actions.editTodo(1, 'Use Redux everywhere')).to.eql({ 22 | type: types.EDIT_TODO, 23 | id: 1, 24 | text: 'Use Redux everywhere' 25 | }); 26 | }); 27 | 28 | it('completeTodo should create COMPLETE_TODO action', () => { 29 | expect(actions.completeTodo(1)).to.eql({ 30 | type: types.COMPLETE_TODO, 31 | id: 1 32 | }); 33 | }); 34 | 35 | it('completeAll should create COMPLETE_ALL action', () => { 36 | expect(actions.completeAll()).to.eql({ 37 | type: types.COMPLETE_ALL 38 | }); 39 | }); 40 | 41 | it('clearCompleted should create CLEAR_COMPLETED action', () => { 42 | expect(actions.clearCompleted('Use Redux')).to.eql({ 43 | type: types.CLEAR_COMPLETED 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /extension/test/app/components/Header.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import sinon from 'sinon'; 3 | import React from 'react'; 4 | import TestUtils from 'react-addons-test-utils'; 5 | import Header from '../../../app/components/Header'; 6 | import TodoTextInput from '../../../app/components/TodoTextInput'; 7 | 8 | function setup() { 9 | const props = { 10 | addTodo: sinon.spy() 11 | }; 12 | 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(
); 15 | const output = renderer.getRenderOutput(); 16 | 17 | return { props, output, renderer }; 18 | } 19 | 20 | describe('todoapp Header component', () => { 21 | it('should render correctly', () => { 22 | const { output } = setup(); 23 | 24 | expect(output.type).to.equal('header'); 25 | 26 | const [h1, input] = output.props.children; 27 | 28 | expect(h1.type).to.equal('h1'); 29 | expect(h1.props.children).to.equal('todos'); 30 | 31 | expect(input.type).to.equal(TodoTextInput); 32 | expect(input.props.newTodo).to.equal(true); 33 | expect(input.props.placeholder).to.equal('What needs to be done?'); 34 | }); 35 | 36 | it('should call addTodo if length of text is greater than 0', () => { 37 | const { output, props } = setup(); 38 | const input = output.props.children[1]; 39 | input.props.onSave(''); 40 | expect(props.addTodo.callCount).to.equal(0); 41 | input.props.onSave('Use Redux'); 42 | expect(props.addTodo.callCount).to.equal(1); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /extension/app/components/TodoTextInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import style from './TodoTextInput.css'; 4 | 5 | export default class TodoTextInput extends Component { 6 | 7 | static propTypes = { 8 | onSave: PropTypes.func.isRequired, 9 | text: PropTypes.string, 10 | placeholder: PropTypes.string, 11 | editing: PropTypes.bool, 12 | newTodo: PropTypes.bool 13 | }; 14 | 15 | constructor(props, context) { 16 | super(props, context); 17 | this.state = { 18 | text: this.props.text || '' 19 | }; 20 | } 21 | 22 | handleSubmit = (evt) => { 23 | const text = evt.target.value.trim(); 24 | if (evt.which === 13) { 25 | this.props.onSave(text); 26 | if (this.props.newTodo) { 27 | this.setState({ text: '' }); 28 | } 29 | } 30 | }; 31 | 32 | handleChange = (evt) => { 33 | this.setState({ text: evt.target.value }); 34 | }; 35 | 36 | handleBlur = (evt) => { 37 | if (!this.props.newTodo) { 38 | this.props.onSave(evt.target.value); 39 | } 40 | }; 41 | 42 | render() { 43 | return ( 44 | 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/your-app.js: -------------------------------------------------------------------------------- 1 | // Comes from JSON.stringify(selectorGraph()); 2 | const graph = { 3 | "edges": [ 4 | { 5 | "from": "users$", 6 | "to": "data$" 7 | }, 8 | { 9 | "from": "pets$", 10 | "to": "data$" 11 | }, 12 | { 13 | "from": "currentUser$", 14 | "to": "ui$" 15 | }, 16 | { 17 | "from": "currentUser$", 18 | "to": "users$" 19 | }, 20 | { 21 | "from": "currentUserPets$", 22 | "to": "currentUser$" 23 | }, 24 | { 25 | "from": "currentUserPets$", 26 | "to": "pets$" 27 | }, 28 | { 29 | "from": "thingy$", 30 | "to": "random$" 31 | } 32 | ], 33 | "nodes": { 34 | "currentUser$": { 35 | "name": "currentUser$", 36 | "recomputations": 0 37 | }, 38 | "currentUserPets$": { 39 | "name": "currentUserPets$", 40 | "recomputations": 0 41 | }, 42 | "data$": { 43 | "name": "data$", 44 | "recomputations": null, 45 | }, 46 | "pets$": { 47 | "name": "pets$", 48 | "recomputations": 0 49 | }, 50 | "random$": { 51 | "name": "random$", 52 | "recomputations": null, 53 | }, 54 | "thingy$": { 55 | "name": "thingy$", 56 | "recomputations": 0 57 | }, 58 | "ui$": { 59 | "name": "ui$", 60 | "recomputations": null, 61 | }, 62 | "users$": { 63 | "name": "users$", 64 | "recomputations": 0 65 | } 66 | } 67 | } 68 | 69 | // This overrides the graph the devtools get - don't try this at home! 70 | window.__RESELECT_TOOLS__.selectorGraph = () => graph; 71 | 72 | 73 | drawCytoscapeGraph(graph); -------------------------------------------------------------------------------- /examples/simple-graph.js: -------------------------------------------------------------------------------- 1 | var { checkSelector } = ReselectTools; 2 | 3 | 4 | const cytoDefaults = { 5 | style: [ // the stylesheet for the graph 6 | { 7 | selector: 'node', 8 | style: { 9 | 'background-color': '#666', 10 | 'label': 'data(id)' 11 | } 12 | }, 13 | 14 | { 15 | selector: 'edge', 16 | style: { 17 | 'width': 3, 18 | 'line-color': '#ccc', 19 | 'target-arrow-color': '#ccc', 20 | 'target-arrow-shape': 'triangle' 21 | } 22 | } 23 | ], 24 | 25 | layout: { 26 | name: 'dagre', 27 | rankDir: 'BT', 28 | ranker: 'longest-path', 29 | } 30 | }; 31 | 32 | 33 | function drawCytoscapeGraph(graph) { 34 | const { nodes, edges } = graph; 35 | 36 | const cytoNodes = Object.keys(nodes).map((name) => ({ 37 | data: Object.assign({}, nodes[name], { 38 | id: name 39 | }) 40 | })); 41 | 42 | const findSelectorId = (selector) => { 43 | const node = cytoNodes.find(({ data }) => data.name === selector); 44 | return node.data.id; 45 | }; 46 | 47 | const cytoEdges = edges.map((edge, i) => ({data: { 48 | source: findSelectorId(edge.from), 49 | target: findSelectorId(edge.to), 50 | id: i, 51 | }})); 52 | 53 | const elements = cytoNodes.concat(cytoEdges); 54 | 55 | const cy = cytoscape(Object.assign({}, cytoDefaults, { 56 | container: document.getElementById('root'), // container to render in 57 | elements, 58 | })); 59 | 60 | cy.nodes().on("click", function(x, ...args) { 61 | const data = this.data(); 62 | console.log(data.name, checkSelector(data.name)); 63 | }); 64 | 65 | } 66 | -------------------------------------------------------------------------------- /extension/app/components/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | color: #777; 3 | padding: 10px 15px; 4 | height: 20px; 5 | text-align: center; 6 | border-top: 1px solid #e6e6e6; 7 | } 8 | 9 | .footer:before { 10 | content: ''; 11 | position: absolute; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | height: 50px; 16 | overflow: hidden; 17 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 18 | 0 8px 0 -3px #f6f6f6, 19 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 20 | 0 16px 0 -6px #f6f6f6, 21 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 22 | } 23 | 24 | .filters { 25 | margin: 0; 26 | padding: 0; 27 | list-style: none; 28 | position: absolute; 29 | right: 0; 30 | left: 0; 31 | } 32 | 33 | .filters li { 34 | display: inline; 35 | } 36 | 37 | .filters li a { 38 | color: inherit; 39 | margin: 3px; 40 | padding: 3px 7px; 41 | text-decoration: none; 42 | border: 1px solid transparent; 43 | border-radius: 3px; 44 | } 45 | 46 | .filters li a.selected, 47 | .filters li a:hover { 48 | border-color: rgba(175, 47, 47, 0.1); 49 | } 50 | 51 | .filters li a.selected { 52 | border-color: rgba(175, 47, 47, 0.2); 53 | } 54 | 55 | .todoCount { 56 | float: left; 57 | text-align: left; 58 | } 59 | 60 | .todoCount strong { 61 | font-weight: 300; 62 | } 63 | 64 | .clearCompleted, 65 | html .clearCompleted:active { 66 | float: right; 67 | position: relative; 68 | line-height: 20px; 69 | text-decoration: none; 70 | cursor: pointer; 71 | } 72 | 73 | .clearCompleted:hover { 74 | text-decoration: underline; 75 | } 76 | 77 | @media (max-width: 430px) { 78 | .footer { 79 | height: 50px; 80 | } 81 | 82 | .filters { 83 | bottom: 10px; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /extension/app/components/SelectorState.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import StateTree from './StateTree'; 3 | import style from './SelectorState.css'; 4 | 5 | const InputsSection = ({ zipped = [], onClickSelector }) => ( 6 |
7 |
{zipped.length ? 'Inputs' : 'No Inputs'}
8 | 9 | 10 | { zipped.length ? zipped.map(([name, input], i) => ( 11 | 12 | 15 | 16 | )) : null 17 | } 18 | 19 |
13 | onClickSelector({ id: name })}>{name} 14 |
20 |
21 | ); 22 | InputsSection.propTypes = { 23 | zipped: PropTypes.array, 24 | onClickSelector: PropTypes.func 25 | }; 26 | 27 | const OutputSection = ({ output }) => ( 28 |
29 |
Output
30 | 31 |
32 | ); 33 | OutputSection.propTypes = { output: PropTypes.any }; 34 | 35 | export default class SelectorState extends Component { 36 | static propTypes = { 37 | checkedSelector: PropTypes.object, 38 | onClickSelector: PropTypes.func 39 | } 40 | render() { 41 | const { checkedSelector, onClickSelector } = this.props; 42 | const { zipped, output } = checkedSelector; 43 | return ( 44 |
45 | 46 | { zipped && } 47 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /extension/chrome/extension/reselect-tools-app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from '../../app/containers/Root'; 4 | import './reselect-tools-app.css'; 5 | 6 | import * as api from './page-api'; 7 | 8 | import createStore from '../../app/store/configureStore'; 9 | import createApiMiddleware from '../../app/utils/apiMiddleware'; 10 | 11 | const checkSelector = (id) => { 12 | if (id === 'c') { 13 | return Promise.resolve({ inputs: [1], output: {hey: 'hey'}, id, name: id }); 14 | } 15 | if (id === 'b') { 16 | return Promise.resolve({ inputs: [1], output: 5, id, name: id }); 17 | } 18 | if (id === 'a') { 19 | return Promise.resolve({ inputs: [5], output: 5, id, name: id }); 20 | } 21 | return Promise.resolve({ inputs: [], output: 2, id, name: id }); 22 | }; 23 | 24 | const mockApi = { 25 | checkSelector, 26 | selectorGraph: () => { 27 | const a = { id: 'a', recomputations: 10, isNamed: true }; 28 | const b = { id: 'b', recomputations: 10, isNamed: true }; 29 | const c = { id: 'c', recomputations: 10, isNamed: true }; 30 | const d = { id: 'd', recomputations: 2, isNamed: true }; 31 | const e = { id: 'e', recomputations: 4, isNamed: true }; 32 | const f = { id: 'f', recomputations: 6, isNamed: true }; 33 | return Promise.resolve({ nodes: { a, b, c, d, e, f }, edges: [{ from: 'a', to: 'b' }, { from: 'b', to: 'c' }] }); 34 | }, 35 | }; 36 | 37 | 38 | const apiMiddleware = createApiMiddleware(api); 39 | // const apiMiddleware = createApiMiddleware(window.location.origin === 'http://localhost:8000' ? mockApi : api); 40 | 41 | 42 | const initialState = {}; 43 | 44 | ReactDOM.render( 45 | , 46 | document.querySelector('#root') 47 | ); 48 | -------------------------------------------------------------------------------- /extension/test/e2e/injectpage.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webdriver from 'selenium-webdriver'; 3 | import { expect } from 'chai'; 4 | import { delay, startChromeDriver, buildWebDriver } from '../func'; 5 | 6 | describe('inject page (in github.com)', function test() { 7 | let driver; 8 | this.timeout(15000); 9 | 10 | before(async () => { 11 | await startChromeDriver(); 12 | const extPath = path.resolve('build'); 13 | driver = buildWebDriver(extPath); 14 | await driver.get('https://github.com'); 15 | }); 16 | 17 | after(async () => driver.quit()); 18 | 19 | it('should open Github', async () => { 20 | const title = await driver.getTitle(); 21 | expect(title).to.equal('The world\'s leading software development platform · GitHub'); 22 | }); 23 | 24 | it('should render inject app', async () => { 25 | await driver.wait( 26 | () => driver.findElements(webdriver.By.className('inject-react-example')) 27 | .then(elems => elems.length > 0), 28 | 10000, 29 | 'Inject app not found' 30 | ); 31 | }); 32 | 33 | it('should find `Open TodoApp` button', async () => { 34 | await driver.wait( 35 | () => driver.findElements(webdriver.By.css('.inject-react-example button')) 36 | .then(elems => elems.length > 0), 37 | 10000, 38 | 'Inject app `Open TodoApp` button not found' 39 | ); 40 | }); 41 | 42 | it('should find iframe', async () => { 43 | driver.findElement(webdriver.By.css('.inject-react-example button')).click(); 44 | await delay(1000); 45 | await driver.wait( 46 | () => driver.findElements(webdriver.By.css('.inject-react-example iframe')) 47 | .then(elems => elems.length > 0), 48 | 10000, 49 | 'Inject app iframe not found' 50 | ); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /extension/webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const postCSSConfig = require('./postcss.config'); 4 | 5 | const customPath = path.join(__dirname, './customPublicPath'); 6 | 7 | module.exports = { 8 | entry: { 9 | 'reselect-tools-app': [customPath, path.join(__dirname, '../chrome/extension/reselect-tools-app')], 10 | background: [customPath, path.join(__dirname, '../chrome/extension/background')], 11 | panel: [customPath, path.join(__dirname, '../chrome/extension/panel')], 12 | devtools: [customPath, path.join(__dirname, '../chrome/extension/devtools')], 13 | }, 14 | output: { 15 | path: path.join(__dirname, '../build/js'), 16 | filename: '[name].bundle.js', 17 | chunkFilename: '[id].chunk.js' 18 | }, 19 | postcss() { 20 | return postCSSConfig; 21 | }, 22 | plugins: [ 23 | new webpack.optimize.OccurenceOrderPlugin(), 24 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.dev$/), 25 | new webpack.optimize.DedupePlugin(), 26 | new webpack.optimize.UglifyJsPlugin({ 27 | comments: false, 28 | compressor: { 29 | warnings: false 30 | } 31 | }), 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | NODE_ENV: JSON.stringify('production') 35 | } 36 | }) 37 | ], 38 | resolve: { 39 | extensions: ['', '.js'] 40 | }, 41 | module: { 42 | loaders: [{ 43 | test: /\.js$/, 44 | loader: 'babel', 45 | exclude: /node_modules/, 46 | query: { 47 | presets: ['react-optimize'] 48 | } 49 | }, { 50 | test: /\.css$/, 51 | loaders: [ 52 | 'style', 53 | 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 54 | 'postcss' 55 | ] 56 | }] 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /extension/app/reducers/graph.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from '../constants/ActionTypes'; 2 | 3 | const initialState = { 4 | fetching: false, 5 | fetchedSuccessfully: false, 6 | fetchedOnce: false, 7 | nodes: {}, 8 | edges: [], 9 | checkedSelectorId: null, 10 | }; 11 | 12 | const actionsMap = { 13 | [ActionTypes.GET_SELECTOR_GRAPH_FAILED](state) { 14 | return { ...state, fetching: false, fetchedSuccessfully: false }; 15 | }, 16 | [ActionTypes.GET_SELECTOR_GRAPH_SUCCESS](state, action) { 17 | const { nodes, edges } = action.payload.graph; 18 | const oldNodes = state.nodes; 19 | const mergedNodes = {}; 20 | Object.keys(nodes).forEach((id) => { 21 | const node = { id, ...oldNodes[id], ...nodes[id] }; 22 | if (node.isNamed === undefined) { 23 | node.isNamed = node.isRegistered; 24 | } 25 | mergedNodes[id] = node; 26 | }); 27 | 28 | return { 29 | ...state, 30 | fetching: false, 31 | nodes: mergedNodes, 32 | edges, 33 | fetchedSuccessfully: true 34 | }; 35 | }, 36 | [ActionTypes.GET_SELECTOR_GRAPH](state) { 37 | return { 38 | ...state, 39 | fetchedOnce: true, 40 | fetching: true, 41 | }; 42 | }, 43 | [ActionTypes.UNCHECK_SELECTOR](state) { 44 | return { ...state, checkedSelectorId: null }; 45 | }, 46 | [ActionTypes.CHECK_SELECTOR_SUCCESS](state, action) { 47 | const { selector } = action.payload; 48 | const { nodes } = state; 49 | const { id } = selector; 50 | return { 51 | ...state, 52 | checkedSelectorId: id, 53 | nodes: { 54 | ...nodes, 55 | [id]: { ...nodes[id], ...selector } 56 | } 57 | }; 58 | }, 59 | [ActionTypes.CHECK_SELECTOR_FAILED](state, action) { 60 | // set it anyway 61 | return { ...state, checkedSelectorId: action.payload.selector.id }; 62 | }, 63 | }; 64 | 65 | export default function graph(state = initialState, action) { 66 | const reduceFn = actionsMap[action.type]; 67 | if (!reduceFn) return state; 68 | return reduceFn(state, action); 69 | } 70 | -------------------------------------------------------------------------------- /extension/app/components/Dock.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import MdKeyboardArrowLeft from 'react-icons/lib/md/keyboard-arrow-left'; 3 | import MdKeyboardArrowRight from 'react-icons/lib/md/keyboard-arrow-right'; 4 | import Button from 'remotedev-app/lib/components/Button'; 5 | 6 | const Subheader = ({ style, children, ...props }) => ( 7 |
{children}
8 | ); 9 | 10 | const messageContainerStyle = { 11 | display: 'flex', 12 | justifyContent: 'space-between', 13 | alignItems: 'center', 14 | flexShrink: 0, 15 | }; 16 | 17 | const Dock = ({ isOpen, toggleDock, message, children }) => { 18 | const dockStyle = { 19 | position: 'absolute', 20 | background: 'rgb(0, 43, 54)', 21 | top: 0, 22 | borderRight: '1px solid rgb(79, 90, 101)', 23 | borderTop: '1px solid rgb(79, 90, 101)', 24 | transform: `translateX(${isOpen ? 0 : '-100%'})`, 25 | left: 0, 26 | height: '100%', 27 | padding: '10px', 28 | minWidth: '220px', 29 | zIndex: 1, 30 | transition: 'transform 200ms ease-out', 31 | boxSizing: 'border-box', 32 | display: 'flex', 33 | flexDirection: 'column', 34 | }; 35 | const showButtonStyle = { 36 | position: 'relative', 37 | right: 0, 38 | top: 0, 39 | transition: 'transform 200ms ease-out', 40 | transform: `translateX(${isOpen ? 0 : '100%'})`, 41 | }; 42 | return ( 43 |
44 |
45 | {message} 46 | 47 | 51 | 52 |
53 | {children} 54 |
55 | ); 56 | }; 57 | 58 | Dock.propTypes = { 59 | isOpen: PropTypes.bool.isRequired, 60 | toggleDock: PropTypes.func, 61 | children: PropTypes.object, 62 | message: PropTypes.string 63 | }; 64 | 65 | export default Dock; 66 | -------------------------------------------------------------------------------- /extension/webpack/replace/JsonpMainTemplate.runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | /*globals hotAddUpdateChunk parentHotUpdateCallback document XMLHttpRequest $require$ $hotChunkFilename$ $hotMainFilename$ */ 6 | module.exports = function() { 7 | function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars 8 | hotAddUpdateChunk(chunkId, moreModules); 9 | if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); 10 | } 11 | 12 | var context = this; 13 | function evalCode(code, context) { 14 | return (function() { return eval(code); }).call(context); 15 | } 16 | 17 | context.hotDownloadUpdateChunk = function (chunkId) { // eslint-disable-line no-unused-vars 18 | var src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"; 19 | var request = new XMLHttpRequest(); 20 | 21 | request.onload = function() { 22 | evalCode(this.responseText, context); 23 | }; 24 | request.open("get", src, true); 25 | request.send(); 26 | } 27 | 28 | function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars 29 | if(typeof XMLHttpRequest === "undefined") 30 | return callback(new Error("No browser support")); 31 | try { 32 | var request = new XMLHttpRequest(); 33 | var requestPath = $require$.p + $hotMainFilename$; 34 | request.open("GET", requestPath, true); 35 | request.timeout = 10000; 36 | request.send(null); 37 | } catch(err) { 38 | return callback(err); 39 | } 40 | request.onreadystatechange = function() { 41 | if(request.readyState !== 4) return; 42 | if(request.status === 0) { 43 | // timeout 44 | callback(new Error("Manifest request to " + requestPath + " timed out.")); 45 | } else if(request.status === 404) { 46 | // no update available 47 | callback(); 48 | } else if(request.status !== 200 && request.status !== 304) { 49 | // other failure 50 | callback(new Error("Manifest request to " + requestPath + " failed.")); 51 | } else { 52 | // success 53 | try { 54 | var update = JSON.parse(request.responseText); 55 | } catch(e) { 56 | callback(e); 57 | return; 58 | } 59 | callback(null, update); 60 | } 61 | }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /extension/webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const postCSSConfig = require('./postcss.config'); 4 | 5 | const host = 'localhost'; 6 | const port = 3000; 7 | const customPath = path.join(__dirname, './customPublicPath'); 8 | const hotScript = 'webpack-hot-middleware/client?path=__webpack_hmr&dynamicPublicPath=true'; 9 | 10 | const baseDevConfig = () => ({ 11 | devtool: 'eval-cheap-module-source-map', 12 | entry: { 13 | 'reselect-tools-app': [customPath, hotScript, path.join(__dirname, '../chrome/extension/reselect-tools-app')], 14 | background: [customPath, hotScript, path.join(__dirname, '../chrome/extension/background')], 15 | panel: [customPath, hotScript, path.join(__dirname, '../chrome/extension/panel')], 16 | devtools: [customPath, hotScript, path.join(__dirname, '../chrome/extension/devtools')], 17 | }, 18 | devMiddleware: { 19 | publicPath: `http://${host}:${port}/js`, 20 | stats: { 21 | colors: true 22 | }, 23 | noInfo: true, 24 | headers: { 'Access-Control-Allow-Origin': '*' } 25 | }, 26 | hotMiddleware: { 27 | path: '/js/__webpack_hmr' 28 | }, 29 | output: { 30 | path: path.join(__dirname, '../dev/js'), 31 | filename: '[name].bundle.js', 32 | chunkFilename: '[id].chunk.js' 33 | }, 34 | postcss() { 35 | return postCSSConfig; 36 | }, 37 | plugins: [ 38 | new webpack.HotModuleReplacementPlugin(), 39 | new webpack.NoErrorsPlugin(), 40 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.prod$/), 41 | new webpack.DefinePlugin({ 42 | __HOST__: `'${host}'`, 43 | __PORT__: port, 44 | 'process.env': { 45 | NODE_ENV: JSON.stringify('development') 46 | } 47 | }) 48 | ], 49 | resolve: { 50 | extensions: ['', '.js'] 51 | }, 52 | module: { 53 | loaders: [{ 54 | test: /\.js$/, 55 | loader: 'babel', 56 | exclude: /node_modules/, 57 | query: { 58 | presets: ['react-hmre'] 59 | } 60 | }, { 61 | test: /\.css$/, 62 | loaders: [ 63 | 'style', 64 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 65 | 'postcss' 66 | ] 67 | }] 68 | } 69 | }); 70 | 71 | const appConfig = baseDevConfig(); 72 | 73 | module.exports = [ 74 | appConfig 75 | ]; 76 | -------------------------------------------------------------------------------- /extension/app/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classnames from 'classnames'; 3 | import TodoTextInput from './TodoTextInput'; 4 | import style from './TodoItem.css'; 5 | 6 | export default class TodoItem extends Component { 7 | 8 | static propTypes = { 9 | todo: PropTypes.object.isRequired, 10 | editTodo: PropTypes.func.isRequired, 11 | deleteTodo: PropTypes.func.isRequired, 12 | completeTodo: PropTypes.func.isRequired 13 | }; 14 | 15 | constructor(props, context) { 16 | super(props, context); 17 | this.state = { 18 | editing: false 19 | }; 20 | } 21 | 22 | handleDoubleClick = () => { 23 | this.setState({ editing: true }); 24 | }; 25 | 26 | handleSave = (text) => { 27 | const { todo, deleteTodo, editTodo } = this.props; 28 | if (text.length === 0) { 29 | deleteTodo(todo.id); 30 | } else { 31 | editTodo(todo.id, text); 32 | } 33 | this.setState({ editing: false }); 34 | }; 35 | 36 | handleComplete = () => { 37 | const { todo, completeTodo } = this.props; 38 | completeTodo(todo.id); 39 | }; 40 | 41 | handleDelete = () => { 42 | const { todo, deleteTodo } = this.props; 43 | deleteTodo(todo.id); 44 | }; 45 | 46 | render() { 47 | const { todo } = this.props; 48 | 49 | let element; 50 | if (this.state.editing) { 51 | element = ( 52 | 57 | ); 58 | } else { 59 | element = ( 60 |
61 | 67 | 70 |
75 | ); 76 | } 77 | 78 | return ( 79 |
  • 86 | {element} 87 |
  • 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /extension/app/components/SelectorInspector.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | import Search from './SelectorSearch'; 4 | 5 | const hStyle = { 6 | overflow: 'hidden', 7 | textOverflow: 'ellipsis', 8 | margin: 0, 9 | marginRight: '10px', 10 | flexWrap: 'nowrap', 11 | whiteSpace: 'nowrap', 12 | }; 13 | 14 | const containerStyle = { 15 | flexShrink: 0, 16 | overflowX: 'hidden', 17 | overflowY: 'auto', 18 | borderBottomWidth: '3px', 19 | borderBottomStyle: 'double', 20 | display: 'flex', 21 | flexDirection: 'row', 22 | alignItems: 'center', 23 | border: '1px solid rgb(79, 90, 101)', 24 | padding: '10px', 25 | }; 26 | 27 | function SelectorInfo({ selector }) { 28 | const { recomputations, isNamed, name } = selector; 29 | 30 | const subheadStyle = { ...hStyle, color: 'rgb(111, 179, 210)' }; 31 | let message = `(${recomputations} recomputations)`; 32 | if (recomputations === null) { 33 | message = '(not memoized)'; 34 | } 35 | 36 | return ( 37 |
    38 |

    {name}

    39 |
    40 |
    {message}
    41 | { !isNamed &&
    (unregistered)
    } 42 |
    43 |
    44 | ); 45 | } 46 | SelectorInfo.propTypes = { selector: PropTypes.object }; 47 | 48 | export default class SelectorInspector extends Component { 49 | static propTypes = { 50 | selector: PropTypes.object, 51 | selectors: PropTypes.object, 52 | onSelectorChosen: PropTypes.func.isRequired, 53 | } 54 | 55 | constructor(props) { 56 | super(props); 57 | this.state = { 58 | searching: false, 59 | }; 60 | this.toggleSearch = this.toggleSearch.bind(this); 61 | } 62 | 63 | toggleSearch() { 64 | this.setState({ searching: !this.state.searching }); 65 | } 66 | 67 | render() { 68 | const { selector, selectors, onSelectorChosen } = this.props; 69 | const { searching } = this.state; 70 | return ( 71 |
    72 | { selector ? 73 | :

    Choose a selector

    74 | } 75 | 81 |
    82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /extension/app/components/TodoItem.css: -------------------------------------------------------------------------------- 1 | .normal .toggle { 2 | text-align: center; 3 | width: 40px; 4 | /* auto, since non-WebKit browsers doesn't support input styling */ 5 | height: auto; 6 | position: absolute; 7 | top: 0; 8 | bottom: 0; 9 | margin: auto 0; 10 | border: none; /* Mobile Safari */ 11 | -webkit-appearance: none; 12 | -moz-appearance: none; 13 | } 14 | 15 | .normal .toggle:after { 16 | content: url('data:image/svg+xml;utf8,'); 17 | } 18 | 19 | .normal .toggle:checked:after { 20 | content: url('data:image/svg+xml;utf8,'); 21 | } 22 | 23 | .normal label { 24 | white-space: pre-line; 25 | word-break: break-all; 26 | padding: 15px 60px 15px 15px; 27 | margin-left: 45px; 28 | display: block; 29 | line-height: 1.2; 30 | transition: color 0.4s; 31 | } 32 | 33 | .normal .destroy { 34 | display: none; 35 | position: absolute; 36 | top: 0; 37 | right: 10px; 38 | bottom: 0; 39 | width: 40px; 40 | height: 40px; 41 | margin: auto 0; 42 | font-size: 30px; 43 | color: #cc9a9a; 44 | margin-bottom: 11px; 45 | transition: color 0.2s ease-out; 46 | } 47 | 48 | .normal .destroy:hover { 49 | color: #af5b5e; 50 | } 51 | 52 | .normal .destroy:after { 53 | content: '×'; 54 | } 55 | 56 | .normal:hover .destroy { 57 | display: block; 58 | } 59 | 60 | .normal .edit { 61 | display: none; 62 | } 63 | 64 | .editing { 65 | border-bottom: none; 66 | padding: 0; 67 | composes: normal; 68 | } 69 | 70 | .editing:last-child { 71 | margin-bottom: -1px; 72 | } 73 | 74 | .editing .edit { 75 | display: block; 76 | width: 506px; 77 | padding: 13px 17px 12px 17px; 78 | margin: 0 0 0 43px; 79 | } 80 | 81 | .editing .view { 82 | display: none; 83 | } 84 | 85 | .completed label { 86 | color: #d9d9d9; 87 | text-decoration: line-through; 88 | } 89 | 90 | /* 91 | Hack to remove background from Mobile Safari. 92 | Can't use it globally since it destroys checkboxes in Firefox 93 | */ 94 | @media screen and (-webkit-min-device-pixel-ratio:0) { 95 | .normal .toggle { 96 | background: none; 97 | } 98 | 99 | .normal .toggle { 100 | height: 40px; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /extension/app/components/MainSection.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import TodoItem from './TodoItem'; 3 | import Footer from './Footer'; 4 | import { SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE } from '../constants/TodoFilters'; 5 | import style from './MainSection.css'; 6 | 7 | const TODO_FILTERS = { 8 | [SHOW_ALL]: () => true, 9 | [SHOW_ACTIVE]: todo => !todo.completed, 10 | [SHOW_COMPLETED]: todo => todo.completed 11 | }; 12 | 13 | export default class MainSection extends Component { 14 | 15 | static propTypes = { 16 | todos: PropTypes.array.isRequired, 17 | actions: PropTypes.object.isRequired 18 | }; 19 | 20 | constructor(props, context) { 21 | super(props, context); 22 | this.state = { filter: SHOW_ALL }; 23 | } 24 | 25 | handleClearCompleted = () => { 26 | const atLeastOneCompleted = this.props.todos.some(todo => todo.completed); 27 | if (atLeastOneCompleted) { 28 | this.props.actions.clearCompleted(); 29 | } 30 | }; 31 | 32 | handleShow = (filter) => { 33 | this.setState({ filter }); 34 | }; 35 | 36 | renderToggleAll(completedCount) { 37 | const { todos, actions } = this.props; 38 | if (todos.length > 0) { 39 | return ( 40 | 46 | ); 47 | } 48 | } 49 | 50 | renderFooter(completedCount) { 51 | const { todos } = this.props; 52 | const { filter } = this.state; 53 | const activeCount = todos.length - completedCount; 54 | 55 | if (todos.length) { 56 | return ( 57 |