diff --git a/package.json b/package.json index c960cc3a41..fb3bcd66ab 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "private": true, "engines": { - "node": ">=5.0 <7", - "npm": ">=3.3 <4" + "node": ">=6 <7", + "npm": ">=3.8 <4" }, "dependencies": { "babel-polyfill": "6.9.1", @@ -46,14 +46,15 @@ "babel-core": "^6.9.0", "babel-eslint": "^6.0.4", "babel-loader": "^6.2.4", + "babel-plugin-external-helpers": "^6.8.0", "babel-plugin-react-transform": "^2.0.2", "babel-plugin-rewire": "^1.0.0-rc-3", + "babel-plugin-transform-es2015-modules-commonjs": "^6.8.0", "babel-plugin-transform-react-constant-elements": "^6.8.0", "babel-plugin-transform-react-inline-elements": "^6.8.0", "babel-plugin-transform-react-remove-prop-types": "^0.2.7", "babel-plugin-transform-runtime": "^6.9.0", "babel-preset-es2015": "^6.9.0", - "babel-preset-node5": "^11.1.0", "babel-preset-react": "^6.5.0", "babel-preset-stage-0": "^6.5.0", "babel-register": "^6.9.0", @@ -62,7 +63,6 @@ "browser-sync": "^2.12.8", "chai": "^3.5.0", "css-loader": "^0.23.1", - "del": "^2.2.0", "enzyme": "^2.3.0", "eslint": "^2.10.2", "eslint-config-airbnb": "^9.0.1", @@ -79,7 +79,6 @@ "json-loader": "^0.5.4", "mkdirp": "^0.5.1", "mocha": "^2.5.3", - "ncp": "^2.0.0", "pixrem": "^3.0.1", "pleeease-filters": "^3.0.0", "postcss": "^5.0.21", @@ -97,23 +96,42 @@ "postcss-selector-not": "^2.0.0", "raw-loader": "^0.5.1", "react-addons-test-utils": "^15.1.0", + "react-hot-loader": "^3.0.0-beta.2", "react-transform-catch-errors": "^1.0.2", "react-transform-hmr": "^1.0.4", "redbox-react": "^1.2.6", + "rimraf": "^2.5.2", "sinon": "^2.0.0-pre", "stylelint": "^6.5.1", "stylelint-config-standard": "^8.0.0", "url-loader": "^0.5.7", "webpack": "^1.13.1", - "webpack-hot-middleware": "^2.10.0", - "webpack-middleware": "^1.5.1" + "webpack-dev-middleware": "^1.6.1", + "webpack-hot-middleware": "^2.10.0" }, "babel": { "presets": [ "react", - "node5", "stage-0" ], + "plugins": [ + [ + "transform-es2015-modules-commonjs", + { + "loose": true + } + ], + [ + "transform-es2015-destructuring", + { + "loose": true + } + ], + "transform-es2015-function-name", + "transform-es2015-parameters", + "external-helpers", + "transform-runtime" + ], "env": { "test": { "plugins": [ @@ -163,10 +181,11 @@ "test:watch": "npm run test -- --reporter min --watch", "clean": "babel-node tools/run clean", "copy": "babel-node tools/run copy", - "bundle": "babel-node tools/run bundle", - "build": "babel-node tools/run build", - "deploy": "babel-node tools/run deploy", + "compile": "babel-node tools/run compile", + "build": "babel-node tools/run build --release", + "build:debug": "babel-node tools/run build", "render": "babel-node tools/run render", - "start": "babel-node tools/run start" + "start": "babel-node tools/run start", + "deploy": "babel-node tools/run deploy" } } diff --git a/src/server.js b/src/server.js index 976518c787..4fb08ab09c 100644 --- a/src/server.js +++ b/src/server.js @@ -7,7 +7,7 @@ * LICENSE.txt file in the root directory of this source tree. */ -import 'babel-polyfill'; +import 'source-map-support/register'; import path from 'path'; import express from 'express'; import cookieParser from 'cookie-parser'; diff --git a/tools/README.md b/tools/README.md index 6b4fefa42e..73523afec9 100644 --- a/tools/README.md +++ b/tools/README.md @@ -4,17 +4,17 @@ * 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 [Webpack](https://webpack.github.io/) compiler in a watch mode (via [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware)) * Launches Node.js server from the compiled output folder (`runServer.js`) * Launches [Browsersync](https://browsersync.io/), - [HMR](https://webpack.github.io/docs/hot-module-replacement), and - [React Transform](https://github.com/gaearon/babel-plugin-react-transform) + [Hot Module Replacement](https://webpack.github.io/docs/hot-module-replacement), and + [React Hot Loader](https://github.com/gaearon/react-hot-loader) ##### `npm 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`) +* Compiles application (`compile.js`, `compileClient.js`, `compileServer.js`, `webpack.config.js`) ##### `npm run deploy` (`deploy.js`) @@ -32,7 +32,7 @@ Flag | Description For example: ```sh -$ npm run build -- --release --verbose # Build the app in production mode +$ npm run build -- --static --verbose # Build the app with static html files ``` or @@ -43,6 +43,6 @@ $ npm start -- --release # Launch dev server in production mode #### Misc -* `webpack.config.js` - Webpack configuration for both client-side and server-side bundles +* `webpack.config.js` - Webpack configuration for client-side bundle * `run.js` - Helps to launch other scripts with `babel-node` (e.g. `babel-node tools/run build`) * `.eslintrc` - ESLint overrides for built automation scripts diff --git a/tools/build.js b/tools/build.js index 9ce2632a68..a385426e71 100644 --- a/tools/build.js +++ b/tools/build.js @@ -1,7 +1,7 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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. @@ -10,7 +10,7 @@ import run from './run'; import clean from './clean'; import copy from './copy'; -import bundle from './bundle'; +import compile from './compile'; import render from './render'; /** @@ -19,8 +19,10 @@ import render from './render'; */ async function build() { await run(clean); - await run(copy); - await run(bundle); + await Promise.all([ + run(copy), + run(compile), + ]); if (process.argv.includes('--static')) { await run(render); diff --git a/tools/clean.js b/tools/clean.js index dd97506ff2..f2c60554b7 100644 --- a/tools/clean.js +++ b/tools/clean.js @@ -1,21 +1,31 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 del from 'del'; -import fs from './lib/fs'; +import { cleanDir } from './lib/fs'; /** * Cleans up the output (build) directory. */ -async function clean() { - await del(['.tmp', 'build/*', '!build/.git'], { dot: true }); - await fs.makeDir('build/public'); +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; diff --git a/tools/compile.js b/tools/compile.js new file mode 100644 index 0000000000..cdedac0233 --- /dev/null +++ b/tools/compile.js @@ -0,0 +1,21 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present 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 run from './run'; +import compileClient from './compileClient'; +import compileServer from './compileServer'; + +function compile() { + return Promise.all([ + run(compileClient), + run(compileServer), + ]); +} + +export default compile; diff --git a/tools/bundle.js b/tools/compileClient.js similarity index 66% rename from tools/bundle.js rename to tools/compileClient.js index 29772c4907..91b8ace1c7 100644 --- a/tools/bundle.js +++ b/tools/compileClient.js @@ -1,7 +1,7 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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. @@ -11,19 +11,19 @@ import webpack from 'webpack'; import webpackConfig from './webpack.config'; /** - * Creates application bundles from the source files. + * Creates client-side application bundle from the source files. */ -function bundle() { +function compileClient() { return new Promise((resolve, reject) => { webpack(webpackConfig).run((err, stats) => { if (err) { return reject(err); } - console.log(stats.toString(webpackConfig[0].stats)); + console.log(stats.toString(webpackConfig.stats)); return resolve(); }); }); } -export default bundle; +export default compileClient; diff --git a/tools/compileServer.js b/tools/compileServer.js new file mode 100644 index 0000000000..b598d72b68 --- /dev/null +++ b/tools/compileServer.js @@ -0,0 +1,49 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present 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 path from 'path'; +const babel = require('babel-core'); +import { readFile, writeFile, copyFile, readDir, makeDir } from './lib/fs'; + +/** + * Compile server-side application from the source files. + */ +async function compileServer() { + const dirs = await readDir('**/*.*', { + cwd: 'src', + nosort: true, + dot: false, + ignore: [ + 'client.js', + 'public/*', + '**/*.css', + '**/*.client.js', + '**/*.test.js', + ], + }); + + await Promise.all(dirs.map(async dir => { + const from = path.resolve('src', dir); + const to = path.resolve('build', dir); + const ext = path.extname(dir); + await makeDir(path.dirname(to)); + if (ext === '.js') { + const file = await readFile(from); + const result = babel.transform(file, { + filename: dir, + filenameRelative: from, + sourceMaps: 'inline', + }); + return await writeFile(to, result.code); + } + return await copyFile(from, to); + })); +} + +export default compileServer; diff --git a/tools/copy.js b/tools/copy.js index 4144ade903..971b056f96 100644 --- a/tools/copy.js +++ b/tools/copy.js @@ -1,51 +1,46 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 path from 'path'; -import gaze from 'gaze'; -import Promise from 'bluebird'; -import fs from './lib/fs'; +import { writeFile, copyFile, readDir, makeDir } from './lib/fs'; import pkg from '../package.json'; + /** * Copies static files such as robots.txt, favicon.ico to the * output (build) folder. */ -async function copy({ watch } = {}) { - const ncp = Promise.promisify(require('ncp')); +function copy() { + return Promise.all([ + makeDir('build').then(() => + writeFile('build/package.json', JSON.stringify({ + private: true, + engines: pkg.engines, + dependencies: pkg.dependencies, + scripts: { + start: 'node server.js', + }, + }, null, 2)) + ), - await Promise.all([ - ncp('src/public', 'build/public'), - ncp('src/content', 'build/content'), + readDir('**/*.*', { + cwd: 'src/public', + nosort: true, + dot: true, + }).then(dirs => + Promise.all(dirs.map(async dir => { + const from = path.resolve('src/public', dir); + const to = path.resolve('build/public', dir); + await makeDir(path.dirname(to)); + return await copyFile(from, to); + })) + ), ]); - - await fs.writeFile('./build/package.json', JSON.stringify({ - private: true, - engines: pkg.engines, - dependencies: pkg.dependencies, - scripts: { - start: 'node server.js', - }, - }, null, 2)); - - if (watch) { - const watcher = await new Promise((resolve, reject) => { - gaze('src/content/**/*.*', (err, val) => err ? reject(err) : resolve(val)); - }); - - const cp = async (file) => { - const relPath = file.substr(path.join(__dirname, '../src/content/').length); - await ncp(`src/content/${relPath}`, `build/content/${relPath}`); - }; - - watcher.on('changed', cp); - watcher.on('added', cp); - } } export default copy; diff --git a/tools/deploy.js b/tools/deploy.js index d8781c4a40..91598116ea 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -1,57 +1,25 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 GitRepo from 'git-repository'; import run from './run'; -import fetch from './lib/fetch'; +import deployToAzureWebApps from './deployToAzureWebApps'; +import deployToGitHubPages from './deployToGitHubPages'; -// TODO: Update deployment URL -// For more information visit http://gitolite.com/deploy.html -const getRemote = (slot) => ({ - name: slot || 'production', - url: `https://example${slot ? `-${slot}` : ''}.scm.azurewebsites.net:443/example.git`, - website: `http://example${slot ? `-${slot}` : ''}.azurewebsites.net`, -}); - -/** - * Deploy the contents of the `/build` folder to a remote - * server via Git. Example: `npm run deploy -- production` - */ async function deploy() { - // 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 - if ((await repo.hasRef(remote.url, 'master'))) { - await repo.fetch(remote.name); - await repo.reset(`${remote.name}/master`, { hard: true }); - await repo.clean({ force: true }); + switch (process.argv[3]) { + case 'azure': + return run(deployToAzureWebApps); + case 'gh': + return run(deployToGitHubPages); + default: + return console.log('Use `npm run deploy -- gh` or `npm run deploy -- azure`'); } - - // 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'); - await repo.push(remote.name, 'master'); - - // Check if the site was successfully deployed - const response = await fetch(remote.website); - console.log(`${remote.website} -> ${response.statusCode}`); } export default deploy; diff --git a/tools/deployToAzureWebApps.js b/tools/deployToAzureWebApps.js new file mode 100644 index 0000000000..15a61530d6 --- /dev/null +++ b/tools/deployToAzureWebApps.js @@ -0,0 +1,57 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present 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 GitRepo from 'git-repository'; +import run from './run'; +import build from './build'; + +// 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 Azure Web Apps. + */ +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); + 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(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.statusCode}`); +} + +export default deployToAzureWebApps; diff --git a/tools/deployToGitHubPages.js b/tools/deployToGitHubPages.js new file mode 100644 index 0000000000..3946dca35d --- /dev/null +++ b/tools/deployToGitHubPages.js @@ -0,0 +1,46 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-present 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 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; diff --git a/tools/lib/fetch.js b/tools/lib/fetch.js deleted file mode 100644 index cfb5f51a69..0000000000 --- a/tools/lib/fetch.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * 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 http from 'http'; - -export default async (url) => new Promise((resolve, reject) => - http.get(url, res => resolve(res)).on('error', err => reject(err)) -); diff --git a/tools/lib/fs.js b/tools/lib/fs.js index fa028b8e69..005a0d61df 100644 --- a/tools/lib/fs.js +++ b/tools/lib/fs.js @@ -1,21 +1,54 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 glob from 'glob'; import mkdirp from 'mkdirp'; +import rimraf from 'rimraf'; -const writeFile = (file, contents) => new Promise((resolve, reject) => { +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()); }); -const makeDir = (name) => new Promise((resolve, reject) => { +export const copyFile = (source, target) => new Promise((resolve, reject) => { + let cbCalled = false; + function done(err) { + if (!cbCalled) { + if (err) { + reject(err); + } else { + resolve(); + } + cbCalled = true; + } + } + + 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 default { writeFile, makeDir }; +export const cleanDir = (pattern, options) => new Promise((resolve, reject) => + rimraf(pattern, { glob: options }, (err, result) => err ? reject(err) : resolve(result)) +); diff --git a/tools/render.js b/tools/render.js index 5512f8e173..6998fb13b4 100644 --- a/tools/render.js +++ b/tools/render.js @@ -1,16 +1,16 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 runServer from './runServer'; -import fs from './lib/fs'; +import path from 'path'; import fetch from 'node-fetch'; -import { host } from '../src/config'; +import { writeFile, makeDir } from './lib/fs'; +import runServer from './runServer'; // Enter your paths here which you want to render as static const routes = [ @@ -24,22 +24,20 @@ const routes = [ ]; async function render() { - let server; - await new Promise(resolve => (server = runServer(resolve))); - - await routes.reduce((promise, route) => promise.then(async () => { - const url = `http://${host}${route}`; - const dir = `build/public${route.replace(/[^\/]*$/, '')}`; - const name = route.endsWith('/') ? 'index.html' : `${route.match(/[^/]+$/)[0]}.html`; + const server = await runServer(); + const result = await Promise.all(routes.map(async route => { + const url = `http://${server.host}${route}`; + const dir = path.resolve('build/public', path.dirname(route)); + const name = route.endsWith('/') ? 'index.html' : `${path.basename(route, '.html')}.html`; const dist = `${dir}${name}`; const res = await fetch(url); const text = await res.text(); - await fs.makeDir(dir); - await fs.writeFile(dist, text); - console.log(`${dist} => ${res.status} ${res.statusText}`); - }), Promise.resolve()); - + await makeDir(dir); + await writeFile(dist, text); + return `${dist} => ${res.status} ${res.statusText}`; + })); server.kill('SIGTERM'); + console.log(result.join('\n')); } export default render; diff --git a/tools/run.js b/tools/run.js index a42f0a5eb4..db0b0d44d2 100644 --- a/tools/run.js +++ b/tools/run.js @@ -1,7 +1,7 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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. @@ -15,13 +15,13 @@ 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})` : ''}'...` + `[${format(start)}] Starting '${task.name}${options ? ` (${options})` : ''}'...` ); return task(options).then(() => { const end = new Date(); const time = end.getTime() - start.getTime(); console.log( - `[${format(end)}] Finished '${task.name}${options ? `(${options})` : ''}' after ${time} ms` + `[${format(end)}] Finished '${task.name}${options ? ` (${options})` : ''}' after ${time} ms` ); }); } diff --git a/tools/runServer.js b/tools/runServer.js index f2bb51316c..51545850d8 100644 --- a/tools/runServer.js +++ b/tools/runServer.js @@ -1,53 +1,49 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 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(cb) { - 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.stdout.removeListener('data', onStdOut); - server.stdout.on('data', x => process.stdout.write(x)); - if (cb) { - cb(null, match[1]); +function runServer() { + return new Promise(resolve => { + 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)); + resolve(server); } } - } - if (server) { - server.kill('SIGTERM'); - } + if (server) { + server.kill('SIGTERM'); + } - server = cp.spawn('node', [serverPath], { - env: Object.assign({ NODE_ENV: 'development' }, process.env), - silent: false, - }); + server = cp.spawn('node', ['build/server.js'], { + env: Object.assign({ NODE_ENV: 'development' }, process.env), + silent: false, + }); - server.stdout.on('data', onStdOut); - server.stderr.on('data', x => process.stderr.write(x)); - return server; + server.stdout.on('data', onStdOut); + server.stderr.on('data', x => process.stderr.write(x)); + }); } process.on('exit', () => { diff --git a/tools/start.js b/tools/start.js index 754ff386c5..4cac1f717f 100644 --- a/tools/start.js +++ b/tools/start.js @@ -1,23 +1,22 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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 Browsersync from 'browser-sync'; +import browserSync from 'browser-sync'; import webpack from 'webpack'; -import webpackMiddleware from 'webpack-middleware'; +import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware'; +import config from './webpack.config'; import run from './run'; -import runServer from './runServer'; -import webpackConfig from './webpack.config'; import clean from './clean'; import copy from './copy'; - -const DEBUG = !process.argv.includes('--release'); +import compileServer from './compileServer'; +import runServer from './runServer'; /** * Launches a development web server with "live reload" functionality - @@ -25,83 +24,51 @@ const DEBUG = !process.argv.includes('--release'); */ async function start() { await run(clean); - await run(copy.bind(undefined, { watch: true })); + await Promise.all([ + run(copy), + run(compileServer), + ]); await new Promise(resolve => { - // Patch the client-side bundle configurations - // to enable Hot Module Replacement (HMR) and React Transform - webpackConfig.filter(x => x.target !== 'node').forEach(config => { - /* eslint-disable no-param-reassign */ - config.entry = ['webpack-hot-middleware/client'].concat(config.entry); + // Hot Module Replacement (HMR) + React Hot Reload + if (config.debug) { + config.entry.unshift('react-hot-loader/patch', 'webpack-hot-middleware/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()); - config - .module - .loaders - .filter(x => x.loader === 'babel-loader') - .forEach(x => (x.query = { - ...x.query, + } - // Wraps all React components into arbitrary transforms - // https://github.com/gaearon/babel-plugin-react-transform - plugins: [ - ...(x.query ? x.query.plugins : []), - ['react-transform', { - transforms: [ - { - transform: 'react-transform-hmr', - imports: ['react'], - locals: ['module'], - }, { - transform: 'react-transform-catch-errors', - imports: ['react', 'redbox-react'], - }, - ], - }, - ], - ], - })); - /* eslint-enable no-param-reassign */ - }); + const bundler = webpack(config); - const bundler = webpack(webpackConfig); - const wpMiddleware = webpackMiddleware(bundler, { + let handleServerBundleComplete = async () => { + const server = await runServer(); + const bs = browserSync.create(); + bs.init({ + ...(config.debug ? {} : { notify: false, ui: false }), - // IMPORTANT: webpack middleware can't access config, - // so we should provide publicPath by ourselves - publicPath: webpackConfig[0].output.publicPath, + proxy: { + target: server.host, + middleware: [ + webpackDevMiddleware(bundler, { + // IMPORTANT: webpack middleware can't access config, + // so we should provide publicPath by ourselves + publicPath: config.output.publicPath, - // Pretty colored output - stats: webpackConfig[0].stats, + // Pretty colored output + stats: config.stats, - // For other settings see - // https://webpack.github.io/docs/webpack-dev-middleware - }); - const hotMiddlewares = bundler - .compilers - .filter(compiler => compiler.options.target !== 'node') - .map(compiler => webpackHotMiddleware(compiler)); + // For other settings see + // https://webpack.github.io/docs/webpack-dev-middleware + }), - let handleServerBundleComplete = () => { - runServer((err, host) => { - if (!err) { - const bs = Browsersync.create(); - bs.init({ - ...(DEBUG ? {} : { notify: false, ui: false }), - - proxy: { - target: host, - middleware: [wpMiddleware, ...hotMiddlewares], - }, - - // no need to watch '*.js' here, webpack will take care of it for us, - // including full page reloads if HMR won't work - files: ['build/content/**/*.*'], - }, resolve); - handleServerBundleComplete = runServer; - } - }); + // bundler should be the same as above + webpackHotMiddleware(bundler), + ], + }, + }, resolve); + handleServerBundleComplete = runServer; }; bundler.plugin('done', () => handleServerBundleComplete()); diff --git a/tools/webpack.config.js b/tools/webpack.config.js index 4af4d52c1f..74225abe69 100644 --- a/tools/webpack.config.js +++ b/tools/webpack.config.js @@ -1,7 +1,7 @@ /** * React Starter Kit (https://www.reactstarterkit.com/) * - * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * Copyright © 2014-present 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. @@ -9,52 +9,107 @@ import path from 'path'; import webpack from 'webpack'; -import extend from 'extend'; import AssetsPlugin from 'assets-webpack-plugin'; -const DEBUG = !process.argv.includes('--release'); -const VERBOSE = process.argv.includes('--verbose'); -const AUTOPREFIXER_BROWSERS = [ - 'Android 2.3', - 'Android >= 4', - 'Chrome >= 35', - 'Firefox >= 31', - 'Explorer >= 9', - 'iOS >= 7', - 'Opera >= 12', - 'Safari >= 7.1', -]; -const GLOBALS = { - 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"', - __DEV__: DEBUG, -}; - -// -// Common configuration chunk to be used for both -// client-side (client.js) and server-side (server.js) bundles -// ----------------------------------------------------------------------------- +const isDebug = !process.argv.includes('--release'); +const isVerbose = process.argv.includes('--verbose'); +/** + * Webpack configuration (src/client.js => build/public/assets/main.js) + * http://webpack.github.io/docs/configuration.html + */ const config = { + + // The base directory context: path.resolve(__dirname, '../src'), + // The entry point for the bundle + entry: ['./client.js'], + + // Options affecting the output of the compilation output: { path: path.resolve(__dirname, '../build/public/assets'), publicPath: '/assets/', + filename: isDebug ? '[name].js?[chunkhash]' : '[name].[chunkhash].js', + chunkFilename: isDebug ? '[name].[id].js?[chunkhash]' : '[name].[id].[chunkhash].js', sourcePrefix: ' ', }, + // Switch loaders to debug or release mode + debug: isDebug, + + // Developer tool to enhance debugging, source maps + // http://webpack.github.io/docs/configuration.html#devtool + devtool: isDebug ? 'source-map' : false, + + // What information should be printed to the console + stats: { + colors: true, + reasons: isDebug, + hash: isVerbose, + version: isVerbose, + timings: true, + chunks: isVerbose, + chunkModules: isVerbose, + cached: isVerbose, + cachedAssets: isVerbose, + }, + + // The list of plugins for Webpack compiler + 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.json', + prettyPrint: true, + }), + + // 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), + + ...isDebug ? [] : [ + + // 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, + warnings: isVerbose, + }, + }), + + // A plugin for a more aggressive chunk merging strategy + // https://webpack.github.io/docs/list-of-plugins.html#aggressivemergingplugin + new webpack.optimize.AggressiveMergingPlugin(), + ], + ], + + // Options affecting the normal modules module: { loaders: [ { - test: /\.jsx?$/, + test: /\.js$/, loader: 'babel-loader', - include: [ - path.resolve(__dirname, '../node_modules/react-routing/src'), - path.resolve(__dirname, '../src'), - ], + include: path.resolve(__dirname, '../src'), query: { // https://github.com/babel/babel-loader#options - cacheDirectory: DEBUG, + cacheDirectory: isDebug, // https://babeljs.io/docs/usage/options/ babelrc: false, @@ -65,7 +120,7 @@ const config = { ], plugins: [ 'transform-runtime', - ...DEBUG ? [] : [ + ...isDebug ? [] : [ 'transform-react-remove-prop-types', 'transform-react-constant-elements', 'transform-react-inline-elements', @@ -78,12 +133,12 @@ const config = { loaders: [ 'isomorphic-style-loader', `css-loader?${JSON.stringify({ - sourceMap: DEBUG, + sourceMap: isDebug, // CSS Modules https://github.com/css-modules/css-modules modules: true, - localIdentName: DEBUG ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]', + localIdentName: isDebug ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]', // CSS Nano http://cssnano.co/options/ - minimize: !DEBUG, + minimize: !isDebug, })}`, 'postcss-loader?pack=default', ], @@ -92,7 +147,7 @@ const config = { test: /\.scss$/, loaders: [ 'isomorphic-style-loader', - `css-loader?${JSON.stringify({ sourceMap: DEBUG, minimize: !DEBUG })}`, + `css-loader?${JSON.stringify({ sourceMap: isDebug, minimize: !isDebug })}`, 'postcss-loader?pack=sass', 'sass-loader', ], @@ -109,7 +164,7 @@ const config = { test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/, loader: 'url-loader', query: { - name: DEBUG ? '[path][name].[ext]?[hash]' : '[hash].[ext]', + name: isDebug ? '[path][name].[ext]?[hash]' : '[hash].[ext]', limit: 10000, }, }, @@ -117,7 +172,7 @@ const config = { test: /\.(eot|ttf|wav|mp3)$/, loader: 'file-loader', query: { - name: DEBUG ? '[path][name].[ext]?[hash]' : '[hash].[ext]', + name: isDebug ? '[path][name].[ext]?[hash]' : '[hash].[ext]', }, }, { @@ -127,27 +182,8 @@ const config = { ], }, - resolve: { - root: path.resolve(__dirname, '../src'), - modulesDirectories: ['node_modules'], - extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx', '.json'], - }, - - cache: DEBUG, - debug: DEBUG, - - stats: { - colors: true, - reasons: DEBUG, - hash: VERBOSE, - version: VERBOSE, - timings: true, - chunks: VERBOSE, - chunkModules: VERBOSE, - cached: VERBOSE, - cachedAssets: VERBOSE, - }, - + // The list of plugins for PostCSS + // https://github.com/postcss/postcss postcss(bundler) { return { default: [ @@ -189,121 +225,13 @@ const config = { require('postcss-selector-not')(), // Add vendor prefixes to CSS rules using values from caniuse.com // https://github.com/postcss/autoprefixer - require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS }), + require('autoprefixer')(), ], sass: [ - require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS }), + require('autoprefixer')(), ], }; }, }; -// -// Configuration for the client-side bundle (client.js) -// ----------------------------------------------------------------------------- - -const clientConfig = extend(true, {}, config, { - entry: './client.js', - - output: { - filename: DEBUG ? '[name].js?[chunkhash]' : '[name].[chunkhash].js', - chunkFilename: DEBUG ? '[name].[id].js?[chunkhash]' : '[name].[id].[chunkhash].js', - }, - - target: 'web', - - plugins: [ - - // Define free variables - // https://webpack.github.io/docs/list-of-plugins.html#defineplugin - new webpack.DefinePlugin({ ...GLOBALS, 'process.env.BROWSER': true }), - - // 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)};`, - }), - - // 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.OccurenceOrderPlugin(true), - - ...DEBUG ? [] : [ - - // 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, // jscs:ignore requireCamelCaseOrUpperCaseIdentifiers - warnings: VERBOSE, - }, - }), - - // A plugin for a more aggressive chunk merging strategy - // https://webpack.github.io/docs/list-of-plugins.html#aggressivemergingplugin - new webpack.optimize.AggressiveMergingPlugin(), - ], - ], - - // Choose a developer tool to enhance debugging - // http://webpack.github.io/docs/configuration.html#devtool - devtool: DEBUG ? 'cheap-module-eval-source-map' : false, -}); - -// -// Configuration for the server-side bundle (server.js) -// ----------------------------------------------------------------------------- - -const serverConfig = extend(true, {}, config, { - entry: './server.js', - - output: { - filename: '../../server.js', - libraryTarget: 'commonjs2', - }, - - target: 'node', - - externals: [ - /^\.\/assets$/, - function filter(context, request, cb) { - const isExternal = - request.match(/^[@a-z][a-z\/\.\-0-9]*$/i) && - !request.match(/^react-routing/) && - !context.match(/[\\/]react-routing/); - cb(null, Boolean(isExternal)); - }, - ], - - plugins: [ - - // Define free variables - // https://webpack.github.io/docs/list-of-plugins.html#defineplugin - new webpack.DefinePlugin({ ...GLOBALS, 'process.env.BROWSER': false }), - - // 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: 'source-map', -}); - -export default [clientConfig, serverConfig]; +export default config;