diff --git a/.babelrc b/.babelrc index e68d2fea..0171979d 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { + "plugins": ["lodash"], "presets": ["es2015", "stage-2", "react"] } diff --git a/.eslintrc.js b/.eslintrc.js index 5dceb630..fb9325fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,8 @@ module.exports = { "jest/no-focused-tests": "error", "jest/no-identical-title": "error", "jest/valid-expect": "error", - "no-confusing-arrow": ["error", {"allowParens": true}] + "no-confusing-arrow": ["error", {"allowParens": true}], + "no-plusplus": ["error", {"allowForLoopAfterthoughts": true}] }, "plugins": [ "jest", diff --git a/.gitignore b/.gitignore index 8bc09423..4c5299a4 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ public/dist/ # webstorm files .idea +webpack-stats.json diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..1d4a52e0 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./node_modules/.bin/forever build/app.js \ No newline at end of file diff --git a/README.md b/README.md index 1bfd7f7b..7355bae2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A phone banking solution powered by Twilio # local development (Mac) ## dev prerequisites -- node v6.11.1 +- node v12.x - npm v3.10.10 - postgres v9.6.3 diff --git a/package.json b/package.json index 41ce9502..a9197888 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "phonebank", "version": "0.0.0", "private": true, + "engines": { + "node": "12.x" + }, "scripts": { "babel": "babel --presets=env,es2015,stage-1 server --out-dir build", "db": "psql -U $PG_USER -d $PG_USER", @@ -11,13 +14,16 @@ "db:seed": "babel-node --presets env,es2015,stage-1 server/db/seed.js", "db:refresh": "npm run db:teardown && npm run db:setup && npm run db:seed", "lint": "eslint --ext js --ext jsx .", - "build": "webpack -d --watch && cp public/index.html dist/index.html && webpack-dev-server --content-base src/ --inline", - "build:prod": "webpack -p && cp public/index.html dist/index.html", + "build": "webpack -d --watch", + "build:prod": "webpack -p", + "build:profile": "webpack --profile --json > webpack-stats.json", "start": "nodemon server/app.js --exec babel-node --presets env,es2015,stage-1", "test": "npm run lint && npm run test:server && npm run test:client", "test:client": "jest", "test:client:refresh": "jest --updateSnapshot", - "test:server": "PG_CONNECTION_STRING=$PG_CONNECTION_STRING_TEST DEBUG=true mocha './test/server' --recursive --compilers js:babel-core/register" + "test:server": "PG_CONNECTION_STRING=$PG_CONNECTION_STRING_TEST DEBUG=true mocha './test/server' --recursive --compilers js:babel-core/register", + "heroku-prebuild": "export NPM_CONFIG_PRODUCTION=false; npm install --only=dev", + "heroku-postbuild": "export NPM_CONFIG_PRODUCTION=true; npm run babel; npm run build:prod;" }, "jest": { "verbose": true, @@ -26,30 +32,32 @@ } }, "dependencies": { - "axios": "^0.16.2", + "axios": "^0.20.0", "axios-mock-adapter": "^1.9.0", - "bcrypt": "^1.0.2", - "body-parser": "~1.17.1", + "bcrypt": "^5.0.0", + "body-parser": "^1.19.0", "bookshelf": "^0.10.4", - "bookshelf-bcrypt": "^2.1.0", + "bookshelf-bcrypt": "^3.0.2", "chai": "^4.1.0", "chai-as-promised": "^7.1.1", "chai-http": "^3.0.0", - "chart.js": "^2.7.0", + "chart.js": "^2.9.3", "concurrently": "^3.5.0", "cookie-parser": "~1.4.3", + "cryptiles": "^4.1.3", "csv-parse": "^1.2.1", "debug": "~2.6.3", "ejs": "^2.5.6", - "express": "~4.15.2", + "express": "^4.17.1", "express-fileupload": "^0.1.4", "faker": "^4.1.0", + "forever": "^0.15.3", "immutable": "^3.8.1", "jsonwebtoken": "^7.4.1", - "knex": "^0.13.0", - "lodash": "^4.17.4", - "moment": "^2.18.1", - "morgan": "~1.8.1", + "knex": "^0.21.5", + "lodash": "^4.17.20", + "moment": "^2.29.0", + "morgan": "^1.10.0", "passport": "^0.3.2", "passport-jwt": "^2.2.1", "passport-local": "^1.0.0", @@ -70,20 +78,22 @@ "redux-promise": "^0.5.3", "redux-promise-middleware": "^4.3.0", "redux-thunk": "^2.2.0", - "serve-favicon": "~2.4.2", - "twilio": "^3.6.5" + "serve-favicon": "^2.5.0", + "twilio": "^3.49.3" }, "devDependencies": { - "babel-cli": "^6.24.1", + "babel-cli": "^6.26.0", "babel-core": "^6.25.0", "babel-jest": "^20.0.3", "babel-loader": "^7.1.1", + "babel-plugin-lodash": "^3.2.11", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-env": "^1.6.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-1": "^6.24.1", - "bootstrap": "^3.3.7", + "bootstrap": "^3.4.1", + "compression-webpack-plugin": "^1.0.0", "css-loader": "^0.28.4", "enzyme": "^2.9.1", "eslint": "^3.19.0", @@ -94,6 +104,7 @@ "eslint-plugin-react": "^7.1.0", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", + "html-webpack-plugin": "^2.30.1", "jest": "^20.0.4", "less": "^2.7.2", "less-loader": "^4.0.5", @@ -104,7 +115,8 @@ "request": "^2.81.0", "style-loader": "^0.18.2", "url-loader": "^0.5.9", - "webpack": "^3.3.0", - "webpack-dev-server": "^2.5.1" + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-server": "^2.11.5" } } diff --git a/public/index.html b/public/index.html index 1ccfac1b..f674f973 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,10 @@
- + <% if (typeof htmlWebpackPlugin != "undefined") { for (var chunk in htmlWebpackPlugin.files.chunks) { %> + + <% } } else { %> + + <% } %> diff --git a/server/app.js b/server/app.js index 46f5af67..2f2e1812 100644 --- a/server/app.js +++ b/server/app.js @@ -24,4 +24,10 @@ server.on('listening', errorHandle.onListening); middleware(app, express); // routes(app, express); +app.get('*.js', (req, res, next) => { + req.url = `${req.url}.gz`; + res.set('Content-Encoding', 'gzip'); + next(); +}); + export default server; diff --git a/server/config/middleware.js b/server/config/middleware.js index a1a95fc0..8f2963c0 100644 --- a/server/config/middleware.js +++ b/server/config/middleware.js @@ -1,6 +1,8 @@ import logger from 'morgan'; import bodyParser from 'body-parser'; import path from 'path'; +import fs from 'fs'; +import ejs from 'ejs'; import indexRouter from './routes/index'; import scriptsRouter from './routes/scripts'; import questionsRouter from './routes/questions'; @@ -18,7 +20,22 @@ export default function middleware(app, express) { app.use(bodyParser.json()); // define where express should look for static assests - app.use(express.static(path.join(__dirname, '../../public/dist/src'))); + const staticFilesDir = path.join(__dirname, '../../public/dist/src'); + app.use(express.static(staticFilesDir)); + app.get('/main.js', (req, res) => { + // redirect to latest build with content hash + try { + const files = fs.readdirSync(staticFilesDir); + for (let i = 0; i < files.length; i++) { + if (files[i].lastIndexOf('main') === 0) { + return res.redirect(files[i]); + } + } + return res.send('alert("No main.js found! You may need to run npm build.");'); + } catch (error) { + return res.send('alert("No public/dist/src dir! You may need to run npm build.");'); + } + }); // use passport for authentication app.use(passport.initialize()); @@ -31,6 +48,8 @@ export default function middleware(app, express) { app.use('/users', usersRouter); app.use('/auth', authRouter); app.use('/campaigns', campaignsRouter); + + app.engine('html', ejs.renderFile); app.use('*', indexRouter); // pass the logger diff --git a/server/controllers/index.js b/server/controllers/index.js index 85e570e7..9c6775a4 100644 --- a/server/controllers/index.js +++ b/server/controllers/index.js @@ -1,5 +1,9 @@ import path from 'path'; export default function serveReactApp(req, res) { - res.sendFile(path.resolve(__dirname, '../../public/index.html')); + if (process.env.NODE_ENV === 'production') { + res.sendFile(path.resolve(__dirname, '../../public/dist/src/index.html')); + } else { + res.render(path.resolve(__dirname, '../../public/index.html')); + } } diff --git a/server/db/db.js b/server/db/db.js index 6440ad6d..dd6a0a12 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -4,7 +4,7 @@ import knexModule from 'knex'; const config = { client: 'pg', - connection: process.env.PG_CONNECTION_STRING, + connection: process.env.PG_CONNECTION_STRING || process.env.DATABASE_URL, debug: process.env.DEBUG }; const knex = knexModule(config); diff --git a/webpack.config.js b/webpack.config.js index 328c48fd..9cbe2852 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,9 +1,12 @@ const path = require('path'); +const webpack = require('webpack'); const SRC_DIR = path.resolve(__dirname, 'public'); const DIST_DIR = path.resolve(__dirname, 'public/dist'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const CompressionPlugin = require('compression-webpack-plugin'); const extractSass = new ExtractTextPlugin({ filename: '[name].[contenthash].css', @@ -14,7 +17,7 @@ module.exports = { entry: `${SRC_DIR}/src/index.jsx`, output: { path: `${DIST_DIR}/src/`, - filename: 'bundle.js', + filename: '[name].[chunkhash:8].js', publicPath: '/' }, resolve: { @@ -28,6 +31,7 @@ module.exports = { exclude: /node_modules/, loader: 'babel-loader', query: { + plugins: ['lodash'], presets: ['react', 'es2015', 'stage-1', 'env'] } }, @@ -57,7 +61,21 @@ module.exports = { ] }, plugins: [ - new ExtractTextPlugin('style.css') + new HtmlWebpackPlugin({ + inject: false, + template: 'public/index.html', + }), + new ExtractTextPlugin('style.css'), + new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/), // add more locales here if needed + new webpack.optimize.UglifyJsPlugin(), + new webpack.optimize.AggressiveMergingPlugin(), + new CompressionPlugin({ + asset: "[path].gz[query]", + algorithm: "gzip", + test: /\.js$|\.css$|\.html$/, + threshold: 10240, + minRatio: 0.8 + }) ] };