Skip to content

Instantly share code, notes, and snippets.

@colbycheeze
Last active April 15, 2017 13:59
Show Gist options
  • Save colbycheeze/6014416d719a09348ad4018cbf38e482 to your computer and use it in GitHub Desktop.
Save colbycheeze/6014416d719a09348ad4018cbf38e482 to your computer and use it in GitHub Desktop.
A configurable webpack 2 config used in a large production app
{
"name": "medspoke-redux",
"description": "A React frontend for the MedSpoke application",
"version": "0.1.0",
"author": "MedSpoke, Inc.",
"license": "GPL-3.0",
"main": "index.js",
"engines": {
"node": ">=6.9.2",
"yarn": "^0.19.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/medspoke/medspoke-redux.git"
},
"scripts": {
"start": "webpack-dev-server --config config/webpack.config.js --progress --env.sourceMap",
"dev": "yarn start",
"storybook": "start-storybook -p 9001",
"serve": "node server/server.js",
"test": "better-npm-run test",
"test:verbose": "yarn test -- --verbose",
"test:watch": "yarn test -- --watch",
"test:coverage": "yarn test -- --coverage && codecov",
"analyze": "yarn build -- --env.analyze",
"build": "better-npm-run build",
"deploy:staging": "better-npm-run deploy:staging",
"deploy:production": "better-npm-run deploy:production",
"build-storybook": "build-storybook -o ./.out",
"lint:all": "eslint src common_modules",
"lint:fix-changes": "LIST=`git diff-index --name-only HEAD | grep -E '.(js|jsx)$';`; if [[ $LIST ]]; then eslint $LIST --fix; fi"
},
"pre-commit": [
"test",
"lint:fix-changes"
],
"betterScripts": {
"test": {
"command": "jest",
"env": {
"NODE_ENV": "test"
}
},
"build": {
"command": "rimraf build && webpack --config config/webpack.config.js --bail --env.optimize --env.extractText",
"env": {
"NODE_ENV": "production"
}
},
"deploy:staging": {
"command": "yarn build",
"env": {
"deployLocation": "staging",
"apiUrl": "https://staging.api.medspoke.tech",
"S3Bucket": "react-staging"
}
},
"deploy:production": {
"command": "yarn build",
"env": {
"deployLocation": "production",
"apiUrl": "https://api.medspoke.tech",
"S3Bucket": "react-medspoke-production"
}
}
},
"jest": {
"setupFiles": [
"<rootDir>/config/jest.js"
],
"modulePaths": [ "src" ],
"moduleNameMapper": {
"^.+\\.(css|scss)$": "identity-obj-proxy"
},
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!**/index.{js,jsx}",
"!**/*.factory.{js,jsx}",
"!**/*.story.{js,jsx}",
"!**/node_modules/**",
"!**/vendor/**"
],
"globals": {
"__DEV__": false,
"__PROD__": false,
"__TEST__": true,
"__DEBUG__": false,
"__API_ROOT__": "/"
}
},
"devDependencies": {
"@kadira/storybook": "^2.35.3",
"axios-mock-adapter": "^1.7.1",
"babel-core": "^6.21.0",
"babel-eslint": "^7.1.1",
"babel-jest": "^18.0.0",
"babel-loader": "^6.2.10",
"babel-plugin-dynamic-import-webpack": "^1.0.1",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-react-transform": "^2.0.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0",
"babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-1": "^6.16.0",
"babel-register": "^6.7.2",
"babel-resolver": "0.0.18",
"better-npm-run": "0.0.13",
"codecov": "^1.0.1",
"compression-webpack-plugin": "^0.3.2",
"connect-gzip-static": "^1.0.0",
"css-loader": "^0.26.1",
"enzyme": "^2.7.0",
"enzyme-to-json": "^1.4.5",
"eslint": "^3.13.1",
"eslint-config-rallycoding": "^3.1.0",
"eslint-plugin-react": "^6.7.1",
"express": "^4.14.0",
"extract-text-webpack-plugin": "^v2.0.0-beta.5",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.26.0",
"identity-obj-proxy": "^3.0.0",
"ignore-styles": "^4.0.0",
"jest": "^18.1.0",
"node-neat": "^1.7.2",
"node-sass": "^3.4.2",
"pre-commit": "^1.2.2",
"react-addons-css-transition-group": "~15.3.0",
"react-addons-test-utils": "^15.4.2",
"react-dnd-test-backend": "^1.0.2",
"react-hot-loader": "^3.0.0-alpha.8",
"react-transform-catch-errors": "^1.0.2",
"redux-mock-store": "^1.2.1",
"rimraf": "^2.5.4",
"sass-loader": "^4.1.1",
"script-ext-html-webpack-plugin": "^1.5.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^2.2.0",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-server": "^2.2.0",
"webpack-hot-middleware": "^2.15.0",
"webpack-md5-hash": "^0.0.5",
"webpack-s3-plugin": "git://github.com/trbritt/s3-plugin-webpack.git"
},
"dependencies": {
"@blueprintjs/core": "^1.6.0",
"accounting": "^0.4.1",
"argv": "^0.0.2",
"axios": "~0.15.2",
"axios-mock-adapter": "^1.7.1",
"bourbon": "^4.2.7",
"chart.js": "^1.1.1",
"classnames": "~2.2.5",
"credit-card-type": "^5.0.0",
"cssnano": "^3.9.1",
"fuzzy": "^0.1.3",
"jwt-decode": "^2.1.0",
"kefir": "^3.7.0",
"kefir-bus": "^2.2.1",
"little-loader": "^0.2.0",
"lodash": "~4.15.0",
"moment": "~2.14.1",
"node-neat": "^1.7.2",
"normalizr": "^3.0.2",
"pdfjs-dist": "1.4.20",
"qs": "~6.2.1",
"react": "^15.4.1",
"react-addons-css-transition-group": "~15.3.0",
"react-addons-shallow-compare": "^15.4.1",
"react-breadcrumbs": "~1.3.16",
"react-chartjs": "^0.8.0",
"react-datepicker": "~0.28.2",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-dom": "^15.4.1",
"react-dropzone": "~3.5.3",
"react-flip-move": "^2.7.4",
"react-maskedinput": "^3.3.4",
"react-redux": "~4.4.5",
"react-router": "~2.6.1",
"react-selectize": "~2.0.3",
"react-sticky": "^5.0.5",
"redbox-react": "^1.3.3",
"redux": "~3.5.2",
"redux-form": "~5.3.2",
"redux-thunk": "~2.1.0",
"reselect": "^2.5.4",
"yargs": "^6.6.0"
}
}
const path = require('path')
const webpack = require('webpack')
const S3Plugin = require('webpack-s3-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin')
const WebpackMd5Hash = require('webpack-md5-hash')
const cssnano = require('cssnano')
const BASE_PATH = '..'
const resolvePath = subPath => path.resolve(__dirname, `${BASE_PATH}/${subPath}`)
module.exports = config => {
const NODE_ENV = process.env.NODE_ENV || 'development'
/* eslint-disable */
const __DEV__ = NODE_ENV === 'development'
const __PROD__ = NODE_ENV === 'production'
const __TEST__ = NODE_ENV === 'test'
/* eslint-enable */
if (!(__DEV__ || __PROD__ || __TEST__)) {
throw new Error(`Unknown NODE_ENV: ${NODE_ENV}.`)
}
/*
** IMPORTANT **
Globals added here must _also_ be added to .eslintrc to avoid 'xxx is not defined' errors
In addition, you will have to place a default value in the jest config within package.json,
*/
const globals = {
'process.env': {
NODE_ENV: JSON.stringify(NODE_ENV),
},
__DEV__,
__PROD__,
__TEST__,
__DEBUG__: config.debug,
__API_ROOT__: JSON.stringify(config.apiUrl || process.env.apiUrl || 'http://localhost:3000'),
}
// eslint-disable-next-line no-underscore-dangle
console.log(`Using ${globals.__API_ROOT__} for api calls`)
const paths = {
build: resolvePath('build'),
src: resolvePath('src'),
styles: resolvePath('src/style'),
images: resolvePath('src/images'),
}
const rules = [
// ------------------------------------
// JS / JSX / Json
// ------------------------------------
{
test: /\.jsx?$/,
exclude: /node_modules/,
include: [paths.src],
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [['es2015', { modules: false }], 'react', 'stage-1'],
plugins: ['transform-runtime', 'lodash'],
},
}],
},
]
const sassLoader = {
loader: 'sass-loader',
options: {
sourceMap: config.sourcemaps,
},
}
const baseCSSLoader = {
loader: 'css-loader',
options: {
minimize: config.optimize,
sourceMap: config.sourceMap,
},
}
// ------------------------------------
// Global Styles
// ------------------------------------
if (config.extractText) {
rules.push({
test: /\.scss$/,
include: paths.styles,
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: [
baseCSSLoader,
'postcss-loader',
sassLoader,
],
publicPath: '/',
}),
})
} else {
rules.push({
test: /\.scss$/,
include: paths.styles,
use: ['style-loader', baseCSSLoader, 'postcss-loader', sassLoader],
})
}
// ------------------------------------
// CSS Modules
// ------------------------------------
const cssModulesLoader = {
loader: 'css-loader',
query: {
minimize: config.optimize,
sourceMap: config.sourceMap,
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]__[hash:base64:5]',
},
}
if (config.extractText) {
rules.push({
test: /\.scss$/,
exclude: paths.styles,
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: [
cssModulesLoader,
'postcss-loader',
sassLoader,
],
publicPath: '/',
}),
})
} else {
rules.push({
test: /\.scss$/,
exclude: paths.styles,
use: ['style-loader', cssModulesLoader, 'postcss-loader', sassLoader],
})
}
// ------------------------------------
// File Loaders
// ------------------------------------
const createFontLoader = (test, mimetype) => ({
test,
use: [{
loader: 'url-loader',
options: {
name: 'fonts/[name].[ext]',
limit: 10000,
mimetype,
},
}],
})
rules.push(
createFontLoader(/\.woff(\?.*)?$/, 'application/font-woff'),
createFontLoader(/\.woff2(\?.*)?$/, 'application/font-woff2'),
createFontLoader(/\.ttf(\?.*)?$/, 'application/octet-stream'),
createFontLoader(/\.otf(\?.*)?$/, 'font/opentype'),
createFontLoader(/\.eot(\?.*)?$/),
createFontLoader(/\.svg(\?.*)?$/, 'image/svg+xml'),
{
test: /\.(png|jpg)$/,
include: paths.images,
use: [{
loader: 'url-loader',
options: {
name: 'images/[name].[hash].[ext]',
limit: 8192,
},
}],
}
)
// ------------------------------------
// Plugins
// ------------------------------------
const plugins = [
// Ensure any predefined globals are available throughout our project
new webpack.DefinePlugin(globals),
// Webpack 2 requires that our loaders be passed options, and this is how you do it
new webpack.LoaderOptionsPlugin({
sourceMap: config.sourceMap,
options: {
debug: config.debug,
context: resolvePath('/'),
minimize: config.optimize,
sassLoader: {
includePaths: [paths.styles, 'node_modules'],
sourceMap: config.sourceMap,
},
// postcss processing messes up "true" source maps, so we disable
// it when debug mode is passed in by returning an empty config array
postcss: config.debug ? [] : [cssnano({
autoprefixer: {
add: true,
remove: true,
browsers: ['last 2 versions'],
},
discardComments: {
removeAll: !config.sourceMap, // removing comments breaks the inline source maps
},
discardUnused: true,
mergeIdents: false,
reduceIdents: false,
safe: true,
sourcemap: config.sourceMap,
})],
},
}),
// Autogen an HTML file with proper links to the js/css files
new HtmlWebpackPlugin({
filename: 'index.html',
hash: false,
inject: 'body',
favicon: `${paths.src}/images/favicon.ico`,
template: `${paths.src}/index.template.html`,
minify: {
collapseWhitespace: config.optimize,
},
}),
]
if (config.analyze) {
plugins.push(new BundleAnalyzerPlugin())
}
if (__DEV__) {
plugins.push(
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
)
}
if (config.extractText) {
plugins.push(new ExtractTextPlugin({
filename: '[name]-[contenthash].css',
allChunks: true,
disable: false,
}))
}
if (config.optimize) {
plugins.push(
// Extract 3rd party modules into a 'vendor' chunk which can be cached by users
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: ({ resource }) => /node_modules/.test(resource),
}),
new webpack.IgnorePlugin(/^\.\/locale$/, [/moment$/]),
// Generate a 'manifest' chunk to be inlined in the HTML template
new webpack.optimize.CommonsChunkPlugin('manifest'),
// Need this plugin for deterministic hashing
// until this issue is resolved: https://github.com/webpack/webpack/issues/1315
// for more info: https://webpack.js.org/how-to/cache/
new WebpackMd5Hash(),
new webpack.optimize.UglifyJsPlugin({
compress: {
unused: true,
dead_code: true,
warnings: false,
},
sourceMap: config.sourceMap,
}),
// https://github.com/webpack/docs/wiki/list-of-plugins#aggressivemergingplugin
new webpack.optimize.AggressiveMergingPlugin(),
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 50000,
}),
new ScriptExtHtmlWebpackPlugin({
inline: ['manifest'],
defaultAttribute: 'defer',
}),
// TODO: Check if assets are served gzipped in prod: http://gzipwtf.com/
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.html$/,
threshold: 10240,
minRatio: 0.8,
})
)
}
if (process.env.deployLocation) {
const upperLocation = process.env.deployLocation.toUpperCase()
if (process.env[`AWS_REACT_${upperLocation}_ACCESS_KEY_ID`]) {
console.log('Deploy Initiated ---------')
console.log(`Deploying to ${process.env.deployLocation} under bucket: ${process.env.S3Bucket}`)
plugins.push(new S3Plugin({
directory: paths.build,
s3Options: {
accessKeyId: process.env[`AWS_REACT_${upperLocation}_ACCESS_KEY_ID`],
secretAccessKey: process.env[`AWS_REACT_${upperLocation}_SECRET_ACCESS_KEY`],
},
s3UploadOptions: {
Bucket: process.env.S3Bucket,
ContentEncoding(fileName) {
if (/\.gz/.test(fileName)) return 'gzip'
},
},
}))
} else {
console.warn('Required env variable missing: ', `AWS_REACT_${upperLocation}_ACCESS_KEY_ID`)
}
}
// ------------------------------------
// Final Config
// ------------------------------------
/*
Pass `--env.debug` if you have to do some serious debugging and need
the exact source to show up in your chrome debugger tool for stepping through code.
Just know, that Hot Module Reload will stop working with this mode, and you will have
full page reloads for every update instead.
You **may** also need to comment out the try/catch and move out `renderApp()`
within index.js if that is causing you to not see errors properly when using this mode
The other use case for this is if you want to see the exact location of styles coming from
large files for debugging some scoping issue
(this happens frequently with global styles)
By default, source maps are generated with `eval` which is EXTRMELY fast and will give the
correct file path in RedBox errors, at the cost of slightly incorrect line numbers.
Ideally we could use `cheap-module-eval-source-map` but there is an issue preventing that from
working as expected. Follow the discussion here: https://github.com/webpack/webpack/issues/2145
*/
const chooseDevtool = () => {
if (config.debug) return 'source-map'
if (config.sourceMap) return 'eval'
return false
}
return {
devtool: chooseDevtool(),
devServer: !__DEV__
? {}
: {
hot: true,
historyApiFallback: true,
contentBase: resolvePath('build'),
},
entry: !__DEV__
? [resolvePath('src/index.js')]
: ['react-hot-loader/patch', resolvePath('src/index.js')],
context: paths.src,
resolve: {
modules: [
paths.src,
'node_modules',
],
extensions: ['.js', '.jsx', '.json'],
},
output: {
publicPath: __DEV__ ? 'http://localhost:8080/' : '/',
path: paths.build,
filename: __DEV__ ? '[name].js' : '[name]-[chunkhash].js',
chunkFilename: __DEV__ ? '[id].js' : '[id]-[chunkhash].js',
},
module: { rules },
plugins,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment