diff --git a/bin/dev-server.js b/bin/dev-server.js
new file mode 100644
index 00000000..aaa670fb
--- /dev/null
+++ b/bin/dev-server.js
@@ -0,0 +1,19 @@
+if (process.env.NODE_ENV === 'production') {
+ process.exit(1);
+}
+
+process.env.NODE_ENV = 'development';
+
+// Error.stackTraceLimit = Infinity;
+require('trace');
+require('clarify');
+
+const bb = require('bluebird');
+require('babel-runtime/core-js/promise').default = bb;
+global.Promise = bb;
+
+require('./utils/hook');
+const attachChangeCallback = require('./utils/watch')();
+
+require('../server')(attachChangeCallback),
+require('../tasks')();
diff --git a/bin/start-server.js b/bin/start-server.js
index bd947cb1..622959fe 100644
--- a/bin/start-server.js
+++ b/bin/start-server.js
@@ -1,21 +1,5 @@
-if (process.env.NODE_ENV === 'development') {
- // Error.stackTraceLimit = Infinity;
- require('trace');
- require('clarify');
-}
-
const bb = require('bluebird');
-
require('babel-runtime/core-js/promise').default = bb;
global.Promise = bb;
-require("babel-register")({
- ignore: /\/(public|node_modules)\//
-});
-
-const { server } = require('universal-webpack');
-
-const settings = require('../server-uwsettings');
-const configuration = require('../webpack.config.server.babel').default;
-
-server(configuration, settings);
+require('../public/server/server')();
diff --git a/bin/start-tasks.js b/bin/start-tasks.js
index 699926de..ca6d38d0 100644
--- a/bin/start-tasks.js
+++ b/bin/start-tasks.js
@@ -1,21 +1,5 @@
-if (process.env.NODE_ENV === 'development') {
- Error.stackTraceLimit = Infinity;
- require('trace');
- require('clarify');
-}
-
const bb = require('bluebird');
-
require('babel-runtime/core-js/promise').default = bb;
global.Promise = bb;
-require("babel-register")({
- ignore: /\/(public|node_modules)\//
-});
-
-const { server } = require('universal-webpack');
-
-const settings = require('../tasks-uwsettings');
-const configuration = require('../webpack.config.server.babel').default;
-
-server(configuration, settings);
+require('../public/server/tasks')();
diff --git a/bin/utils/hook.js b/bin/utils/hook.js
new file mode 100644
index 00000000..f9fe0882
--- /dev/null
+++ b/bin/utils/hook.js
@@ -0,0 +1,31 @@
+const fs = require('fs');
+const addHook = require('asset-require-hook/lib/hook');
+const assetRequireHook = require('asset-require-hook');
+const babelRegisterHook = require("babel-register");
+
+function rawLoader(resourcePath) {
+ const content = fs.readFileSync(resourcePath, 'utf-8');
+ return content;
+}
+
+// attachHook(rawLoader, '.ejs');
+addHook('.ejs', rawLoader);
+
+assetRequireHook({
+ extensions: ['.png', 'jpg', '.gif', '.svg'],
+ name: 'assets/images/[name]-[hash].[ext]',
+ publicPath: '/'
+});
+
+assetRequireHook({
+ extensions: ['.ico'],
+ name: '[name]-[hash].[ext]',
+ publicPath: '/'
+});
+
+babelRegisterHook({
+ ignore: /\/(public|node_modules)\//,
+ plugins: [
+ 'react-hot-loader/babel'
+ ]
+});
diff --git a/bin/utils/watch.js b/bin/utils/watch.js
new file mode 100644
index 00000000..de7ccb42
--- /dev/null
+++ b/bin/utils/watch.js
@@ -0,0 +1,36 @@
+const path = require('path');
+
+const watcher = require('chokidar')
+ .watch([
+ path.join(__dirname, '../../src/api'),
+ path.join(__dirname, '../../src/utils')
+ ]);
+
+module.exports = function () {
+ const callbacks = [];
+
+ watcher.on('ready', () => {
+ watcher.on('all', () => {
+ // eslint-disable-next-line no-console
+ console.log('Clearing server module cache from server');
+ Object.keys(require.cache).forEach(id => {
+ if (/[/\\]src[/\\](api|utils)/.test(id)) {
+ delete require.cache[id];
+ }
+ });
+
+ callbacks.forEach(c => c());
+ });
+ });
+
+ return callbacks.push.bind(callbacks);
+};
+
+/* Optional: "hot-reloading" of client related modules on the server
+
+ const compiler = require('webpack')(require('../../res/webpack/client.js'));
+ compiler.plugin('done', () => {
+ // Need to separate client and server code to prevent unnecessary reloads
+ });
+
+*/
diff --git a/package.json b/package.json
index 31154121..0d8d2345 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,22 @@
},
"main": "index.js",
"scripts": {
- "start": "run-s build:pre dev:all",
- "start:prod": "run-s build:all:prod start:_all:prod",
- "build:all:prod": "run-s build:pre build:_all:prod",
- "test": "NODE_ENV=development DB_ENV=test run-s reset-db:test mocha",
- "coverage": "NODE_ENV=development DB_ENV=test npm run coverage:run",
- "travis": "NODE_ENV=development DB_ENV=travis run-s reset-db:travis build:client:dev travis:test lint travis:flow",
+ "start": "run-s build:client:vendor start-dev",
+ "start-dev": "node bin/dev-server.js 2>&1 | bunyan",
+ "prod": "run-s build:all:prod start:all:prod",
+ "start:all:prod": "NODE_ENV=production run-p start:server start:tasks",
+ "start:server": "node bin/start-server.js 2>&1 | bunyan",
+ "start:tasks": "node bin/start-tasks.js",
+ "build": "run-s cleanup build:client:app build:server build:tasks",
+ "build:all:prod": "NODE_ENV=production npm run build",
+ "build:client": "run-s build:client:vendor build:client:app",
+ "build:client:app": "webpack --config './res/webpack/client.js'",
+ "build:client:vendor": "webpack --config './res/webpack/client-vendor.js'",
+ "build:server": "webpack --config './res/webpack/server.js'",
+ "build:tasks": "webpack --config './res/webpack/tasks.js'",
+ "test": "DB_ENV=test run-s reset-db:test cleanup build:client mocha",
+ "coverage": "DB_ENV=test npm run coverage:run",
+ "travis": "DB_ENV=travis run-s reset-db:travis cleanup build:client travis:test lint travis:flow",
"travis:flow": "run-s flow:install-types flow",
"travis:test": "run-s coverage:run coverage:coveralls coverage:clean",
"reset-db:test": "echo \"Preparing test database...\" && babel-node test-helpers/dropDatabase.js && knex --env test migrate:latest",
@@ -37,28 +47,7 @@
"update-post-counters": "babel-node bin/postCounters.js",
"gulp:build": "gulp build",
"gulp:watch": "gulp watch",
- "build:pre": "run-s cleanup prepare-server-build build:server:dummy-chunks",
- "cleanup": "rm -rf public/*",
- "prepare-server-build": "universal-webpack --settings ./server-uwsettings.js prepare",
- "build:server:dummy-chunks": "ln -s ../webpack-chunks.json public/server/webpack-chunks.json",
- "dev:all": "run-p build:_all:dev:watch start:_all:dev",
- "start:_all:dev": "run-p start:server:dev start:tasks:dev",
- "start:_all:prod": "run-p start:server:prod start:tasks:prod",
- "start:server:prod": "DEV=0 NODE_ENV=production node bin/start-server.js 2>&1 | bunyan",
- "start:tasks:prod": "DEV=0 NODE_ENV=production node bin/start-tasks.js",
- "start:server:dev": "DEV=1 NODE_ENV=development nodemon ./bin/start-server.js --watch ./public/server 2>&1 | bunyan",
- "start:tasks:dev": "DEV=1 NODE_ENV=development nodemon ./bin/start-tasks.js --watch ./public/server",
- "build:_all:prod": "run-p build:client:prod build:server:prod build:tasks:prod",
- "build:_all:dev:watch": "run-p build:client:dev:watch build:server:dev:watch build:tasks:dev:watch",
- "build:client:dev": "DEV=1 NODE_ENV=development webpack --config './webpack.config.client.babel.js' --colors --hide-modules --display-error-details",
- "build:client:dev:watch": "DEV=1 NODE_ENV=development webpack --config './webpack.config.client.babel.js' --colors --hide-modules --watch",
- "build:client:prod": "DEV=0 NODE_ENV=production webpack --config './webpack.config.client.babel.js' --colors --display-error-details",
- "build:server:dev": "DEV=1 NODE_ENV=development webpack --config './webpack.config.server.babel.js' --colors --hide-modules --display-error-details",
- "build:server:dev:watch": "DEV=1 NODE_ENV=development webpack --config './webpack.config.server.babel.js' --colors --hide-modules --display-error-details --watch",
- "build:server:prod": "DEV=0 NODE_ENV=production webpack --config './webpack.config.server.babel.js' --colors --display-error-details",
- "build:tasks:dev": "DEV=1 NODE_ENV=development webpack --config './webpack.config.tasks.babel.js' --colors --hide-modules --display-error-details",
- "build:tasks:dev:watch": "DEV=1 NODE_ENV=development webpack --config './webpack.config.tasks.babel.js' --colors --hide-modules --display-error-details --watch",
- "build:tasks:prod": "DEV=0 NODE_ENV=production webpack --config './webpack.config.tasks.babel.js' --colors --display-error-details"
+ "cleanup": "rm -rf public/*"
},
"author": {
"name": "Loki Education (Social Enterprise)",
@@ -162,6 +151,7 @@
},
"devDependencies": {
"adm-zip": "^0.4.7",
+ "asset-require-hook": "^1.2.0",
"autoprefixer": "~7.1.4",
"babel-core": "~6.26.0",
"babel-eslint": "~8.0.0",
@@ -184,6 +174,7 @@
"babel-preset-react-hmre": "^1.1.1",
"babel-preset-stage-1": "~6.24.1",
"brfs": "^1.4.3",
+ "chokidar": "^1.7.0",
"clarify": "^2.0.0",
"cookie": "~0.3.1",
"coveralls": "^2.11.9",
@@ -211,6 +202,7 @@
"istanbul": "~1.1.0-alpha.1",
"jsdom": "~11.2.0",
"json-loader": "~0.5.7",
+ "koa-webpack-dev-middleware": "^2.0.2",
"less": "^2.6.0",
"less-loader": "~4.0.3",
"loader-utils": "^1.1.0",
@@ -225,6 +217,7 @@
"postcss-loader": "~2.0.6",
"raf": "^3.4.0",
"raw-loader": "^0.5.1",
+ "react-hot-loader": "^3.1.1",
"react-svg-inline-loader": "^0.2.2",
"react-test-renderer": "^16.2.0",
"react-transform-hmr": "^1.0.1",
@@ -240,9 +233,12 @@
"unexpected-immutable": "~0.2.6",
"unexpected-react": "^5.0.1",
"unexpected-sinon": "~10.8.2",
- "universal-webpack": "~0.4.0",
"url-loader": "^0.5.7",
"webpack": "~3.6.0",
+ "webpack-bundle-analyzer": "^2.9.0",
+ "webpack-koa-hot-middleware": "^0.1.2",
+ "webpack-manifest-plugin": "^1.3.2",
+ "webpack-node-externals": "^1.6.0",
"wikidata-sdk": "~5.2.7",
"zopfli-webpack-plugin": "^0.1.0"
},
diff --git a/res/webpack/base.js b/res/webpack/base.js
new file mode 100644
index 00000000..5e13ab14
--- /dev/null
+++ b/res/webpack/base.js
@@ -0,0 +1,90 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const path = require('path');
+const merge = require('lodash/merge');
+const webpack = require('webpack');
+
+const
+ NODE_ENV = process.env.NODE_ENV,
+ __DEV__ = NODE_ENV !== 'production',
+ context = path.join(__dirname, '../../');
+
+// console.log(NODE_ENV);
+
+const baseConfiguration = {
+ context,
+ module: {
+ noParse: (path) => {
+ if (/react.*\.production\.min\.js$/.test(path)) {
+ return false;
+ }
+ return /\.min\.js$/.test(path);
+ }
+ },
+ output: {
+ filename: '[name].js',
+ publicPath: '/'
+ },
+ performance: {
+ hints: false
+ },
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
+ 'process.env.FACEBOOK_AUTH_ENABLED': !!process.env.FACEBOOK_CLIENT_ID,
+ 'process.env.GOOGLE_AUTH_ENABLED': !!process.env.GOOGLE_CLIENT_ID,
+ 'process.env.TWITTER_AUTH_ENABLED': !!process.env.TWITTER_CONSUMER_KEY,
+ 'process.env.GITHUB_AUTH_ENABLED': !!process.env.GITHUB_CLIENT_ID
+ }),
+ new webpack.NoEmitOnErrorsPlugin(),
+ new webpack.LoaderOptionsPlugin({
+ debug: __DEV__,
+ minimize: !__DEV__,
+ options: {
+ context
+ }
+ })
+ ],
+ stats: {
+ assets: false,
+ cached: false,
+ children: false,
+ chunks: true,
+ chunkOrigins: false,
+ modules: false,
+ reasons: false,
+ source: false,
+ timings: true
+ }
+};
+
+if (__DEV__) {
+ merge(baseConfiguration, {
+ cache: true,
+ performance: {
+ hints: 'warning'
+ },
+ resolve: {
+ alias: {
+ 'prop-types$': path.join(context, './src/external/prop-types.js')
+ }
+ }
+ });
+}
+
+module.exports = baseConfiguration;
diff --git a/res/webpack/client-vendor.js b/res/webpack/client-vendor.js
new file mode 100644
index 00000000..60e74e22
--- /dev/null
+++ b/res/webpack/client-vendor.js
@@ -0,0 +1,78 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const path = require('path');
+const webpack = require('webpack');
+
+const context = path.join(__dirname, '../../');
+
+module.exports = {
+ entry: {
+ vendor: [
+ 'babel-polyfill',
+ // 'babel-runtime',
+ 'bluebird',
+ 'classnames',
+ 'codemirror',
+ 'debounce-promise',
+ 'flow-runtime',
+ // 'font-awesome',
+ 'immutable',
+ 'isomorphic-fetch',
+ 'leaflet',
+ 'lodash',
+ 'memoizee',
+ 'prop-types',
+ 'react',
+ 'react-autosuggest',
+ 'react-dom',
+ 'react-helmet',
+ 'react-hot-loader',
+ 'react-hot-loader/patch',
+ 'react-icons/lib/fa',
+ 'react-icons/lib/md',
+ 'react-inform',
+ 'react-linkify',
+ 'react-router',
+ 'react-router-redux',
+ 'react-transition-group',
+ 'redux',
+ 'redux-catch',
+ 'redux-immutablejs',
+ 'reselect',
+ 't8on',
+ 'webpack-hot-middleware',
+ 'zxcvbn'
+ ]
+ },
+ output: {
+ filename: 'assets/[name].js',
+ library: '[name]',
+ libraryTarget: 'var',
+ path: path.join(context, 'public')
+ },
+ plugins: [
+ new webpack.DllPlugin({
+ name: '[name]',
+ path: path.join(context, 'public/[name]-manifest.json')
+ })
+ ],
+ resolve: {
+ extensions: ['.js', '.jsx', '.less', '.css']
+ },
+ target: 'web'
+};
diff --git a/res/webpack/client.js b/res/webpack/client.js
new file mode 100644
index 00000000..c41fd2cd
--- /dev/null
+++ b/res/webpack/client.js
@@ -0,0 +1,146 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const path = require('path');
+const merge = require('lodash/merge');
+
+const webpack = require('webpack');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const ManifestPlugin = require('webpack-manifest-plugin');
+const ZopfliPlugin = require('zopfli-webpack-plugin');
+
+const baseConfiguration = require('./base');
+const rules = require('./rules');
+
+const
+ context = baseConfiguration.context,
+ NODE_ENV = process.env.NODE_ENV,
+ __DEV__ = NODE_ENV !== 'production';
+
+const clientConfiguration = merge({}, baseConfiguration, {
+ output: {
+ filename: __DEV__ ? 'assets/[name].js' : 'assets/[name]-[chunkhash].js',
+ path: `${context}/public`
+ },
+ module: {
+ rules: [
+ ...rules.clientLibFixes(),
+ ...rules.clientJS(__DEV__, context),
+ ...rules.css(__DEV__),
+ ...rules.less(__DEV__, context),
+ ...rules.fonts(__DEV__),
+ ...rules.favicons(),
+ ...rules.images(),
+ ]
+ },
+ plugins: [
+ new webpack.IgnorePlugin(/^ioredis$/),
+ new webpack.ContextReplacementPlugin(/moment[\\/]locale$/, /en/),
+ new webpack.optimize.CommonsChunkPlugin({
+ filename: __DEV__ ? 'assets/[name].js' : 'assets/[name]-[chunkhash].js',
+ name: 'manifest',
+ minChunks: Infinity
+ })
+ ],
+ target: 'web'
+});
+
+if (__DEV__) {
+ merge(clientConfiguration, {
+ devtool: 'eval',
+ entry: {
+ app: [
+ 'babel-polyfill',
+ 'react-hot-loader/patch',
+ 'webpack-hot-middleware/client?path=/__webpack_hmr',
+ `${context}/src/scripts/app.js`
+ ],
+ uikit: [
+ 'babel-polyfill',
+ 'react-hot-loader/patch',
+ 'webpack-hot-middleware/client?path=/__webpack_hmr',
+ `${context}/src/uikit/scripts.js`
+ ]
+ },
+ plugins: [
+ new webpack.DllReferencePlugin({
+ context,
+ name: 'vendor',
+ manifest: require(`${context}/public/vendor-manifest.json`), // eslint-disable-line
+ extensions: ['.js', '.jsx']
+ }),
+ new webpack.NamedChunksPlugin(),
+ new webpack.HotModuleReplacementPlugin(),
+ new ManifestPlugin({
+ fileName: 'webpack-chunks.json',
+ publicPath: '/',
+ writeToFileEmit: true
+ })
+ ],
+ resolve: {
+ unsafeCache: true
+ }
+ });
+} else {
+ merge(clientConfiguration, {
+ entry: {
+ app: ['babel-polyfill', `${context}/src/scripts/app.js`],
+ uikit: ['babel-polyfill', `${context}/src/uikit/scripts.js`]
+ },
+ plugins: [
+ new webpack.EnvironmentPlugin([
+ 'API_HOST', 'NODE_ENV', 'MAPBOX_ACCESS_TOKEN',
+ 'GOOGLE_ANALYTICS_ID', 'GOOGLE_TAG_MANAGER_ID'
+ ]),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ warnings: false,
+ pure_getters: true,
+ unsafe: true,
+ unsafe_comps: true,
+ screw_ie8: true
+ },
+ exclude: [/\.min\.js$/gi],
+ output: {
+ comments: false
+ },
+ sourceMap: false
+ }),
+ new ExtractTextPlugin({
+ filename: path.join('assets/styles/[name]-[contentHash].css'),
+ allChunks: true
+ }),
+ // cannot push to __DEV__-independent config
+ // since the ordering makes sense (after ExtractTextPlugin in this case)
+ new ManifestPlugin({
+ fileName: 'webpack-chunks.json',
+ publicPath: '/',
+ writeToFileEmit: true
+ }),
+ new webpack.optimize.AggressiveMergingPlugin({
+ minSizeReduce: true
+ }),
+ new ZopfliPlugin({
+ algorithm: 'zopfli',
+ asset: '[path].gz[query]',
+ minRatio: 0.8
+ })
+ ]
+ });
+}
+
+module.exports = clientConfiguration;
diff --git a/res/webpack/rules.js b/res/webpack/rules.js
new file mode 100644
index 00000000..c7da34b2
--- /dev/null
+++ b/res/webpack/rules.js
@@ -0,0 +1,247 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const path = require('path');
+const ExtractTextPlugin = require('extract-text-webpack-plugin');
+const omit = require('lodash/omit');
+
+/** Embed font in CSS if dev */
+function fontLoader(options, __DEV__) {
+ const loaderObject = {};
+ loaderObject.options = options;
+ if (__DEV__) {
+ loaderObject.loader = 'url-loader';
+ loaderObject.options.limit = 150000;
+ } else {
+ loaderObject.loader = 'file-loader';
+ }
+ return loaderObject;
+}
+
+module.exports = {};
+
+module.exports.clientJS = (__DEV__, context) => [{
+ test: /\.js?$/,
+ include: [path.join(context, 'src'), path.join(context, 'public/assets')],
+ exclude: /(node_modules)/,
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: true,
+ ignore: /(node_modules)/,
+ presets: [
+ 'react',
+ ['es2015', { modules: false }],
+ 'stage-1'
+ ],
+ plugins: (function () {
+ const plugins = [
+ 'syntax-do-expressions',
+ 'transform-do-expressions',
+ 'lodash'
+ ];
+
+ if (__DEV__) {
+ plugins.push(
+ 'transform-runtime'
+ /*, ['flow-runtime', { annotate: false, assert: true, warn: true }] */
+ );
+ } else {
+ plugins.push(
+ 'transform-react-constant-elements',
+ 'transform-react-inline-elements'
+ );
+ }
+
+ return plugins;
+ })()
+ }
+}];
+
+module.exports.clientLibFixes = () => [{
+ enforce: 'post',
+ test: /\.js$/,
+ include: /node_modules\/grapheme-breaker/,
+ loader: 'transform-loader/cacheable?brfs'
+}];
+
+module.exports.css = (__DEV__) => [{
+ test: /\.css$/
+}].map(rule => {
+ let use;
+ if (__DEV__) {
+ use = [
+ { loader: 'style-loader?sourceMap' },
+ { loader: 'css-loader?sourceMap' },
+ { loader: 'postcss-loader?sourceMap' }
+ ];
+ } else {
+ use = ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ publicPath: '../../',
+ use: [
+ { loader: 'css-loader',
+ options: {
+ autoprefixer: false,
+ calc: false,
+ mergeIdents: false,
+ mergeRules: false,
+ uniqueSelectors: false
+ } },
+ { loader: 'postcss-loader' }
+ ]
+ });
+ }
+
+ return Object.assign(rule, { use });
+});
+
+module.exports.ejsIndexTemplate = () => [{
+ test: /\/index\.ejs$/,
+ loader: 'raw-loader'
+}];
+
+module.exports.favicons = () => [{
+ test: /\.ico$/,
+ loader: 'file-loader',
+ options: {
+ name: '[name]-[hash].[ext]'
+ }
+}];
+
+module.exports.fonts = (__DEV__) => {
+ const outputName = 'assets/fonts/[name]-[hash].[ext]';
+ return [
+ { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+ include: /node_modules\/font-awesome/,
+ options: {
+ mimetype: 'image/svg+xml',
+ name: outputName } },
+ { test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
+ options: {
+ mimetype: 'application/font-woff',
+ name: outputName } },
+ { test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
+ options: {
+ mimetype: 'application/font-woff2',
+ name: outputName } },
+ { test: /\.otf(\?v=\d+\.\d+\.\d+)?$/,
+ options: {
+ mimetype: 'application/x-font-opentype',
+ name: outputName } },
+ { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+ options: {
+ mimetype: 'application/x-font-truetype',
+ name: outputName } },
+ { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
+ options: {
+ mimetype: 'application/vnd.ms-fontobject',
+ name: outputName } }
+ ].map(rule => ({
+ ...omit(rule, ['options']),
+ loader: fontLoader(rule.options, __DEV__)
+ }));
+};
+
+module.exports.images = (__SERVER__) => [{
+ test: /\.(png|jpg|gif|svg)(\?v=\d+\.\d+\.\d+)?$/,
+ exclude: /node_modules|fonts/,
+ loader: 'file-loader',
+ options: {
+ emitFile: !__SERVER__,
+ name: 'assets/images/[name]-[hash].[ext]'
+ }
+}];
+
+module.exports.less = (__DEV__, context) => [{
+ resource: {
+ and: [
+ { test: /\.less$/ },
+ { include: path.join(context, 'src/less') },
+ ]
+ }
+}].map(rule => {
+ let use;
+ if (__DEV__) {
+ use = [
+ { loader: 'style-loader?sourceMap' },
+ { loader: 'css-loader?sourceMap' },
+ { loader: 'postcss-loader?sourceMap' },
+ { loader: 'less-loader?sourceMap' }
+ ];
+ } else {
+ use = ExtractTextPlugin.extract({
+ fallback: 'style-loader',
+ publicPath: '../../',
+ use: [
+ { loader: 'css-loader',
+ options: {
+ autoprefixer: false,
+ calc: false,
+ mergeIdents: false,
+ mergeRules: false,
+ uniqueSelectors: false
+ } },
+ { loader: 'postcss-loader' },
+ { loader: 'less-loader' }
+ ]
+ });
+ }
+
+ return Object.assign(rule, { use });
+});
+
+module.exports.serverJS = (__DEV__, context) => [{
+ test: /\.js?$/,
+ include: [
+ path.join(context, 'server.js'),
+ path.join(context, 'tasks.js'),
+ path.join(context, 'src'),
+ ],
+ exclude: /(node_modules)/,
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: true,
+ ignore: /(node_modules)/,
+ presets: ["react"],
+ plugins: (() => {
+ const plugins = [
+ "syntax-class-properties",
+ "syntax-do-expressions",
+ "syntax-dynamic-import",
+ "syntax-object-rest-spread",
+ "transform-do-expressions",
+ "transform-object-rest-spread",
+ "transform-class-properties",
+ "lodash"
+ ];
+
+ /*
+ if (__DEV__) {
+ plugins.push(
+ ["flow-runtime", {
+ "annotate": false,
+ "assert": true,
+ "warn": true
+ }]
+ );
+ }
+ */
+
+ return plugins;
+ })(),
+ }
+}];
diff --git a/res/webpack/server.js b/res/webpack/server.js
new file mode 100644
index 00000000..bde80991
--- /dev/null
+++ b/res/webpack/server.js
@@ -0,0 +1,55 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const merge = require('lodash/merge');
+const nodeExternals = require('webpack-node-externals');
+
+const baseConfiguration = require('./base');
+const rules = require('./rules');
+
+const
+ context = baseConfiguration.context,
+ NODE_ENV = process.env.NODE_ENV,
+ __DEV__ = NODE_ENV !== 'production',
+ __SERVER__ = true;
+
+const serverConfiguration = merge({}, baseConfiguration, {
+ entry: {
+ server: `${context}/server.js`,
+ },
+ externals: [
+ nodeExternals()
+ ],
+ module: {
+ rules: [
+ ...rules.serverJS(__DEV__, context),
+ ...rules.ejsIndexTemplate(),
+ ...rules.images(__SERVER__)
+ ]
+ },
+ node: {
+ __dirname: false,
+ __filename: false
+ },
+ output: {
+ libraryTarget: 'commonjs2',
+ path: `${context}/public/server`
+ },
+ target: 'node'
+});
+
+module.exports = serverConfiguration;
diff --git a/res/webpack/tasks.js b/res/webpack/tasks.js
new file mode 100644
index 00000000..1357e675
--- /dev/null
+++ b/res/webpack/tasks.js
@@ -0,0 +1,50 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+const merge = require('lodash/merge');
+const nodeExternals = require('webpack-node-externals');
+
+const baseConfiguration = require('./base');
+const rules = require('./rules');
+
+const
+ context = baseConfiguration.context,
+ NODE_ENV = process.env.NODE_ENV,
+ __DEV__ = NODE_ENV !== 'production';
+
+const tasksConfiguration = merge({}, baseConfiguration, {
+ entry: {
+ tasks: `${context}/tasks.js`,
+ },
+ externals: [
+ nodeExternals()
+ ],
+ module: {
+ rules: rules.serverJS(__DEV__, context)
+ },
+ node: {
+ __dirname: false,
+ __filename: false
+ },
+ output: {
+ libraryTarget: 'commonjs2',
+ path: `${context}/public/server`
+ },
+ target: 'node'
+});
+
+module.exports = tasksConfiguration;
diff --git a/server-uwsettings.js b/server-uwsettings.js
deleted file mode 100644
index 3e80e675..00000000
--- a/server-uwsettings.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
- server: {
- input: `${__dirname}/server.js`,
- output: `${__dirname}/public/server/server.js`,
- publicPath: '/'
- }
-};
diff --git a/server.js b/server.js
index 82d9ed75..2f1577c8 100644
--- a/server.js
+++ b/server.js
@@ -39,7 +39,6 @@ import redis from 'redis';
import t from 't8on';
import createRequestLogger from './src/utils/bunyan-koa-request';
-import { initApi } from './src/api/routing';
import initBookshelf from './src/api/db';
import initSphinx from './src/api/sphinx';
import { API_HOST } from './src/config';
@@ -183,11 +182,15 @@ const initReduxForMainApp = async (ctx) => {
const serve = (...params) => staticCache(...params);
-function startServer(/*params*/) {
+module.exports = function startServer(attachChangeCallback) {
const sphinx = initSphinx();
- const api = initApi(bookshelf);
- const staticsRoot = path.join(__dirname, '..'); // calculated starting from "public/server/server.js"
+ let staticsRoot;
+ if (process.env.NODE_ENV === 'production') {
+ staticsRoot = path.join(__dirname, '..');
+ } else {
+ staticsRoot = path.join(__dirname, 'public');
+ }
const staticsAppConfig = {
buffer: true,
@@ -236,8 +239,24 @@ function startServer(/*params*/) {
}
});
- if (exec_env === 'development') {
+ if (process.env.NODE_ENV !== 'production' && dbEnv === 'development') {
logger.level('debug');
+
+ const webpack = require('webpack');
+ const webpackDevMiddleware = require('koa-webpack-dev-middleware');
+ const webpackHotMiddleware = require('webpack-koa-hot-middleware').default;
+ const webpackConfig = require('./res/webpack/client');
+ const compiler = webpack(webpackConfig);
+
+ app.use(convert(webpackDevMiddleware(compiler, {
+ log: logger.debug.bind(logger),
+ path: '/__webpack_hmr',
+ publicPath: webpackConfig.output.publicPath,
+ stats: {
+ colors: true
+ }
+ })));
+ app.use(convert(webpackHotMiddleware(compiler)));
}
app.use(createRequestLogger({ level: 'info', logger }));
@@ -269,7 +288,16 @@ function startServer(/*params*/) {
app.use(koaConditional());
app.use(koaEtag());
- app.use(mount('/api/v1', api));
+ if (process.env.NODE_ENV !== 'production' && attachChangeCallback) {
+ const setupApiReload = require('./src/utils/reload-api').default;
+ const reloadApi = setupApiReload(app, bookshelf, sphinx);
+ attachChangeCallback(reloadApi);
+ reloadApi(true); // initialize the API
+ } else {
+ const { initApi } = require('./src/api/routing');
+ app.use(mount('/api/v1', initApi(bookshelf, sphinx)));
+ }
+
app.use(staticsApp);
app.use(mount('/uikit', getReactMiddleware(
'uikit',
@@ -294,6 +322,4 @@ function startServer(/*params*/) {
});
return app;
-}
-
-export default startServer;
+};
diff --git a/src/components/dev-container.js b/src/components/dev-container.js
new file mode 100644
index 00000000..828c6103
--- /dev/null
+++ b/src/components/dev-container.js
@@ -0,0 +1,41 @@
+/*
+ This file is a part of libertysoil.org website
+ Copyright (C) 2017 Loki Education (Social Enterprise)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Provider } from 'react-redux';
+import { Router } from 'react-router';
+
+import { getRoutes } from '../routing';
+
+export default class DevContainer extends React.PureComponent {
+ static propTypes = {
+ handlers: PropTypes.arrayOf(PropTypes.any),
+ history: PropTypes.shape(),
+ store: PropTypes.shape()
+ };
+
+ render() {
+ return (
+
+
+ {getRoutes(...this.props.handlers)}
+
+
+ );
+ }
+}
diff --git a/src/scripts/app.js b/src/scripts/app.js
index 5cdbb56d..42593608 100644
--- a/src/scripts/app.js
+++ b/src/scripts/app.js
@@ -19,12 +19,10 @@ import bluebird from 'bluebird';
import 'raf/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
-import { Provider } from 'react-redux';
-import { Router, browserHistory } from 'react-router';
+import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import t from 't8on';
-import { getRoutes } from '../routing';
import { isStorageAvailable } from '../utils/browser';
import { AuthHandler, FetchHandler } from '../utils/loader';
import { API_HOST } from '../config';
@@ -65,12 +63,50 @@ if (!is_logged_in) {
const authHandler = new AuthHandler(store);
const fetchHandler = new FetchHandler(store, client);
+const handlers = [
+ authHandler.handle,
+ fetchHandler.handle,
+ fetchHandler.handleChange
+];
-ReactDOM.hydrate(
-
-
- {getRoutes(authHandler.handle, fetchHandler.handle, fetchHandler.handleChange)}
-
- ,
- document.getElementById('content')
-);
+let render;
+if (process.env.NODE_ENV !== 'production' && module.hot) {
+ const ReactHotLoader = require('react-hot-loader').AppContainer;
+ const composeWith = (DevContainer) => (
+
+
+
+ );
+
+ render = function () {
+ const { default: DevContainer } = require('../components/dev-container');
+
+ ReactDOM.hydrate(
+ composeWith(DevContainer),
+ document.getElementById('content')
+ );
+ };
+
+ module.hot.accept('../components/dev-container', render);
+} else {
+ const { Provider } = require('react-redux');
+ const { Router } = require('react-router');
+ const { getRoutes } = require('../routing');
+
+ render = function () {
+ ReactDOM.hydrate(
+
+
+ {getRoutes(...handlers)}
+
+ ,
+ document.getElementById('content')
+ );
+ };
+}
+
+render();
diff --git a/src/utils/koa-react.js b/src/utils/koa-react.js
index 4117e924..699bf6d6 100644
--- a/src/utils/koa-react.js
+++ b/src/utils/koa-react.js
@@ -1,5 +1,5 @@
import fs from 'fs';
-import path from 'path';
+// import path from 'path';
import React from 'react';
@@ -20,11 +20,9 @@ import { AuthHandler, FetchHandler } from './loader';
const matchPromisified = promisify(match, { multiArgs: true });
const readFile = promisify(fs.readFile);
-const isTest = ['test', 'travis'].includes(process.env.DB_ENV);
-
let template;
-if (isTest) {
- template = ejs.compile(fs.readFileSync(path.resolve(__dirname, '../views/index.ejs'), 'utf8'));
+if (typeof templateData === 'function') {
+ template = templateData;
} else {
template = ejs.compile(templateData, { filename: 'index.ejs' });
}
@@ -35,10 +33,7 @@ export function getReactMiddleware(appName, prefix, getRoutes, reduxInitializer,
const reactMiddleware = async (ctx) => {
if (!webpackChunks) {
try {
- let chunksFilename = `${__dirname}/../webpack-chunks.json`;
- if (isTest) {
- chunksFilename = `${__dirname}/../../public/webpack-chunks.json`;
- }
+ const chunksFilename = `${__dirname}/../../public/webpack-chunks.json`;
const data = await readFile(chunksFilename);
webpackChunks = JSON.parse(data);
@@ -96,6 +91,7 @@ export function getReactMiddleware(appName, prefix, getRoutes, reduxInitializer,
);
+
const state = JSON.stringify(store.getState().toJS());
if (fetchHandler.status !== null) {
@@ -111,6 +107,10 @@ export function getReactMiddleware(appName, prefix, getRoutes, reduxInitializer,
webpackChunks,
};
+ if (process.env.NODE_ENV !== 'production') {
+ paths.webpackChunks['vendor.js'] = '/assets/vendor.js';
+ }
+
ctx.staus = 200;
ctx.body = template({ appName, state, html, metadata, gtm, localization, paths });
} catch (e) {
diff --git a/src/utils/reload-api.js b/src/utils/reload-api.js
new file mode 100644
index 00000000..ffaff1dd
--- /dev/null
+++ b/src/utils/reload-api.js
@@ -0,0 +1,19 @@
+import mount from 'koa-mount';
+
+export default function setupApiReloader(app, bookshelf, sphinx) {
+ let middleware;
+ return function handleChange(initial) {
+ const { initApi } = require('../api/routing');
+ const api = initApi(bookshelf, sphinx);
+
+ if (initial) {
+ middleware = mount('/api/v1', api);
+ app.use(middleware);
+ } else {
+ const i = app.middleware.findIndex(m => m === middleware);
+ if (i >= 0) {
+ app.middleware[i] = middleware = mount('/api/v1', api);
+ }
+ }
+ };
+}
diff --git a/src/views/index.ejs b/src/views/index.ejs
index 77d96f3c..39290985 100644
--- a/src/views/index.ejs
+++ b/src/views/index.ejs
@@ -20,8 +20,12 @@
<%- metadata.title %>
-
-
+ <% if (paths.webpackChunks['vendor.css']) { %>
+
+ <% } %>
+ <% if (paths.webpackChunks['app.css']) { %>
+
+ <% } %>
<% if (gtm) { %>
@@ -45,9 +49,9 @@
var state = <%- state %>;
<%- html %>
-
-
-
+
+
+