diff --git a/js-frontend/config/webpackConfigParts.js b/js-frontend/config/webpackConfigParts.js new file mode 100644 index 0000000..26f3432 --- /dev/null +++ b/js-frontend/config/webpackConfigParts.js @@ -0,0 +1,11 @@ +/** + * Created by andrew on 8/18/16. + */ +export const useJSON = () => {}; +export const clean = () => {}; +export const useJQuery = () => {}; +export const extractBundle = () => {}; +export const extractLESS = () => {}; +export const purifyCSS = () => {}; +export const setupLess = () => {}; +export const devServer = () => {}; \ No newline at end of file diff --git a/js-frontend/package.json b/js-frontend/package.json index 0594ddc..810342a 100644 --- a/js-frontend/package.json +++ b/js-frontend/package.json @@ -23,6 +23,7 @@ "babel-preset-stage-0": "6.1.2", "babel-register": "6.1.4", "babel-runtime": "^6.0.14", + "copy-webpack-plugin": "^3.0.1", "css-loader": "^0.14.4", "del": "^1.2.0", "extract-text-webpack-plugin": "^0.8.1", @@ -35,6 +36,7 @@ "gulp-load-plugins": "^0.10.0", "gulp-size": "^1.2.1", "gulp-util": "^3.0.5", + "html-webpack-plugin": "^2.22.0", "json-loader": "^0.5.4", "less": "^2.5.3", "less-loader": "^2.2.0", @@ -45,7 +47,9 @@ "style-loader": "^0.12.3", "url-loader": "^0.5.6", "webpack": "^1.9.10", - "webpack-dev-server": "^1.9.0" + "webpack-dev-server": "^1.9.0", + "webpack-merge": "^0.14.1", + "webpack-validator": "^2.2.7" }, "dependencies": { "classnames": "^2.2.3", diff --git a/js-frontend/src/constants/ACTION_TYPES.js b/js-frontend/src/constants/ACTION_TYPES.js index be3284b..b046597 100644 --- a/js-frontend/src/constants/ACTION_TYPES.js +++ b/js-frontend/src/constants/ACTION_TYPES.js @@ -1,81 +1,81 @@ -import defineActionTypes from '../utils/defineActionTypes' +import { TODO_DEFINE, defineActionType } from '../utils/defineActionTypes' -export default defineActionTypes({ +export default defineActionType({ /* * View model */ - AUTH: ` - CONFIGURE_START - CONFIGURE_COMPLETE - CONFIGURE_ERROR - AUTHENTICATE_START - AUTHENTICATE_COMPLETE - AUTHENTICATE_ERROR - SIGN_IN_START - SIGN_IN_COMPLETE - SIGN_IN_ERROR - SIGN_IN_FORM_UPDATE - SIGN_UP_START - SIGN_UP_COMPLETE - SIGN_UP_ERROR - SIGN_UP_FORM_UPDATE - SIGN_OUT_START - SIGN_OUT_COMPLETE - `, + AUTH: { + CONFIGURE_START: TODO_DEFINE, + CONFIGURE_COMPLETE: TODO_DEFINE, + CONFIGURE_ERROR: TODO_DEFINE, + AUTHENTICATE_START: TODO_DEFINE, + AUTHENTICATE_COMPLETE: TODO_DEFINE, + AUTHENTICATE_ERROR: TODO_DEFINE, + SIGN_IN_START: TODO_DEFINE, + SIGN_IN_COMPLETE: TODO_DEFINE, + SIGN_IN_ERROR: TODO_DEFINE, + SIGN_IN_FORM_UPDATE: TODO_DEFINE, + SIGN_UP_START: TODO_DEFINE, + SIGN_UP_COMPLETE: TODO_DEFINE, + SIGN_UP_ERROR: TODO_DEFINE, + SIGN_UP_FORM_UPDATE: TODO_DEFINE, + SIGN_OUT_START: TODO_DEFINE, + SIGN_OUT_COMPLETE: TODO_DEFINE + }, - ENTITIES: ` - REQUESTED - RECEIVED - RECEIVED_LIST - `, + ENTITIES: { + REQUESTED: TODO_DEFINE, + RECEIVED: TODO_DEFINE, + RECEIVED_LIST: TODO_DEFINE + }, - ACCOUNTS: ` - LIST_START - LIST_COMPLETE - LIST_ERROR - LIST_REF_START - LIST_REF_COMPLETE - LIST_REF_ERROR - CREATE_START - CREATE_COMPLETE - CREATE_ERROR - CREATE_FORM_UPDATE - EDIT_START - EDIT_COMPLETE - EDIT_ERROR - EDIT_FORM_UPDATE - CREATE_REF_START - CREATE_REF_COMPLETE - CREATE_REF_ERROR - CREATE_REF_FORM_UPDATE - CREATE_REF_OWNER_LOOKUP_START - CREATE_REF_OWNER_LOOKUP_COMPLETE - CREATE_REF_ACCOUNT_LOOKUP_START - CREATE_REF_ACCOUNT_LOOKUP_COMPLETE - `, + ACCOUNTS: { + LIST_START: TODO_DEFINE, + LIST_COMPLETE: TODO_DEFINE, + LIST_ERROR: TODO_DEFINE, + LIST_REF_START: TODO_DEFINE, + LIST_REF_COMPLETE: TODO_DEFINE, + LIST_REF_ERROR: TODO_DEFINE, + CREATE_START: TODO_DEFINE, + CREATE_COMPLETE: TODO_DEFINE, + CREATE_ERROR: TODO_DEFINE, + CREATE_FORM_UPDATE: TODO_DEFINE, + EDIT_START: TODO_DEFINE, + EDIT_COMPLETE: TODO_DEFINE, + EDIT_ERROR: TODO_DEFINE, + EDIT_FORM_UPDATE: TODO_DEFINE, + CREATE_REF_START: TODO_DEFINE, + CREATE_REF_COMPLETE: TODO_DEFINE, + CREATE_REF_ERROR: TODO_DEFINE, + CREATE_REF_FORM_UPDATE: TODO_DEFINE, + CREATE_REF_OWNER_LOOKUP_START: TODO_DEFINE, + CREATE_REF_OWNER_LOOKUP_COMPLETE: TODO_DEFINE, + CREATE_REF_ACCOUNT_LOOKUP_START: TODO_DEFINE, + CREATE_REF_ACCOUNT_LOOKUP_COMPLETE: TODO_DEFINE + }, - ACCOUNT: ` - SINGLE_START - SINGLE_COMPLETE - SINGLE_ERROR - DELETE_START - DELETE_COMPLETE - DELETE_ERROR - `, + ACCOUNT: { + SINGLE_START: TODO_DEFINE, + SINGLE_COMPLETE: TODO_DEFINE, + SINGLE_ERROR: TODO_DEFINE, + DELETE_START: TODO_DEFINE, + DELETE_COMPLETE: TODO_DEFINE, + DELETE_ERROR: TODO_DEFINE + }, - TRANSFERS: ` - MAKE_START - MAKE_COMPLETE - MAKE_ERROR - MAKE_FORM_UPDATE - LIST_START - LIST_COMPLETE - LIST_ERROR - `, + TRANSFERS: { + MAKE_START: TODO_DEFINE, + MAKE_COMPLETE: TODO_DEFINE, + MAKE_ERROR: TODO_DEFINE, + MAKE_FORM_UPDATE: TODO_DEFINE, + LIST_START: TODO_DEFINE, + LIST_COMPLETE: TODO_DEFINE, + LIST_ERROR: TODO_DEFINE + }, - ERROR: ` - START - STOP - ` + ERROR: { + START:TODO_DEFINE, + STOP:TODO_DEFINE + } }) diff --git a/js-frontend/src/main.js b/js-frontend/src/main.js index 0e02f30..a189130 100644 --- a/js-frontend/src/main.js +++ b/js-frontend/src/main.js @@ -5,7 +5,8 @@ import { batchedSubscribe } from 'redux-batched-subscribe' import * as navigation from './actions/navigation' import actors from './actors' -import rootReducer from './reducers' +import rootReducer from './reducers'; +import { blocked } from './utils/blockedExecution'; // Add middleware to allow our action creators to return functions and arrays @@ -25,18 +26,11 @@ const store = createStoreWithBatching(rootReducer) // Handle changes to our store with a list of actor functions, but ensure // that the actor sequence cannot be started by a dispatch from an actor -let acting = false -store.subscribe(function() { - if (!acting) { - acting = true - - for (let actor of actors) { - actor(store.getState(), store.dispatch) - } - - acting = false +store.subscribe(blocked(() => { + for (let actor of actors) { + actor(store.getState(), store.dispatch) } -}) +})); // Dispatch navigation events when the URL's hash changes, and when the // application loads diff --git a/js-frontend/src/utils/blockedExecution.js b/js-frontend/src/utils/blockedExecution.js new file mode 100644 index 0000000..c120cb6 --- /dev/null +++ b/js-frontend/src/utils/blockedExecution.js @@ -0,0 +1,17 @@ +/** + * Created by andrew on 8/17/16. + */ + +export const blocked = (fn) => { + let isBlocked = false; + return (...args) => { + if (isBlocked) { + return; + } + //noinspection JSUnusedAssignment + isBlocked = true; + const result = fn(...args); + isBlocked = false; + return result; + }; +}; \ No newline at end of file diff --git a/js-frontend/src/utils/clientSettings.js b/js-frontend/src/utils/clientSettings.js index 739bfbe..d4c4aac 100644 --- a/js-frontend/src/utils/clientSettings.js +++ b/js-frontend/src/utils/clientSettings.js @@ -3,6 +3,8 @@ */ import * as C from "./constants"; +import root from './root'; + import fetch from "./fetch"; import parseEndpointConfig from "./parseEndpointConfig"; @@ -20,9 +22,6 @@ import { destroySession } from "./sessionStorage"; -// can't use "window" with node app -var root = Function("return this")() || (42, eval)("this"); - const defaultSettings = { //proxyIf: function() { return false; }, //proxyUrl: "/proxy", diff --git a/js-frontend/src/utils/compose.js b/js-frontend/src/utils/compose.js index d991959..96a087b 100644 --- a/js-frontend/src/utils/compose.js +++ b/js-frontend/src/utils/compose.js @@ -1,4 +1,4 @@ export default function compose(...funcs) { - const innerFunc = funcs.pop() + const innerFunc = funcs.pop(); return (...args) => funcs.reduceRight((composed, f) => f(composed), innerFunc(...args)) } diff --git a/js-frontend/src/utils/defineActionTypes.js b/js-frontend/src/utils/defineActionTypes.js index 6430679..b3481e5 100644 --- a/js-frontend/src/utils/defineActionTypes.js +++ b/js-frontend/src/utils/defineActionTypes.js @@ -1,5 +1,46 @@ import invariant from 'invariant' +export const TODO_DEFINE = Symbol('Define property'); + +export const defineActionType = (obj) => { + const result = Object.entries(obj).reduce((memo, [namespace, value]) => { + let types = []; + const namespaceTypes = {}; + + if (typeof value == 'string') { + types = value.trim().split(/\s+/); + } else { + types = Object.entries(value) + .filter(([key, needDefinition]) => needDefinition === TODO_DEFINE) + .map(([key]) => key); + } + + invariant( + /^[A-Z][A-Z0-9_]*$/.test(namespace), + "Namespace names must start with a capital letter, and be composed entirely of capital letters, numbers, and the underscore character." + ); + + invariant( + (new Set(types)).size == types.length, + "There must be no repeated action types passed to defineActionTypes" + ); + + types.forEach(t => { + invariant( + /^[A-Z][A-Z0-9_]*$/.test(t), + "Types must start with a capital letter, and be composed entirely of capital letters, numbers, and the underscore character." + ); + namespaceTypes[t] = `@@app/${namespace}/${t}`; + }); + + memo[namespace] = namespaceTypes; + + return memo; + }, {}); + + return result; + +}; export default function defineActionTypes(obj) { const result = {} diff --git a/js-frontend/webpack.config.js b/js-frontend/webpack.config.js index 5bf05b4..04f0db7 100644 --- a/js-frontend/webpack.config.js +++ b/js-frontend/webpack.config.js @@ -1,6 +1,174 @@ -import path from "path"; -import webpack from "webpack"; -import ExtractTextPlugin from "extract-text-webpack-plugin"; +'use strict'; + +const path = require('path'); +const webpack = require('webpack'); + +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const merge = require('webpack-merge'); +const validate = require('webpack-validator'); + +const CopyWebpackPlugin = require('copy-webpack-plugin'); + +const pkg = require('./package.json'); + +const parts = require('./config/webpackConfigParts'); + +const ENTRIES = Object.keys(pkg.dependencies); +const CSS_ENTIRES = ['bootstrap-horizon']; + +const JS_ENTRIES = ENTRIES.filter(p => CSS_ENTIRES.indexOf(p) < 0); + +const PATHS = { + app: path.join(__dirname, 'src'), + appEntry: path.join(__dirname, 'src', 'index.js'), + style: [ + path.join(__dirname, 'node_modules', 'bootstrap-horizon'), + path.join(__dirname, 'node_modules', 'rc-slider/assets'), + path.join(__dirname, 'src', 'style.css') + ], + styleLess: [ + path.join(__dirname, 'node_modules', 'bootstrap-horizon'), + path.join(__dirname, 'node_modules/rc-slider/assets/index.css'), + path.join(__dirname, 'src', 'style.less') + ], + build: path.join(__dirname, 'build') +}; + +const common = { + entry: { + app: PATHS.appEntry, + // , + style: PATHS.styleLess + }, + module: { + loaders: [{ + test: /\.jsx?$/, + exclude: /node_modules/, + loader: 'react-hot!babel' + }] + }, + resolve: { + extensions: ['', '.js', '.jsx'], + alias: { + 'bootstrap': path.join(__dirname, 'node_modules/bootstrap/dist/js/bootstrap.js') + } + }, + node: { + net: 'empty', + tls: 'empty', + dns: 'empty' + }, + output: { + path: PATHS.build, + publicPath: '/', + filename: '[name].js', + // Modify the name of the generated sourcemap file. + // You can use [file], [id], and [hash] replacements here. + // The default option is enough for most use cases. + sourceMapFilename: '[file].map', // Default + + // This is the sourcemap filename template. It's default format + // depends on the devtool option used. You don't need to modify this + // often. + devtoolModuleFilenameTemplate: 'webpack:///[resource-path]?[loaders]' + }, + plugins: [ + new CopyWebpackPlugin([ + { from: 'public/**', to: PATHS.build, flatten: true } + ], { + ignore: [ + '*.ejs' + ] + }), + new HtmlWebpackPlugin({ + // Required + inject: false, + template: './public/index.ejs', + //template: 'node_modules/html-webpack-template/index.ejs', + + // Optional + title: 'ES IOT Lighting', + description: 'ES IOT Lighting App', + appMountId: 'root', + // baseHref: 'http://example.com/awesome', + // devServer: 3001, + googleAnalytics: { + trackingId: 'UA-XXXX-XX', + pageViewOnLoad: true + }, + mobile: true + // window: { + // env: { + // apiHost: 'http://myapi.com/api/v1' + // } + // } + }) + ] +}; + +console.log('process.env.npm_lifecycle_event', process.env.npm_lifecycle_event); + + +const config = (() => { + switch(process.env.npm_lifecycle_event) { + case 'build': + case 'watch': + return merge( + common, + { + devtool: 'source-map', + output: { + path: PATHS.build, + filename: '[name].[chunkhash].js', + // This is used for require.ensure. The setup + // will work without but this is useful to set. + chunkFilename: '[chunkhash].js' + } + }, + parts.clean(path.join(PATHS.build, '*')), + // parts.setupCSS(PATHS.style), + parts.useJSON(), + parts.useJQuery(), + parts.extractBundle({ + name: 'vendor', + entries: JS_ENTRIES + }), + // parts.minify(), + // parts.extractCSS(PATHS.style), + parts.extractLESS(PATHS.styleLess), + parts.purifyCSS([PATHS.app]) + ); + default: + return merge( + common, + { + devtool: 'eval-source-map' + }, + parts.useJSON(), + parts.useJQuery(), + parts.extractBundle({ + name: 'vendor', + entries: JS_ENTRIES + }), + // parts.setupCSS(PATHS.style), + parts.setupLess(PATHS.styleLess), + parts.devServer({ + // Customize host/port here if needed + host: process.env.HOST, + port: process.env.PORT + }) + + ); + } + +})(); + +// module.exports = validate(config); + + + + +//import ExtractTextPlugin from "extract-text-webpack-plugin"; export default (DEBUG, PATH, PORT=3000) => ({