🔥
This commit is contained in:
6
tools/.eslintrc
Normal file
6
tools/.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-console": 0,
|
||||
"global-require": 0
|
||||
}
|
||||
}
|
||||
48
tools/README.md
Normal file
48
tools/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
## Build Automation Tools
|
||||
|
||||
##### `yarn start` (`start.js`)
|
||||
|
||||
* Cleans up the output `/build` directory (`clean.js`)
|
||||
* Copies static files to the output folder (`copy.js`)
|
||||
* Launches [Webpack](https://webpack.github.io/) compiler in a watch mode (via [webpack-middleware](https://github.com/kriasoft/webpack-middleware))
|
||||
* Launches Node.js server from the compiled output folder (`runServer.js`)
|
||||
* Launches [Browsersync](https://browsersync.io/),
|
||||
[Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement), and
|
||||
[React Hot Loader](https://github.com/gaearon/react-hot-loader)
|
||||
|
||||
##### `yarn run build` (`build.js`)
|
||||
|
||||
* Cleans up the output `/build` folder (`clean.js`)
|
||||
* Copies static files to the output folder (`copy.js`)
|
||||
* Creates application bundles with Webpack (`bundle.js`, `webpack.config.js`)
|
||||
|
||||
##### `yarn run deploy` (`deploy.js`)
|
||||
|
||||
* Builds the project from source files (`build.js`)
|
||||
* Pushes the contents of the `/build` folder to a remote server with Git
|
||||
|
||||
##### Options
|
||||
|
||||
Flag | Description
|
||||
----------- | --------------------------------------------------
|
||||
`--release` | Minimizes and optimizes the compiled output
|
||||
`--verbose` | Prints detailed information to the console
|
||||
`--static` | Renders [specified routes](./render.js#L15) as static html files
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
$ yarn run build -- --release --verbose # Build the app in production mode
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```sh
|
||||
$ yarn start -- --release # Launch dev server in production mode
|
||||
```
|
||||
|
||||
#### Misc
|
||||
|
||||
* `webpack.config.js` - Webpack configuration for both client-side and server-side bundles
|
||||
* `run.js` - Helps to launch other scripts with `babel-node` (e.g. `babel-node tools/run build`)
|
||||
* `.eslintrc` - ESLint overrides for built automation scripts
|
||||
27
tools/build.js
Normal file
27
tools/build.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import cp from 'child_process';
|
||||
import run from './run';
|
||||
import clean from './clean';
|
||||
import copy from './copy';
|
||||
import bundle from './bundle';
|
||||
import render from './render';
|
||||
import pkg from '../package.json';
|
||||
|
||||
/**
|
||||
* Compiles the project from source files into a distributable
|
||||
* format and copies it to the output (build) folder.
|
||||
*/
|
||||
async function build() {
|
||||
await run(clean);
|
||||
await run(copy);
|
||||
await run(bundle);
|
||||
|
||||
if (process.argv.includes('--static')) {
|
||||
await run(render);
|
||||
}
|
||||
|
||||
if (process.argv.includes('--docker')) {
|
||||
cp.spawnSync('docker', ['build', '-t', pkg.name, '.'], { stdio: 'inherit' });
|
||||
}
|
||||
}
|
||||
|
||||
export default build;
|
||||
20
tools/bundle.js
Normal file
20
tools/bundle.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import webpack from 'webpack';
|
||||
import webpackConfig from './webpack.config';
|
||||
|
||||
/**
|
||||
* Creates application bundles from the source files.
|
||||
*/
|
||||
function bundle() {
|
||||
return new Promise((resolve, reject) => {
|
||||
webpack(webpackConfig).run((err, stats) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
console.log(stats.toString(webpackConfig[0].stats));
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default bundle;
|
||||
22
tools/clean.js
Normal file
22
tools/clean.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { cleanDir } from './lib/fs';
|
||||
|
||||
/**
|
||||
* Cleans up the output (build) directory.
|
||||
*/
|
||||
function clean() {
|
||||
return Promise.all([
|
||||
cleanDir('build/*', {
|
||||
nosort: true,
|
||||
dot: true,
|
||||
ignore: ['build/.git', 'build/public'],
|
||||
}),
|
||||
|
||||
cleanDir('build/public/*', {
|
||||
nosort: true,
|
||||
dot: true,
|
||||
ignore: ['build/public/.git'],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export default clean;
|
||||
57
tools/copy.js
Normal file
57
tools/copy.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import path from 'path';
|
||||
import chokidar from 'chokidar';
|
||||
import { writeFile, copyFile, makeDir, copyDir, cleanDir } from './lib/fs';
|
||||
import pkg from '../package.json';
|
||||
import { format } from './run';
|
||||
|
||||
/**
|
||||
* Copies static files such as robots.txt, favicon.ico to the
|
||||
* output (build) folder.
|
||||
*/
|
||||
async function copy() {
|
||||
await makeDir('build');
|
||||
await Promise.all([
|
||||
writeFile('build/package.json', JSON.stringify({
|
||||
private: true,
|
||||
engines: pkg.engines,
|
||||
dependencies: pkg.dependencies,
|
||||
scripts: {
|
||||
start: 'node server.js',
|
||||
},
|
||||
}, null, 2)),
|
||||
copyFile('LICENSE.txt', 'build/LICENSE.txt'),
|
||||
copyDir('src/content', 'build/content'),
|
||||
copyDir('public', 'build/public'),
|
||||
]);
|
||||
|
||||
if (process.argv.includes('--watch')) {
|
||||
const watcher = chokidar.watch([
|
||||
'src/content/**/*',
|
||||
'public/**/*',
|
||||
], { ignoreInitial: true });
|
||||
|
||||
watcher.on('all', async (event, filePath) => {
|
||||
const start = new Date();
|
||||
const src = path.relative('./', filePath);
|
||||
const dist = path.join('build/', src.startsWith('src') ? path.relative('src', src) : src);
|
||||
switch (event) {
|
||||
case 'add':
|
||||
case 'change':
|
||||
await makeDir(path.dirname(dist));
|
||||
await copyFile(filePath, dist);
|
||||
break;
|
||||
case 'unlink':
|
||||
case 'unlinkDir':
|
||||
cleanDir(dist, { nosort: true, dot: true });
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const end = new Date();
|
||||
const time = end.getTime() - start.getTime();
|
||||
console.log(`[${format(end)}] ${event} '${dist}' after ${time} ms`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default copy;
|
||||
51
tools/deployToAzureWebApps.js
Normal file
51
tools/deployToAzureWebApps.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import GitRepo from 'git-repository';
|
||||
import fetch from 'node-fetch';
|
||||
import run from './run';
|
||||
|
||||
// For more information visit http://gitolite.com/deploy.html
|
||||
function getRemote(slot) {
|
||||
return {
|
||||
name: slot || 'production',
|
||||
url: `https://example${slot ? `-${slot}` : ''}.scm.azurewebsites.net:443/example.git`,
|
||||
branch: 'master',
|
||||
website: `http://example${slot ? `-${slot}` : ''}.azurewebsites.net`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy the contents of the `/build` folder to a remote
|
||||
* server via Git. Example: `yarn run deploy -- --production`
|
||||
*/
|
||||
async function deployToAzureWebApps() {
|
||||
// By default deploy to the staging deployment slot
|
||||
const remote = getRemote(process.argv.includes('--production') ? null : 'staging');
|
||||
|
||||
// Initialize a new Git repository inside the `/build` folder
|
||||
// if it doesn't exist yet
|
||||
const repo = await GitRepo.open('build', { init: true });
|
||||
await repo.setRemote(remote.name, remote.url);
|
||||
|
||||
// Fetch the remote repository if it exists
|
||||
const isRefExists = await repo.hasRef(remote.url, remote.branch);
|
||||
if (isRefExists) {
|
||||
await repo.fetch(remote.name);
|
||||
await repo.reset(`${remote.name}/${remote.branch}`, { hard: true });
|
||||
await repo.clean({ force: true });
|
||||
}
|
||||
|
||||
// Build the project in RELEASE mode which
|
||||
// generates optimized and minimized bundles
|
||||
process.argv.push('--release');
|
||||
await run(require('./build'));
|
||||
|
||||
// Push the contents of the build folder to the remote server via Git
|
||||
await repo.add('--all .');
|
||||
await repo.commit(`Update ${new Date().toISOString()}`);
|
||||
await repo.push(remote.name, `master:${remote.branch}`);
|
||||
|
||||
// Check if the site was successfully deployed
|
||||
const response = await fetch(remote.website);
|
||||
console.log(`${remote.website} -> ${response.status}`);
|
||||
}
|
||||
|
||||
export default deployToAzureWebApps;
|
||||
37
tools/deployToGitHubPages.js
Normal file
37
tools/deployToGitHubPages.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import GitRepo from 'git-repository';
|
||||
import run from './run';
|
||||
import build from './build';
|
||||
|
||||
const remote = {
|
||||
name: 'github',
|
||||
url: 'https://github.com/{user}/{repo}.git',
|
||||
branch: 'gh-pages',
|
||||
};
|
||||
|
||||
/**
|
||||
* Deploy the contents of the `/build/public` folder to GitHub Pages.
|
||||
*/
|
||||
async function deployToGitHubPages() {
|
||||
// Initialize a new Git repository inside the `/build` folder
|
||||
// if it doesn't exist yet
|
||||
const repo = await GitRepo.open('build/public', { init: true });
|
||||
await repo.setRemote(remote.name, remote.url);
|
||||
const isRefExists = await repo.hasRef(remote.url, remote.branch);
|
||||
if (isRefExists) {
|
||||
await repo.fetch(remote.name);
|
||||
await repo.reset(`${remote.name}/${remote.branch}`, { hard: true });
|
||||
await repo.clean({ force: true });
|
||||
}
|
||||
|
||||
// Build the project in RELEASE mode which
|
||||
// generates optimized and minimized bundles
|
||||
process.argv.push('--static', '--release');
|
||||
await run(build);
|
||||
|
||||
// Push the contents of the build folder to the remote server via Git
|
||||
await repo.add('--all .');
|
||||
await repo.commit(`Update ${new Date().toISOString()}`);
|
||||
await repo.push(remote.name, `master:${remote.branch}`);
|
||||
}
|
||||
|
||||
export default deployToGitHubPages;
|
||||
69
tools/lib/fs.js
Normal file
69
tools/lib/fs.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* React Starter Kit (https://www.reactstarterkit.com/)
|
||||
*
|
||||
* Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.txt file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'glob';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
|
||||
export const readFile = (file) => new Promise((resolve, reject) => {
|
||||
fs.readFile(file, 'utf8', (err, data) => (err ? reject(err) : resolve(data)));
|
||||
});
|
||||
|
||||
export const writeFile = (file, contents) => new Promise((resolve, reject) => {
|
||||
fs.writeFile(file, contents, 'utf8', err => (err ? reject(err) : resolve()));
|
||||
});
|
||||
|
||||
export const copyFile = (source, target) => new Promise((resolve, reject) => {
|
||||
let cbCalled = false;
|
||||
function done(err) {
|
||||
if (!cbCalled) {
|
||||
cbCalled = true;
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rd = fs.createReadStream(source);
|
||||
rd.on('error', err => done(err));
|
||||
const wr = fs.createWriteStream(target);
|
||||
wr.on('error', err => done(err));
|
||||
wr.on('close', err => done(err));
|
||||
rd.pipe(wr);
|
||||
});
|
||||
|
||||
export const readDir = (pattern, options) => new Promise((resolve, reject) =>
|
||||
glob(pattern, options, (err, result) => (err ? reject(err) : resolve(result))),
|
||||
);
|
||||
|
||||
export const makeDir = (name) => new Promise((resolve, reject) => {
|
||||
mkdirp(name, err => (err ? reject(err) : resolve()));
|
||||
});
|
||||
|
||||
export const copyDir = async (source, target) => {
|
||||
const dirs = await readDir('**/*.*', {
|
||||
cwd: source,
|
||||
nosort: true,
|
||||
dot: true,
|
||||
});
|
||||
await Promise.all(dirs.map(async dir => {
|
||||
const from = path.resolve(source, dir);
|
||||
const to = path.resolve(target, dir);
|
||||
await makeDir(path.dirname(to));
|
||||
await copyFile(from, to);
|
||||
}));
|
||||
};
|
||||
|
||||
export const cleanDir = (pattern, options) => new Promise((resolve, reject) =>
|
||||
rimraf(pattern, { glob: options }, (err, result) => (err ? reject(err) : resolve(result))),
|
||||
);
|
||||
25
tools/lib/markdown-loader.js
Normal file
25
tools/lib/markdown-loader.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* React Starter Kit (https://www.reactstarterkit.com/)
|
||||
*
|
||||
* Copyright © 2014-2016 Kriasoft, LLC. All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.txt file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
const MarkdownIt = require('markdown-it');
|
||||
const fm = require('front-matter');
|
||||
|
||||
module.exports = function markdownLoader(source) {
|
||||
this.cacheable();
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
const frontmatter = fm(source);
|
||||
frontmatter.attributes.html = md.render(frontmatter.body);
|
||||
|
||||
return `module.exports = ${JSON.stringify(frontmatter.attributes)};`;
|
||||
};
|
||||
53
tools/render.js
Normal file
53
tools/render.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { writeFile, makeDir } from './lib/fs';
|
||||
import runServer from './runServer';
|
||||
|
||||
// Enter your paths here which you want to render as static
|
||||
// Example:
|
||||
// const routes = [
|
||||
// '/', // => build/public/index.html
|
||||
// '/page', // => build/public/page.html
|
||||
// '/page/', // => build/public/page/index.html
|
||||
// '/page/name', // => build/public/page/name.html
|
||||
// '/page/name/', // => build/public/page/name/index.html
|
||||
// ];
|
||||
const routes = [
|
||||
'/',
|
||||
'/contact',
|
||||
'/login',
|
||||
'/register',
|
||||
'/about',
|
||||
'/privacy',
|
||||
'/404', // https://help.github.com/articles/creating-a-custom-404-page-for-your-github-pages-site/
|
||||
];
|
||||
|
||||
async function render() {
|
||||
const server = await runServer();
|
||||
|
||||
// add dynamic routes
|
||||
// const products = await fetch(`http://${server.host}/api/products`).then(res => res.json());
|
||||
// products.forEach(product => routes.push(
|
||||
// `/product/${product.uri}`,
|
||||
// `/product/${product.uri}/specs`
|
||||
// ));
|
||||
|
||||
await Promise.all(routes.map(async (route, index) => {
|
||||
const url = `http://${server.host}${route}`;
|
||||
const fileName = route.endsWith('/') ? 'index.html' : `${path.basename(route, '.html')}.html`;
|
||||
const dirName = path.join('build/public', route.endsWith('/') ? route : path.dirname(route));
|
||||
const dist = path.join(dirName, fileName);
|
||||
const timeStart = new Date();
|
||||
const response = await fetch(url);
|
||||
const timeEnd = new Date();
|
||||
const text = await response.text();
|
||||
await makeDir(dirName);
|
||||
await writeFile(dist, text);
|
||||
const time = timeEnd.getTime() - timeStart.getTime();
|
||||
console.log(`#${index + 1} ${dist} => ${response.status} ${response.statusText} (${time} ms)`);
|
||||
}));
|
||||
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
|
||||
export default render;
|
||||
27
tools/run.js
Normal file
27
tools/run.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export function format(time) {
|
||||
return time.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
|
||||
}
|
||||
|
||||
function run(fn, options) {
|
||||
const task = typeof fn.default === 'undefined' ? fn : fn.default;
|
||||
const start = new Date();
|
||||
console.log(
|
||||
`[${format(start)}] Starting '${task.name}${options ? ` (${options})` : ''}'...`,
|
||||
);
|
||||
return task(options).then(resolution => {
|
||||
const end = new Date();
|
||||
const time = end.getTime() - start.getTime();
|
||||
console.log(
|
||||
`[${format(end)}] Finished '${task.name}${options ? ` (${options})` : ''}' after ${time} ms`,
|
||||
);
|
||||
return resolution;
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module && process.argv.length > 2) {
|
||||
delete require.cache[__filename]; // eslint-disable-line no-underscore-dangle
|
||||
const module = require(`./${process.argv[2]}.js`).default; // eslint-disable-line import/no-dynamic-require
|
||||
run(module).catch(err => { console.error(err.stack); process.exit(1); });
|
||||
}
|
||||
|
||||
export default run;
|
||||
63
tools/runServer.js
Normal file
63
tools/runServer.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import path from 'path';
|
||||
import cp from 'child_process';
|
||||
import webpackConfig from './webpack.config';
|
||||
|
||||
// Should match the text string used in `src/server.js/server.listen(...)`
|
||||
const RUNNING_REGEXP = /The server is running at http:\/\/(.*?)\//;
|
||||
|
||||
let server;
|
||||
const { output } = webpackConfig.find(x => x.target === 'node');
|
||||
const serverPath = path.join(output.path, output.filename);
|
||||
|
||||
// Launch or restart the Node.js server
|
||||
function runServer() {
|
||||
return new Promise(resolve => {
|
||||
let pending = true;
|
||||
|
||||
function onStdOut(data) {
|
||||
const time = new Date().toTimeString();
|
||||
const match = data.toString('utf8').match(RUNNING_REGEXP);
|
||||
|
||||
process.stdout.write(time.replace(/.*(\d{2}:\d{2}:\d{2}).*/, '[$1] '));
|
||||
process.stdout.write(data);
|
||||
|
||||
if (match) {
|
||||
server.host = match[1];
|
||||
server.stdout.removeListener('data', onStdOut);
|
||||
server.stdout.on('data', x => process.stdout.write(x));
|
||||
pending = false;
|
||||
resolve(server);
|
||||
}
|
||||
}
|
||||
|
||||
if (server) {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
|
||||
server = cp.spawn('node', [serverPath], {
|
||||
env: Object.assign({ NODE_ENV: 'development' }, process.env),
|
||||
silent: false,
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
server.once('exit', (code, signal) => {
|
||||
if (pending) {
|
||||
throw new Error(`Server terminated unexpectedly with code: ${code} signal: ${signal}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
server.stdout.on('data', onStdOut);
|
||||
server.stderr.on('data', x => process.stderr.write(x));
|
||||
|
||||
return server;
|
||||
});
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
if (server) {
|
||||
server.kill('SIGTERM');
|
||||
}
|
||||
});
|
||||
|
||||
export default runServer;
|
||||
71
tools/start.js
Normal file
71
tools/start.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import browserSync from 'browser-sync';
|
||||
import webpack from 'webpack';
|
||||
import webpackMiddleware from 'webpack-middleware';
|
||||
import webpackHotMiddleware from 'webpack-hot-middleware';
|
||||
import run from './run';
|
||||
import runServer from './runServer';
|
||||
import webpackConfig from './webpack.config';
|
||||
import clean from './clean';
|
||||
import copy from './copy';
|
||||
|
||||
process.argv.push('--watch');
|
||||
const [config] = webpackConfig;
|
||||
|
||||
/**
|
||||
* Launches a development web server with "live reload" functionality -
|
||||
* synchronizing URLs, interactions and code changes across multiple devices.
|
||||
*/
|
||||
async function start() {
|
||||
await run(clean);
|
||||
await run(copy);
|
||||
await new Promise(resolve => {
|
||||
// Hot Module Replacement (HMR) + React Hot Reload
|
||||
if (config.debug) {
|
||||
config.entry.client = ['react-hot-loader/patch', 'webpack-hot-middleware/client']
|
||||
.concat(config.entry.client);
|
||||
config.output.filename = config.output.filename.replace('[chunkhash', '[hash');
|
||||
config.output.chunkFilename = config.output.chunkFilename.replace('[chunkhash', '[hash');
|
||||
config.module.loaders.find(x => x.loader === 'babel-loader')
|
||||
.query.plugins.unshift('react-hot-loader/babel');
|
||||
config.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
config.plugins.push(new webpack.NoErrorsPlugin());
|
||||
}
|
||||
|
||||
const bundler = webpack(webpackConfig);
|
||||
const wpMiddleware = webpackMiddleware(bundler, {
|
||||
// IMPORTANT: webpack middleware can't access config,
|
||||
// so we should provide publicPath by ourselves
|
||||
publicPath: config.output.publicPath,
|
||||
|
||||
// Pretty colored output
|
||||
stats: config.stats,
|
||||
|
||||
// For other settings see
|
||||
// https://webpack.github.io/docs/webpack-dev-middleware
|
||||
});
|
||||
const hotMiddleware = webpackHotMiddleware(bundler.compilers[0]);
|
||||
|
||||
let handleBundleComplete = async () => {
|
||||
handleBundleComplete = stats => !stats.stats[1].compilation.errors.length && runServer();
|
||||
|
||||
const server = await runServer();
|
||||
const bs = browserSync.create();
|
||||
|
||||
bs.init({
|
||||
...(config.debug ? {} : { notify: false, ui: false }),
|
||||
|
||||
proxy: {
|
||||
target: server.host,
|
||||
middleware: [wpMiddleware, hotMiddleware],
|
||||
proxyOptions: {
|
||||
xfwd: true,
|
||||
},
|
||||
},
|
||||
}, resolve);
|
||||
};
|
||||
|
||||
bundler.plugin('done', stats => handleBundleComplete(stats));
|
||||
});
|
||||
}
|
||||
|
||||
export default start;
|
||||
358
tools/webpack.config.js
Normal file
358
tools/webpack.config.js
Normal file
@@ -0,0 +1,358 @@
|
||||
import path from 'path';
|
||||
import webpack from 'webpack';
|
||||
import extend from 'extend';
|
||||
import AssetsPlugin from 'assets-webpack-plugin';
|
||||
|
||||
const isDebug = !process.argv.includes('--release');
|
||||
const isVerbose = process.argv.includes('--verbose');
|
||||
|
||||
//
|
||||
// Common configuration chunk to be used for both
|
||||
// client-side (client.js) and server-side (server.js) bundles
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const config = {
|
||||
context: path.resolve(__dirname, '../src'),
|
||||
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../build/public/assets'),
|
||||
publicPath: '/assets/',
|
||||
sourcePrefix: ' ',
|
||||
pathinfo: isVerbose,
|
||||
},
|
||||
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
loader: 'babel-loader',
|
||||
include: [
|
||||
path.resolve(__dirname, '../src'),
|
||||
],
|
||||
query: {
|
||||
// https://github.com/babel/babel-loader#options
|
||||
cacheDirectory: isDebug,
|
||||
|
||||
// https://babeljs.io/docs/usage/options/
|
||||
babelrc: false,
|
||||
presets: [
|
||||
// Latest stable ECMAScript features
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-preset-latest
|
||||
'latest',
|
||||
// Experimental ECMAScript proposals
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-preset-stage-0
|
||||
'stage-0',
|
||||
// JSX, Flow
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-preset-react
|
||||
'react',
|
||||
...isDebug ? [] : [
|
||||
// Optimize React code for the production build
|
||||
// https://github.com/thejameskyle/babel-react-optimize
|
||||
'react-optimize',
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
// Externalise references to helpers and builtins,
|
||||
// automatically polyfilling your code without polluting globals.
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-runtime
|
||||
'transform-runtime',
|
||||
...!isDebug ? [] : [
|
||||
// Adds component stack to warning messages
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
|
||||
'transform-react-jsx-source',
|
||||
// Adds __self attribute to JSX which React will use for some warnings
|
||||
// https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
|
||||
'transform-react-jsx-self',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel',
|
||||
include: path.resolve(__dirname, './node_modules/react-icons/md'),
|
||||
query: {
|
||||
presets: ['latest', 'react'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css/,
|
||||
loaders: [
|
||||
'isomorphic-style-loader',
|
||||
`css-loader?${JSON.stringify({
|
||||
// CSS Loader https://github.com/webpack/css-loader
|
||||
importLoaders: 1,
|
||||
sourceMap: isDebug,
|
||||
// CSS Modules https://github.com/css-modules/css-modules
|
||||
modules: true,
|
||||
localIdentName: isDebug ? '[name]-[local]-[hash:base64:5]' : '[hash:base64:5]',
|
||||
// CSS Nano http://cssnano.co/options/
|
||||
minimize: !isDebug,
|
||||
discardComments: { removeAll: true },
|
||||
})}`,
|
||||
'postcss-loader?pack=default',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.md$/,
|
||||
loader: path.resolve(__dirname, './lib/markdown-loader.js'),
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
},
|
||||
{
|
||||
test: /\.txt$/,
|
||||
loader: 'raw-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
query: {
|
||||
name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
name: isDebug ? '[path][name].[ext]?[hash:8]' : '[hash:8].[ext]',
|
||||
limit: 10000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
resolve: {
|
||||
root: path.resolve(__dirname, '../src'),
|
||||
modulesDirectories: ['node_modules'],
|
||||
extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx', '.json'],
|
||||
},
|
||||
|
||||
// Don't attempt to continue if there are any errors.
|
||||
bail: !isDebug,
|
||||
|
||||
cache: isDebug,
|
||||
debug: isDebug,
|
||||
|
||||
stats: {
|
||||
colors: true,
|
||||
reasons: isDebug,
|
||||
hash: isVerbose,
|
||||
version: isVerbose,
|
||||
timings: true,
|
||||
chunks: isVerbose,
|
||||
chunkModules: isVerbose,
|
||||
cached: isVerbose,
|
||||
cachedAssets: isVerbose,
|
||||
},
|
||||
|
||||
// The list of plugins for PostCSS
|
||||
// https://github.com/postcss/postcss
|
||||
postcss(bundler) {
|
||||
return {
|
||||
default: [
|
||||
// Transfer @import rule by inlining content, e.g. @import 'normalize.css'
|
||||
// https://github.com/jonathantneal/postcss-partial-import
|
||||
require('postcss-partial-import')({ addDependencyTo: bundler }),
|
||||
// Allow you to fix url() according to postcss to and/or from options
|
||||
// https://github.com/postcss/postcss-url
|
||||
require('postcss-url')(),
|
||||
// W3C variables, e.g. :root { --color: red; } div { background: var(--color); }
|
||||
// https://github.com/postcss/postcss-custom-properties
|
||||
require('postcss-custom-properties')(),
|
||||
// W3C CSS Custom Media Queries, e.g. @custom-media --small-viewport (max-width: 30em);
|
||||
// https://github.com/postcss/postcss-custom-media
|
||||
require('postcss-custom-media')(),
|
||||
// CSS4 Media Queries, e.g. @media screen and (width >= 500px) and (width <= 1200px) { }
|
||||
// https://github.com/postcss/postcss-media-minmax
|
||||
require('postcss-media-minmax')(),
|
||||
// W3C CSS Custom Selectors, e.g. @custom-selector :--heading h1, h2, h3, h4, h5, h6;
|
||||
// https://github.com/postcss/postcss-custom-selectors
|
||||
require('postcss-custom-selectors')(),
|
||||
// W3C calc() function, e.g. div { height: calc(100px - 2em); }
|
||||
// https://github.com/postcss/postcss-calc
|
||||
require('postcss-calc')(),
|
||||
// Allows you to nest one style rule inside another
|
||||
// https://github.com/jonathantneal/postcss-nesting
|
||||
require('postcss-nesting')(),
|
||||
// Unwraps nested rules like how Sass does it
|
||||
// https://github.com/postcss/postcss-nested
|
||||
require('postcss-nested')(),
|
||||
// W3C color() function, e.g. div { background: color(red alpha(90%)); }
|
||||
// https://github.com/postcss/postcss-color-function
|
||||
require('postcss-color-function')(),
|
||||
// Convert CSS shorthand filters to SVG equivalent, e.g. .blur { filter: blur(4px); }
|
||||
// https://github.com/iamvdo/pleeease-filters
|
||||
require('pleeease-filters')(),
|
||||
// Generate pixel fallback for "rem" units, e.g. div { margin: 2.5rem 2px 3em 100%; }
|
||||
// https://github.com/robwierzbowski/node-pixrem
|
||||
require('pixrem')(),
|
||||
// W3C CSS Level4 :matches() pseudo class, e.g. p:matches(:first-child, .special) { }
|
||||
// https://github.com/postcss/postcss-selector-matches
|
||||
require('postcss-selector-matches')(),
|
||||
// Transforms :not() W3C CSS Level 4 pseudo class to :not() CSS Level 3 selectors
|
||||
// https://github.com/postcss/postcss-selector-not
|
||||
require('postcss-selector-not')(),
|
||||
// Postcss flexbox bug fixer
|
||||
// https://github.com/luisrudge/postcss-flexbugs-fixes
|
||||
require('postcss-flexbugs-fixes')(),
|
||||
// Add vendor prefixes to CSS rules using values from caniuse.com
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require('autoprefixer')({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9', // React doesn't support IE8 anyway
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
//
|
||||
// Configuration for the client-side bundle (client.js)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const clientConfig = extend(true, {}, config, {
|
||||
entry: {
|
||||
client: './client.js',
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: isDebug ? '[name].js' : '[name].[chunkhash:8].js',
|
||||
chunkFilename: isDebug ? '[name].chunk.js' : '[name].[chunkhash:8].chunk.js',
|
||||
},
|
||||
|
||||
target: 'web',
|
||||
|
||||
plugins: [
|
||||
|
||||
// Define free variables
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
|
||||
'process.env.BROWSER': true,
|
||||
__DEV__: isDebug,
|
||||
}),
|
||||
|
||||
// Emit a file with assets paths
|
||||
// https://github.com/sporto/assets-webpack-plugin#options
|
||||
new AssetsPlugin({
|
||||
path: path.resolve(__dirname, '../build'),
|
||||
filename: 'assets.js',
|
||||
processOutput: x => `module.exports = ${JSON.stringify(x, null, 2)};`,
|
||||
}),
|
||||
|
||||
// Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
|
||||
// http://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks: module => /node_modules/.test(module.resource),
|
||||
}),
|
||||
|
||||
...isDebug ? [] : [
|
||||
// Assign the module and chunk ids by occurrence count
|
||||
// Consistent ordering of modules required if using any hashing ([hash] or [chunkhash])
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#occurrenceorderplugin
|
||||
new webpack.optimize.OccurrenceOrderPlugin(true),
|
||||
|
||||
// Search for equal or similar files and deduplicate them in the output
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#dedupeplugin
|
||||
new webpack.optimize.DedupePlugin(),
|
||||
|
||||
// Minimize all JavaScript output of chunks
|
||||
// https://github.com/mishoo/UglifyJS2#compressor-options
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
screw_ie8: true, // React doesn't support IE8
|
||||
warnings: isVerbose,
|
||||
unused: true,
|
||||
dead_code: true,
|
||||
},
|
||||
mangle: {
|
||||
screw_ie8: true,
|
||||
},
|
||||
output: {
|
||||
comments: false,
|
||||
screw_ie8: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
|
||||
// Choose a developer tool to enhance debugging
|
||||
// http://webpack.github.io/docs/configuration.html#devtool
|
||||
devtool: isDebug ? 'cheap-module-source-map' : false,
|
||||
|
||||
// Some libraries import Node modules but don't use them in the browser.
|
||||
// Tell Webpack to provide empty mocks for them so importing them works.
|
||||
// https://webpack.github.io/docs/configuration.html#node
|
||||
// https://github.com/webpack/node-libs-browser/tree/master/mock
|
||||
node: {
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
},
|
||||
});
|
||||
|
||||
//
|
||||
// Configuration for the server-side bundle (server.js)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const serverConfig = extend(true, {}, config, {
|
||||
entry: {
|
||||
server: './server.js',
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '../../server.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
},
|
||||
|
||||
target: 'node',
|
||||
|
||||
externals: [
|
||||
/^\.\/assets$/,
|
||||
(context, request, callback) => {
|
||||
const isExternal =
|
||||
request.match(/^[@a-z][a-z/.\-0-9]*$/i) &&
|
||||
!request.match(/\.(css|less|scss|sss)$/i);
|
||||
callback(null, Boolean(isExternal));
|
||||
},
|
||||
],
|
||||
|
||||
plugins: [
|
||||
// Define free variables
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#defineplugin
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': isDebug ? '"development"' : '"production"',
|
||||
'process.env.BROWSER': false,
|
||||
__DEV__: isDebug,
|
||||
}),
|
||||
|
||||
// Do not create separate chunks of the server bundle
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#limitchunkcountplugin
|
||||
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
||||
|
||||
// Adds a banner to the top of each generated chunk
|
||||
// https://webpack.github.io/docs/list-of-plugins.html#bannerplugin
|
||||
new webpack.BannerPlugin('require("source-map-support").install();',
|
||||
{ raw: true, entryOnly: false }),
|
||||
],
|
||||
|
||||
node: {
|
||||
console: false,
|
||||
global: false,
|
||||
process: false,
|
||||
Buffer: false,
|
||||
__filename: false,
|
||||
__dirname: false,
|
||||
},
|
||||
|
||||
devtool: isDebug ? 'cheap-module-source-map' : 'source-map',
|
||||
});
|
||||
|
||||
export default [clientConfig, serverConfig];
|
||||
Reference in New Issue
Block a user