diff --git a/client/index.js b/client/index.js index 4fce290f7f..f00e2c74b8 100644 --- a/client/index.js +++ b/client/index.js @@ -23,9 +23,9 @@ import { Rx.config.longStackSupport = !!debug.enabled; const log = debug('fcc:client'); -const DOMContainer = document.getElementById('fcc'); const hotReloadTimeout = 5000; const csrfToken = window.__fcc__.csrf.token; +const DOMContainer = document.getElementById('fcc'); const initialState = isColdStored() ? getColdStorage() : window.__fcc__.data; diff --git a/common/app/routes/Hikes/index.js b/common/app/routes/Hikes/index.js index b759608fa9..206bf97b4d 100644 --- a/common/app/routes/Hikes/index.js +++ b/common/app/routes/Hikes/index.js @@ -1,11 +1,24 @@ -import Hikes from './components/Hikes.jsx'; -import Hike from './components/Hike.jsx'; - export default { path: 'videos', - component: Hikes, - childRoutes: [{ - path: ':dashedName', - component: Hike - }] + getComponent(_, cb) { + require.ensure( + [ './components/Hikes.jsx' ], + require => { + cb(null, require('./components/Hikes.jsx').default); + }, + 'hikes' + ); + }, + getChildRoutes(_, cb) { + require.ensure( + [ './components/Hike.jsx' ], + require => { + cb(null, [{ + path: ':dashedName', + component: require('./components/Hike.jsx').default + }]); + }, + 'hikes' + ); + } }; diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index b388bdc577..1c7dfa85ed 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,34 +1,36 @@ -import Jobs from './components/Jobs.jsx'; -import NewJob from './components/NewJob.jsx'; -import Show from './components/Show.jsx'; -import Preview from './components/Preview.jsx'; -import JobTotal from './components/JobTotal.jsx'; -import NewJobCompleted from './components/NewJobCompleted.jsx'; - -/* - * index: /jobs list jobs - * show: /jobs/:id show one job - * create /jobs/new create a new job - */ - export default { - childRoutes: [{ - path: '/jobs', - component: Jobs - }, { - path: 'jobs/new', - component: NewJob - }, { - path: 'jobs/new/preview', - component: Preview - }, { - path: 'jobs/new/check-out', - component: JobTotal - }, { - path: 'jobs/new/completed', - component: NewJobCompleted - }, { - path: 'jobs/:id', - component: Show - }] + getChildRoutes: (_, cb) => { + require.ensure( + [ + './components/Jobs.jsx', + './components/NewJob.jsx', + './components/Preview.jsx', + './components/JobTotal.jsx', + './components/NewJobCompleted.jsx', + './components/Show.jsx' + ], + require => { + cb(null, [{ + path: '/jobs', + component: require('./components/Jobs.jsx').default + }, { + path: 'jobs/new', + component: require('./components/NewJob.jsx').default + }, { + path: 'jobs/new/preview', + component: require('./components/Preview.jsx').default + }, { + path: 'jobs/new/check-out', + component: require('./components/JobTotal.jsx').default + }, { + path: 'jobs/new/completed', + component: require('./components/NewJobCompleted.jsx').default + }, { + path: 'jobs/:id', + component: require('./components/Show.jsx').default + }]); + }, + 'jobs' + ); + } }; diff --git a/gulpfile.js b/gulpfile.js index a12c623354..a98ed1b5b7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -5,27 +5,28 @@ require('babel-core/register'); var Rx = require('rx'), gulp = require('gulp'), path = require('path'), + debug = require('debug')('fcc:gulp'), + yargs = require('yargs'), + sortKeys = require('sort-keys'), + del = require('del'), // utils plumber = require('gulp-plumber'), notify = require('gulp-notify'), gutil = require('gulp-util'), reduce = require('gulp-reduce-file'), - sortKeys = require('sort-keys'), - debug = require('debug')('fcc:gulp'), - yargs = require('yargs'), concat = require('gulp-concat'), uglify = require('gulp-uglify'), merge = require('merge-stream'), babel = require('gulp-babel'), sourcemaps = require('gulp-sourcemaps'), + gulpif = require('gulp-if'), // react app webpack = require('webpack'), webpackStream = require('webpack-stream'), WebpackDevServer = require('webpack-dev-server'), webpackConfig = require('./webpack.config.js'), - webpackConfigNode = require('./webpack.config.node.js'), // server process nodemon = require('gulp-nodemon'), @@ -190,7 +191,7 @@ function delRev(dest, manifestName) { }); } -gulp.task('serve', ['build-manifest'], function(cb) { +gulp.task('serve', function(cb) { var called = false; nodemon({ script: paths.server, @@ -225,8 +226,7 @@ var syncDepenedents = [ 'serve', 'js', 'less', - 'dependents', - 'build-manifest' + 'dependents' ]; gulp.task('sync', syncDepenedents, function() { @@ -264,29 +264,43 @@ gulp.task('test-challenges', ['lint-json']); gulp.task('pack-client', function() { if (!__DEV__) { console.log('\n\nbundling production\n\n'); } - var manifestName = 'react-manifest.json'; + function condition(file) { + var filepath = file.relative; + return __DEV__ || (/json$/).test('' + filepath); + } + var dest = webpackConfig.output.path; - return gulp.src(webpackConfig.entry) + return gulp.src(webpackConfig.entry.bundle) .pipe(plumber({ errorHandler: errorHandler })) .pipe(webpackStream(Object.assign( {}, webpackConfig, webpackOptions ))) - .pipe(__DEV__ ? gutil.noop() : uglify()) - .pipe(gulp.dest(dest)) - .pipe(rev()) - // copy files to public - .pipe(gulp.dest(dest)) - // create manifest - .pipe(rev.manifest(manifestName)) - // delete old rev - .pipe(delRev( - dest, - manifestName - )) - .pipe(gulp.dest(paths.manifest)); + .pipe(gulpif(condition, gutil.noop(), uglify())) + .pipe(gulp.dest(dest)); +}); + +var webpackManifestFiles = [ 'react-manifest.json', 'chunk-manifest.json' ]; +gulp.task('move-webpack-manifest', ['pack-client'], function() { + var files = webpackManifestFiles.map(function(filename) { + return path.join(webpackConfig.output.path, filename); + }); + return gulp.src(files).pipe(gulp.dest(paths.manifest)); +}); + +var cleanDeps = ['pack-client', 'move-webpack-manifest']; +gulp.task('clean-webpack-manifest', cleanDeps, function() { + return del(webpackManifestFiles.map(function(filename) { + return path.join(webpackConfig.output.path, filename); + })) + .then(function(pathsDeleted) { + gutil.log('[clean-webpack-manifest]', 'paths deleted' + pathsDeleted); + }) + .catch(function(err) { + throw new gutil.PluginError('clean-webpack-manifest', err); + }); }); var webpackCalled = false; @@ -304,10 +318,10 @@ gulp.task('webpack-dev-server', function(cb) { contentBase: false, publicPath: '/js' }; - webpackConfig.entry = [ + webpackConfig.entry.bundle = [ 'webpack-dev-server/client?http://localhost:2999/', 'webpack/hot/dev-server' - ].concat(webpackConfig.entry); + ].concat(webpackConfig.entry.bundle); var compiler = webpack(webpackConfig); var devServer = new WebpackDevServer(compiler, devServerOptions); @@ -329,31 +343,6 @@ gulp.task('webpack-dev-server', function(cb) { }); }); -gulp.task('pack-watch-manifest', function() { - var manifestName = 'react-manifest.json'; - var dest = webpackConfig.output.path; - return gulp.src(dest + '/bundle.js') - .pipe(rev()) - // copy files to public - .pipe(gulp.dest(dest)) - // create manifest - .pipe(rev.manifest(manifestName)) - .pipe(delRev( - dest, - manifestName - )) - .pipe(gulp.dest(paths.manifest)); -}); - -gulp.task('pack-node', function() { - return gulp.src(webpackConfigNode.entry) - .pipe(plumber({ errorHandler: errorHandler })) - .pipe(webpack(webpackConfigNode)) - .pipe(gulp.dest(webpackConfigNode.output.path)); -}); - -gulp.task('pack', ['pack-client', 'pack-node']); - gulp.task('less', function() { var manifestName = 'css-manifest.json'; var dest = paths.css; @@ -482,24 +471,18 @@ function done(manifest) { return sortKeys(manifest); } -function buildManifest() { +var buildDependents = [ + 'less', + 'js', + 'dependents', + 'pack-client', + 'move-webpack-manifest' +]; + +gulp.task('build-manifest', buildDependents, function() { return gulp.src(paths.manifest + '*.json') .pipe(reduce('rev-manifest.json', collector, done, {})) .pipe(gulp.dest('server/')); -} - -var buildDependents = ['less', 'js', 'dependents']; - -if (__DEV__) { - buildDependents.push('pack-watch-manifest'); -} - -gulp.task('build-manifest', buildDependents, function() { - return buildManifest(); -}); - -gulp.task('build-manifest-watch', function() { - return buildManifest(); }); gulp.task('build', [ @@ -507,6 +490,8 @@ gulp.task('build', [ 'js', 'dependents', 'pack-client', + 'move-webpack-manifest', + 'clean-webpack-manifest', 'build-manifest' ]); @@ -515,8 +500,7 @@ var watchDependents = [ 'js', 'dependents', 'serve', - 'sync', - 'build-manifest' + 'sync' ]; gulp.task('reload', function() { @@ -533,14 +517,12 @@ gulp.task('watch', watchDependents, function() { formatCommonFrameworkPaths.call(paths.commonFramework), ['dependents'] ); - gulp.watch(paths.manifest + '/*.json', ['build-manifest-watch']); }); gulp.task('default', [ 'less', 'serve', 'webpack-dev-server', - 'build-manifest-watch', 'watch', 'sync' ]); diff --git a/package.json b/package.json index ca604a5acd..44f4d41e38 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,7 @@ "license": "(BSD-3-Clause AND CC-BY-SA-4.0)", "dependencies": { "accepts": "^1.3.0", - "adler32": "~0.1.7", "async": "^1.5.0", - "babel-cli": "^6.3.17", - "babel-core": "^6.3.26", - "babel-eslint": "^6.1.2", - "babel-loader": "^6.2.1", - "babel-plugin-add-module-exports": "^0.2.1", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "babel-preset-stage-0": "^6.3.13", "babel-register": "^6.3.0", "body-parser": "^1.13.2", "cheerio": "~0.20.0", @@ -52,8 +43,6 @@ "emmet-codemirror": "^1.2.5", "errorhandler": "^1.4.2", "es6-map": "~0.1.1", - "eslint": "^3.1.0", - "eslint-plugin-react": "^5.1.1", "express": "^4.13.3", "express-flash": "~0.0.2", "express-session": "^1.12.1", @@ -61,32 +50,15 @@ "express-validator": "^2.18.0", "fetchr": "~0.5.12", "frameguard": "^2.0.0", - "gulp": "^3.9.0", - "gulp-babel": "^6.1.1", - "gulp-concat": "^2.6.0", - "gulp-eslint": "^3.0.1", - "gulp-jsonlint": "^1.1.0", - "gulp-less": "^3.0.3", - "gulp-nodemon": "^2.0.3", - "gulp-notify": "^2.2.0", - "gulp-plumber": "^1.0.1", - "gulp-reduce-file": "0.0.1", - "gulp-rev": "^7.0.0", - "gulp-rev-replace": "~0.4.2", - "gulp-uglify": "^1.5.1", - "gulp-util": "^3.0.6", "helmet": "^2.0.0", "helmet-csp": "^1.0.3", "history": "^2.0.0", "jade": "^1.11.0", - "json-loader": "~0.5.2", - "less": "^2.5.1", "lodash": "^4.1.0", "loopback": "^2.22.0", "loopback-boot": "^2.13.0", "loopback-component-passport": "^2.0.0", "loopback-connector-mongodb": "1.15.2", - "merge-stream": "^1.0.0", "method-override": "^2.3.0", "moment": "^2.10.2", "moment-timezone": "^0.5.0", @@ -112,6 +84,7 @@ "react-dom": "^15.0.2", "react-addons-css-transition-group": "^0.14.7", "react-css-transition-replace": "^1.1.0", + "react-fontawesome": "^0.3.3", "react-motion": "~0.4.2", "react-no-ssr": "^1.0.1", "react-pure-render": "^1.0.2", @@ -126,31 +99,65 @@ "redux-form": "^5.2.3", "request": "^2.65.0", "reselect": "^2.0.2", - "rev-del": "^1.0.5", "rx": "^4.0.0", "sanitize-html": "^1.11.1", - "sort-keys": "^1.1.1", "stampit": "^2.1.1", "store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server", "url-regex": "^3.0.0", "validator": "^5.0.0", - "webpack": "^1.9.12", - "webpack-dev-server": "^1.14.1", - "webpack-stream": "^3.1.0", "xss-filters": "^1.2.6", - "yargs": "^4.1.0", "snyk": "^1.17.1" }, "devDependencies": { + "adler32": "~0.1.7", + "babel-cli": "^6.3.17", + "babel-core": "^6.3.26", + "babel-eslint": "^6.1.2", + "babel-loader": "^6.2.1", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babel-preset-stage-0": "^6.3.13", "browser-sync": "^2.9.12", + "chunk-manifest-webpack-plugin": "0.0.1", + "del": "^2.2.0", + "eslint": "^3.1.0", + "eslint-plugin-react": "^5.1.1", + "gulp": "^3.9.0", + "gulp-babel": "^6.1.1", + "gulp-concat": "^2.6.0", + "gulp-dest": "^0.2.3", + "gulp-eslint": "^3.0.1", + "gulp-if": "^2.0.0", + "gulp-jsonlint": "^1.1.0", + "gulp-less": "^3.0.3", + "gulp-nodemon": "^2.0.3", + "gulp-notify": "^2.2.0", + "gulp-plumber": "^1.0.1", + "gulp-reduce-file": "0.0.1", + "gulp-rev": "^7.0.0", + "gulp-rev-replace": "~0.4.2", "gulp-sourcemaps": "^1.6.0", "gulp-tape": "0.0.9", + "gulp-uglify": "^1.5.1", + "gulp-util": "^3.0.6", + "happy": "0.0.1", + "json-loader": "~0.5.2", "jsonlint": "^1.6.2", + "less": "^2.5.1", "loopback-component-explorer": "^2.1.1", "loopback-testing": "^1.1.0", + "merge-stream": "^1.0.0", + "rev-del": "^1.0.5", "sinon": "^1.17.3", + "sort-keys": "^1.1.1", "tap-spec": "^4.1.1", "tape": "^4.2.2" + "webpack": "^1.9.12", + "webpack-dev-server": "^1.14.0", + "webpack-manifest-plugin": "^1.0.0", + "webpack-stream": "^3.1.0", + "yargs": "^4.1.0" }, "snyk": true } diff --git a/server/server.js b/server/server.js index 7958853ecb..99d5cf3db5 100755 --- a/server/server.js +++ b/server/server.js @@ -10,6 +10,22 @@ var _ = require('lodash'), path = require('path'), setupPassport = require('./component-passport'); +// polyfill for webpack bundle splitting +const requireProto = Object.getPrototypeOf(require); +if (!requireProto.hasOwnProperty('ensure')) { + Object.defineProperties( + requireProto, + { + 'ensure': { + value: function ensure(modules, callback) { + callback(this); + }, + writable: false, + enumarble: false + } + } + ); +} Rx.config.longStackSupport = process.env.NODE_DEBUG !== 'production'; var app = loopback(); var isBeta = !!process.env.BETA; diff --git a/webpack.config.js b/webpack.config.js index 8839a1492f..bde282836b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,13 +1,20 @@ var webpack = require('webpack'); var path = require('path'); +var ManifestPlugin = require('webpack-manifest-plugin'); +var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin'); var __DEV__ = process.env.NODE_ENV !== 'production'; module.exports = { - entry: './client', - devtool: 'inline-source-map', + entry: { + bundle: './client' + }, + devtool: __DEV__ ? 'inline-source-map' : null, output: { - filename: 'bundle.js', + filename: __DEV__ ? 'bundle.js' : 'bundle-[hash].js', + chunkFilename: __DEV__ ? + 'bundle-[name].js' : + 'bundle-[name]-[chunkhash].js', path: path.join(__dirname, '/public/js'), publicPath: __DEV__ ? 'http://localhost:2999/js' : '/js' }, @@ -42,8 +49,21 @@ module.exports = { 'NODE_ENV': JSON.stringify(__DEV__ ? 'development' : 'production') }, '__DEVTOOLS__': !__DEV__ - }), - new webpack.HotModuleReplacementPlugin(), - new webpack.NoErrorsPlugin() + }) ] }; + +if (!__DEV__) { + module.exports.plugins.push( + new ManifestPlugin({ fileName: 'react-manifest.json' }), + new ChunkManifestPlugin({ + filename: 'chunk-manifest.json', + manifestVariable: 'webpackManifest' + }) + ); +} else { + module.exports.plugins.push( + new webpack.HotModuleReplacementPlugin(), + new webpack.NoErrorsPlugin() + ); +} diff --git a/webpack.config.node.js b/webpack.config.node.js deleted file mode 100644 index f422032d85..0000000000 --- a/webpack.config.node.js +++ /dev/null @@ -1,54 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var webpack = require('webpack'); - -var nodeModules = fs.readdirSync('node_modules') - .filter(function(x) { - return ['.bin'].indexOf(x) === -1; - }) - .reduce(function(nodeModules, module) { - nodeModules[module] = 'commonjs ' + module; - return nodeModules; - }, {}); - -module.exports = { - devtool: 'sourcemap', - target: 'node', - entry: './common/app', - // keeps webpack from bundling modules - externals: nodeModules, - output: { - filename: 'app-stream.bundle.js', - path: path.join(__dirname, '/server'), - publicPath: 'public/' - }, - module: { - loaders: [ - { - test: /\.jsx?$/, - include: [ - path.join(__dirname, 'client/'), - path.join(__dirname, 'common/') - ], - loaders: [ - 'babel-loader' - ] - }, - { - test: /\.json$/, - loaders: [ - 'json-loader' - ] - } - ] - }, - plugins: [ - new webpack.BannerPlugin( - 'require("source-map-support").install();', - { - raw: true, - entryOnly: false - } - ) - ] -};