diff --git a/template/.editorconfig b/template/.editorconfig new file mode 100644 index 0000000..b9b7147 --- /dev/null +++ b/template/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig helps developers define and maintain +# consistent coding styles between different editors and IDEs. + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/template/config/webpack.dev.config.js b/template/config/webpack.dev.config.js index 6c4d21e..d5cbe3b 100644 --- a/template/config/webpack.dev.config.js +++ b/template/config/webpack.dev.config.js @@ -46,9 +46,11 @@ module.exports = { fonts: path.join(project.path.src, 'assets/fonts'), images: path.join(project.path.src, 'assets/images'), less: path.join(project.path.src, 'assets/less'), + models: path.join(project.path.src, 'models'), plugin: path.join(project.path.src, 'index.js'), stores: path.join(project.path.src, 'stores'), - storybook: project.path.storybook + storybook: project.path.storybook, + utils: path.join(project.path.src, 'utils') } }, module: { diff --git a/template/config/webpack.karma.config.js b/template/config/webpack.karma.config.js new file mode 100644 index 0000000..d8aaa33 --- /dev/null +++ b/template/config/webpack.karma.config.js @@ -0,0 +1,110 @@ +const webpack = require('webpack'); +const path = require('path'); +const project = require('./project'); + +const GLOBALS = { + 'process.env': { + 'NODE_ENV': JSON.stringify('test') + }, + __DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'true')) +}; + +module.exports = { + target: 'electron-renderer', // webpack should compile node compatible code for tests + stats: 'errors-only', + externals: { + 'jsdom': 'window', + 'react/addons': 'react', + 'react/lib/ExecutionEnvironment': 'react', + 'react/lib/ReactContext': 'react', + 'react-addons-test-utils': 'react-dom', + }, + resolve: { + modules: ['node_modules'], + extensions: ['.js', '.jsx', '.json', 'less'], + alias: { + actions: path.join(project.path.src, 'actions'), + components: path.join(project.path.src, 'components'), + constants: path.join(project.path.src, 'constants'), + fonts: path.join(project.path.src, 'assets/fonts'), + images: path.join(project.path.src, 'assets/images'), + less: path.join(project.path.src, 'assets/less'), + models: path.join(project.path.src, 'models'), + plugin: path.join(project.path.src, 'index.js'), + stores: path.join(project.path.src, 'stores'), + storybook: project.path.storybook, + utils: path.join(project.path.src, 'utils') + } + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + { loader: 'style-loader'}, + { loader: 'css-loader' } + ] + }, + { + test: /\.less$/, + exclude: /node_modules/, + use: [ + { loader: 'style-loader' }, + { + loader: 'css-loader', + options: { + modules: true, + importLoaders: 1, + localIdentName: 'QueryHistory_[name]-[local]__[hash:base64:5]' + } + }, + { + loader: 'postcss-loader', + options: { + plugins: function () { + return [ + project.plugin.autoprefixer + ]; + } + } + }, + { + loader: 'less-loader', + options: { + noIeCompat: true + } + } + ] + }, + { + test: /node_modules\/JSONStream\/index\.js$/, + use: [{ loader: 'shebang-loader' }] + }, + { + test: /\.(js|jsx)$/, + use: [{ loader: 'babel-loader' }], + exclude: /(node_modules)/ + }, + { + test: /\.(png|jpg|jpeg|gif|svg)$/, + use: [{ + loader: 'ignore-loader', + query: { + limit: 8192, + name: 'assets/images/[name]__[hash:base64:5].[ext]' + } + }] + }, + { + test: /\.(woff|woff2|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, + use: [{ + loader: 'ignore-loader', + query: { + limit: 8192, + name: 'assets/fonts/[name]__[hash:base64:5].[ext]' + } + }], + } + ] + } +}; diff --git a/template/config/webpack.prod.config.js b/template/config/webpack.prod.config.js index f050dfb..461676a 100644 --- a/template/config/webpack.prod.config.js +++ b/template/config/webpack.prod.config.js @@ -41,9 +41,11 @@ module.exports = { fonts: path.join(project.path.src, 'assets/fonts'), images: path.join(project.path.src, 'assets/images'), less: path.join(project.path.src, 'assets/less'), + models: path.join(project.path.src, 'models'), plugin: path.join(project.path.src, 'index.js'), stores: path.join(project.path.src, 'stores'), - storybook: project.path.storybook + storybook: project.path.storybook, + utils: path.join(project.path.src, 'utils') } }, module: { diff --git a/template/config/webpack.test.config.js b/template/config/webpack.test.config.js index 7173162..ef8efd7 100644 --- a/template/config/webpack.test.config.js +++ b/template/config/webpack.test.config.js @@ -29,9 +29,11 @@ module.exports = { fonts: path.join(project.path.src, 'assets/fonts'), images: path.join(project.path.src, 'assets/images'), less: path.join(project.path.src, 'assets/less'), + models: path.join(project.path.src, 'models'), plugin: path.join(project.path.src, 'index.js'), stores: path.join(project.path.src, 'stores'), - storybook: project.path.storybook + storybook: project.path.storybook, + utils: path.join(project.path.src, 'utils') } }, module: { diff --git a/template/config/webpack.watch.config.js b/template/config/webpack.watch.config.js index a4ffbf3..5efb9cc 100644 --- a/template/config/webpack.watch.config.js +++ b/template/config/webpack.watch.config.js @@ -35,9 +35,11 @@ module.exports = { fonts: path.join(project.path.src, 'assets/fonts'), images: path.join(project.path.src, 'assets/images'), less: path.join(project.path.src, 'assets/less'), + models: path.join(project.path.src, 'models'), plugin: path.join(project.path.src, 'index.js'), stores: path.join(project.path.src, 'stores'), - storybook: project.path.storybook + storybook: project.path.storybook, + utils: path.join(project.path.src, 'utils') } }, module: { diff --git a/template/karma.conf.js b/template/karma.conf.js new file mode 100644 index 0000000..f2d6dd9 --- /dev/null +++ b/template/karma.conf.js @@ -0,0 +1,28 @@ +const webpackConfig = require('./config/webpack.karma.config'); + +module.exports = function(config) { + config.set({ + basePath: '', + singleRun: true, + files: [ + 'test/**/*.spec.js' + ], + reporters: ['mocha'], + preprocessors: { + 'test/**/*.spec.js': ['webpack', 'sourcemap'] + }, + browsers: ['Electron'], + frameworks: ['mocha', 'chai', 'sinon', 'chai-sinon'], + webpack: webpackConfig, + webpackMiddleware: { + noInfo: true, + stats: 'errors-only' + }, + // DEV: `useIframe: false` is for launching a new window instead of using an iframe + // In Electron, iframes don't get `nodeIntegration` priveleges yet windows do. + client: { + useIframe: false + }, + logLevel: config.LOG_ERROR + }); +}; diff --git a/template/package.json b/template/package.json index 28dadfe..fb093bb 100644 --- a/template/package.json +++ b/template/package.json @@ -13,6 +13,7 @@ "start:prod": "npm run compile && electron --noDevServer ./electron", "test": "cross-env NODE_ENV=test mocha-webpack \"./src/**/*.spec.js\"", "test:watch": "cross-env NODE_ENV=test mocha-webpack \"./src/**/*.spec.js\" --watch", + "test:karma": "cross-env NODE_ENV=test karma start", "cover": "nyc npm run test", "ci": "npm run check && npm test", "fmt": "mongodb-js-fmt ./*.js ./test/*.js", @@ -63,6 +64,15 @@ "istanbul-instrumenter-loader": "^3.0.0", "jsdom": "^11.1.0", "jsdom-global": "^3.0.2", + "karma": "^1.7.0", + "karma-chai": "^0.1.0", + "karma-chai-sinon": "^0.1.5", + "karma-electron": "^5.2.1", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.4", + "karma-sinon": "^1.0.5", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^2.0.4", "less": "^2.7.2", "less-loader": "^4.0.5", "mocha": "^3.4.2", diff --git a/template/src/actions/actions.js b/template/src/actions/actions.js new file mode 100644 index 0000000..eb41bd2 --- /dev/null +++ b/template/src/actions/actions.js @@ -0,0 +1,11 @@ +import Reflux from 'reflux'; + +const {{pascalcase name}}Actions = Reflux.createActions([ + /** + * define your actions as strings below, for example: + */ + 'toggleStatus' +]); + +export default {{pascalcase name}}Actions; +export { {{pascalcase name}}Actions }; diff --git a/template/src/actions/index.js b/template/src/actions/index.js index 5cbf711..ec23f6a 100644 --- a/template/src/actions/index.js +++ b/template/src/actions/index.js @@ -1,10 +1,4 @@ -const Reflux = require('reflux'); +import {{pascalcase name}}Actions from './actions'; -const {{pascalcase name}}Actions = Reflux.createActions([ - /** - * define your actions as strings below, for example: - */ - 'toggleStatus' -]); - -module.exports = {{pascalcase name}}Actions; +export default {{pascalcase name}}Actions; +export { {{pascalcase name}}Actions }; diff --git a/template/src/components/toggleButton/index.js b/template/src/components/toggle-button/index.js similarity index 55% rename from template/src/components/toggleButton/index.js rename to template/src/components/toggle-button/index.js index 4485dc2..e4b7918 100644 --- a/template/src/components/toggleButton/index.js +++ b/template/src/components/toggle-button/index.js @@ -1,4 +1,4 @@ -import ToggleButton from './ToggleButton'; +import ToggleButton from './toggle-button'; export default ToggleButton; export { ToggleButton }; diff --git a/template/src/components/toggleButton/ToggleButton.jsx b/template/src/components/toggle-button/toggle-button.jsx similarity index 95% rename from template/src/components/toggleButton/ToggleButton.jsx rename to template/src/components/toggle-button/toggle-button.jsx index 9bc1cbf..bbeb292 100644 --- a/template/src/components/toggleButton/ToggleButton.jsx +++ b/template/src/components/toggle-button/toggle-button.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import styles from './ToggleButton.less'; +import styles from './toggle-button.less'; class ToggleButton extends Component { static displayName = 'ToggleButton'; diff --git a/template/src/components/toggleButton/ToggleButton.less b/template/src/components/toggle-button/toggle-button.less similarity index 100% rename from template/src/components/toggleButton/ToggleButton.less rename to template/src/components/toggle-button/toggle-button.less diff --git a/template/src/components/toggleButton/ToggleButton.spec.js b/template/src/components/toggle-button/toggle-button.spec.js similarity index 89% rename from template/src/components/toggleButton/ToggleButton.spec.js rename to template/src/components/toggle-button/toggle-button.spec.js index 4d9cf2c..902883e 100644 --- a/template/src/components/toggleButton/ToggleButton.spec.js +++ b/template/src/components/toggle-button/toggle-button.spec.js @@ -1,8 +1,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import ToggleButton from 'components/toggleButton'; -import styles from './ToggleButton.less'; +import ToggleButton from 'components/toggle-button'; +import styles from './toggle-button.less'; describe('ToggleButton [Component]', () => { let component; diff --git a/template/src/components/toggleButton/ToggleButton.stories.js b/template/src/components/toggle-button/toggle-button.stories.js similarity index 96% rename from template/src/components/toggleButton/ToggleButton.stories.js rename to template/src/components/toggle-button/toggle-button.stories.js index d66a4e0..bc1c162 100644 --- a/template/src/components/toggleButton/ToggleButton.stories.js +++ b/template/src/components/toggle-button/toggle-button.stories.js @@ -5,7 +5,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import ComponentPreview from 'storybook/decorators/componentPreview'; import { withChaptersOptions } from 'constants/storybook'; -import { ToggleButton } from 'components/toggleButton'; +import { ToggleButton } from 'components/toggle-button'; storiesOf('ToggleButton', module) .addWithChapters('Example Title', { diff --git a/template/src/components/{{name}}/{{name}}.jsx b/template/src/components/{{name}}/{{name}}.jsx index 31ed027..23b499a 100644 --- a/template/src/components/{{name}}/{{name}}.jsx +++ b/template/src/components/{{name}}/{{name}}.jsx @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import ToggleButton from 'components/toggleButton'; +import ToggleButton from 'components/toggle-button'; import styles from './{{name}}.less'; diff --git a/template/src/components/{{name}}/{{name}}.spec.js b/template/src/components/{{name}}/{{name}}.spec.js index 6df9d56..7a5c2fe 100644 --- a/template/src/components/{{name}}/{{name}}.spec.js +++ b/template/src/components/{{name}}/{{name}}.spec.js @@ -2,7 +2,7 @@ import React from 'react'; import { mount } from 'enzyme'; import {{pascalcase name}} from 'components/{{name}}'; -import ToggleButton from 'components/toggleButton'; +import ToggleButton from 'components/toggle-button'; import styles from './{{name}}.less'; describe('{{pascalcase name}} [Component]', () => { diff --git a/template/src/index.js b/template/src/index.js index 50dc1a8..f4bbc9c 100644 --- a/template/src/index.js +++ b/template/src/index.js @@ -1,4 +1,4 @@ -import {{pascalcase name}}Plugin from './Plugin'; +import {{pascalcase name}}Plugin from './plugin'; import {{pascalcase name}}Actions from 'actions'; import {{pascalcase name}}Store from 'stores'; diff --git a/template/src/Plugin.js b/template/src/plugin.js similarity index 100% rename from template/src/Plugin.js rename to template/src/plugin.js diff --git a/template/src/Plugin.spec.js b/template/src/plugin.spec.js similarity index 91% rename from template/src/Plugin.spec.js rename to template/src/plugin.spec.js index bc3f0a2..47b04ea 100644 --- a/template/src/Plugin.spec.js +++ b/template/src/plugin.spec.js @@ -1,7 +1,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { StoreConnector } from 'hadron-react-components'; -import {{pascalcase name}}Plugin from './Plugin'; +import {{pascalcase name}}Plugin from './plugin'; describe('{{pascalcase name}} [Plugin]', () => { let component; diff --git a/template/src/stores/index.js b/template/src/stores/index.js index ef1d0b6..323a59f 100644 --- a/template/src/stores/index.js +++ b/template/src/stores/index.js @@ -1,91 +1,4 @@ -const Reflux = require('reflux'); -const {{pascalcase name}}Actions = require('../actions'); -const StateMixin = require('reflux-state-mixin'); +import {{pascalcase name}}Store from './store'; -const debug = require('debug')('mongodb-compass:stores:{{slugcase name}}'); - -/** - * {{capitalcase name}} store. - */ -const {{pascalcase name}}Store = Reflux.createStore({ - /** - * adds a state to the store, similar to React.Component's state - * @see https://github.com/yonatanmn/Super-Simple-Flux#reflux-state-mixin - * - * If you call `this.setState({...})` this will cause the store to trigger - * and push down its state as props to connected components. - */ - mixins: [StateMixin.store], - - /** - * listen to all actions defined in ../actions/index.jsx - */ - listenables: {{pascalcase name}}Actions, - - /** - * Initialize everything that is not part of the store's state. - */ - init() { - }, - - /** - * This method is called when all plugins are activated. You can register - * listeners to other plugins' stores here, e.g. - * - * appRegistry.getStore('OtherPlugin.Store').listen(this.otherStoreChanged.bind(this)); - * - * If this plugin does not depend on other stores, you can delete the method. - * - * @param {Object} appRegistry app registry containing all stores and components - */ - onActivated() { - }, - - /** - * This method is called when the data service is finished connecting. You - * receive either an error or the connected data service object, and if the - * connection was successful you can now make calls to the database, e.g. - * - * dataService.command('admin', {connectionStatus: 1}, this.handleStatus.bind(this)); - * - * If this plugin does not need to talk to the database, you can delete this - * method. - * - * @param {Object} error the error object if connection was unsuccessful - * @param {Object} dataService the dataService object if connection was successful - * - */ - onConnected() { - }, - - /** - * Initialize the {{capitalcase name}} store state. The returned object must - * contain all keys that you might want to modify with this.setState(). - * - * @return {Object} initial store state. - */ - getInitialState() { - return { - status: 'enabled' - }; - }, - - /** - * handlers for each action defined in ../actions/index.jsx, for example: - */ - toggleStatus() { - this.setState({ - status: this.state.status === 'enabled' ? 'disabled' : 'enabled' - }); - }, - - /** - * log changes to the store as debug messages. - * @param {Object} prevState previous state. - */ - storeDidUpdate(prevState) { - debug('{{pascalcase name}} store changed from', prevState, 'to', this.state); - } -}); - -module.exports = {{pascalcase name}}Store; +export default {{pascalcase name}}Store; +export { {{pascalcase name}}Store }; diff --git a/template/src/stores/store.js b/template/src/stores/store.js new file mode 100644 index 0000000..9697c37 --- /dev/null +++ b/template/src/stores/store.js @@ -0,0 +1,92 @@ +import Reflux from 'reflux'; +import StateMixin from 'reflux-state-mixin'; +import {{pascalcase name}}Actions from 'actions'; + +const debug = require('debug')('mongodb-compass:stores:{{slugcase name}}'); + +/** + * {{capitalcase name}} store. + */ +const {{pascalcase name}}Store = Reflux.createStore({ + /** + * adds a state to the store, similar to React.Component's state + * @see https://github.com/yonatanmn/Super-Simple-Flux#reflux-state-mixin + * + * If you call `this.setState({...})` this will cause the store to trigger + * and push down its state as props to connected components. + */ + mixins: [StateMixin.store], + + /** + * listen to all actions defined in ../actions/index.jsx + */ + listenables: {{pascalcase name}}Actions, + + /** + * Initialize everything that is not part of the store's state. + */ + init() { + }, + + /** + * This method is called when all plugins are activated. You can register + * listeners to other plugins' stores here, e.g. + * + * appRegistry.getStore('OtherPlugin.Store').listen(this.otherStoreChanged.bind(this)); + * + * If this plugin does not depend on other stores, you can delete the method. + * + * @param {Object} appRegistry app registry containing all stores and components + */ + onActivated() { + }, + + /** + * This method is called when the data service is finished connecting. You + * receive either an error or the connected data service object, and if the + * connection was successful you can now make calls to the database, e.g. + * + * dataService.command('admin', {connectionStatus: 1}, this.handleStatus.bind(this)); + * + * If this plugin does not need to talk to the database, you can delete this + * method. + * + * @param {Object} error the error object if connection was unsuccessful + * @param {Object} dataService the dataService object if connection was successful + * + */ + onConnected() { + }, + + /** + * Initialize the {{capitalcase name}} store state. The returned object must + * contain all keys that you might want to modify with this.setState(). + * + * @return {Object} initial store state. + */ + getInitialState() { + return { + status: 'enabled' + }; + }, + + /** + * handlers for each action defined in ../actions/index.jsx, for example: + */ + toggleStatus() { + this.setState({ + status: this.state.status === 'enabled' ? 'disabled' : 'enabled' + }); + }, + + /** + * log changes to the store as debug messages. + * @param {Object} prevState previous state. + */ + storeDidUpdate(prevState) { + debug('{{pascalcase name}} store changed from', prevState, 'to', this.state); + } +}); + +export default {{pascalcase name}}Store; +export { {{pascalcase name}}Store }; diff --git a/template/src/stores/index.spec.js b/template/src/stores/store.spec.js similarity index 100% rename from template/src/stores/index.spec.js rename to template/src/stores/store.spec.js diff --git a/template/test/renderer/store.spec.js b/template/test/renderer/store.spec.js new file mode 100644 index 0000000..b45fe10 --- /dev/null +++ b/template/test/renderer/store.spec.js @@ -0,0 +1,32 @@ +/* + * Place tests that must run in a renderer context inside Electron here. + * + * Note: The tests below are just a copy of the store unit tests as an example. + * More complex plugins will require actual renderer/integration tests to be + * executed here. + */ + +import Store from 'stores'; + +describe('{{pascalcase name}}Store [Store]', () => { + beforeEach(function() { + Store.setState( Store.getInitialState() ); + }); + + it('should have an initial state of {status: \'enabled\'}', function() { + expect(Store.state.status).to.be.equal('enabled'); + }); + + describe('toggleStatus()', function() { + it('should switch the state to {status: \'disabled\'}', function() { + Store.toggleStatus(); + expect(Store.state.status).to.be.equal('disabled'); + }); + + it('should switch the state back to {status: \'enabled\'} when used a second time', function() { + Store.toggleStatus(); + Store.toggleStatus(); + expect(Store.state.status).to.be.equal('enabled'); + }); + }); +});