Skip to content

Commit

Permalink
Merge pull request #150 from halfzebra/better-env-handling-and-error-…
Browse files Browse the repository at this point in the history
…highlightning

Better env handling and error highlighting
  • Loading branch information
halfzebra authored Jul 20, 2017
2 parents 98c429d + 7f2c6bd commit 09b3989
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 190 deletions.
87 changes: 82 additions & 5 deletions config/env.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,87 @@
'use strict';

function getClientEnvironment() {
return Object.keys(process.env).reduce((acc, current) => {
acc[`process.env.${current}`] = `"${process.env[current]}"`;
return acc;
}, {});
const fs = require('fs');
const path = require('path');
const paths = require('./paths');

// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];

const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}

// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
var dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv
].filter(Boolean);

// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set.
// https://github.com/motdotla/dotenv
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv').config({
path: dotenvFile
});
}
});

// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebookincubator/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);

// Grab NODE_ENV and ELM_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const ELM_APP = /^ELM_APP_/i;

function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env).filter(key => ELM_APP.test(key)).reduce((
env,
key
) => {
env[key] = process.env[key];
return env;
}, {
// Useful for determining whether we’re running in production mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl
});
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {})
};

return { raw, stringified };
}

module.exports = getClientEnvironment;
34 changes: 24 additions & 10 deletions config/webpack.config.dev.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const path = require('path');
const autoprefixer = require('autoprefixer');
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
Expand All @@ -16,16 +17,27 @@ const publicPath = '/';
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
const publicUrl = '';
// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);

module.exports = {
devtool: 'eval',
devtool: 'cheap-module-source-map',

entry: [
// WebpackDevServer client.
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create Elm App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
require.resolve('react-dev-utils/webpackHotDevClient'),

// Replacement runtime.
require.resolve('webpack/hot/dev-server'),
// Errors should be considered fatal in development
require.resolve('react-error-overlay'),

paths.appIndexJs
],
Expand All @@ -41,7 +53,11 @@ module.exports = {
// containing code from all our entry points, and the Webpack runtime.
filename: 'static/js/bundle.js',

publicPath: publicPath
publicPath: publicPath,

// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: info =>
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')
},

resolve: {
Expand Down Expand Up @@ -89,7 +105,7 @@ module.exports = {
options: {
verbose: true,
warn: true,
debug: true,
debug: !!process.env.ELM_DEBUGGER,
pathToMake: paths.elmMake,
forceWatch: true
}
Expand Down Expand Up @@ -152,11 +168,9 @@ module.exports = {
},

plugins: [
new DefinePlugin(getClientEnvironment()),
new DefinePlugin(env.stringified),

new InterpolateHtmlPlugin({
PUBLIC_URL: publicUrl
}),
new InterpolateHtmlPlugin(env.raw),

new HtmlWebpackPlugin({
inject: true,
Expand Down
11 changes: 6 additions & 5 deletions config/webpack.config.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const shouldUseRelativeAssetPaths = publicPath === './';
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
const publicUrl = publicPath.slice(0, -1);

// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);

// Note: defined here because it will be used more than once.
const cssFilename = 'static/css/[name].[contenthash:8].css';

Expand Down Expand Up @@ -156,13 +159,11 @@ module.exports = {
},

plugins: [
new InterpolateHtmlPlugin({
PUBLIC_URL: publicUrl
}),

new AssetsPlugin({ path: paths.appBuild }),

new DefinePlugin(getClientEnvironment()),
new DefinePlugin(env.stringified),

new InterpolateHtmlPlugin(env.raw),

// Minify the compiled JavaScript.
new UglifyJsPlugin({
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
"elm-hot-loader": "0.5.4",
"elm-test": "^0.18.7",
"elm-webpack-loader": "^4.3.0",
"extract-text-webpack-plugin": "^2.1.2",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.2",
"fs-extra": "^3.0.1",
"fs-extra": "^4.0.0",
"html-webpack-plugin": "^2.29.0",
"http-proxy-middleware": "^0.17.3",
"minimist": "1.2.0",
Expand All @@ -49,12 +49,12 @@
"string-replace-loader": "^1.3.0",
"style-loader": "^0.18.1",
"url-loader": "^0.5.9",
"webpack": "^3.1.0",
"webpack": "^3.3.0",
"webpack-dev-server": "^2.5.1"
},
"devDependencies": {
"babel-eslint": "^7.2.3",
"chai": "^4.0.2",
"chai": "^4.1.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"dir-compare": "^1.3.0",
Expand All @@ -63,7 +63,7 @@
"husky": "^0.14.3",
"mocha": "^3.2.0",
"nightmare": "^2.10.0",
"prettier": "^1.5.2",
"prettier": "^1.5.3",
"rimraf": "^2.5.4",
"semantic-release": "^6.3.2"
},
Expand Down
21 changes: 17 additions & 4 deletions scripts/build.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
'use strict';

// Load environment variables from .env file.
// Suppress warnings if this file is missing.
require('dotenv').config({ silent: true });
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});

// Ensure environment variables are read.
require('../config/env');

const fs = require('fs-extra');
const chalk = require('chalk');
Expand All @@ -12,6 +22,7 @@ const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const highlightElmCompilerErrors = require('./utils/highlightElmCompilerErrors');

const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
Expand Down Expand Up @@ -79,7 +90,9 @@ function build(previousFileSizes) {
if (err) {
return reject(err);
}
const messages = formatWebpackMessages(stats.toJson({}, true));
const messages = highlightElmCompilerErrors(
formatWebpackMessages(stats.toJson({}, true))
);
if (messages.errors.length) {
return reject(new Error(messages.errors.join('\n\n')));
}
Expand Down
4 changes: 4 additions & 0 deletions scripts/eject.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ function performEject(pkg) {
try {
fs.copySync(path.resolve(__dirname, 'build.js'), './scripts/build.js');
fs.copySync(path.resolve(__dirname, 'start.js'), './scripts/start.js');
fs.copySync(
path.resolve(__dirname, './utils/highlightElmCompilerErrors.js'),
'./scripts/utils/highlightElmCompilerErrors.js'
);
fs.copySync(path.resolve(__dirname, '../config'), './config');
} catch (err) {
console.log(chalk.red('Failed to copy scripts, the error is:\n'));
Expand Down
22 changes: 18 additions & 4 deletions scripts/start.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
'use strict';

// Load environment variables from .env file.
// Suppress warnings if this file is missing.
require('dotenv').config({ silent: true });
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});

// Ensure environment variables are read.
require('../config/env');

const fs = require('fs');
const path = require('path');
Expand All @@ -19,6 +29,7 @@ const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const clearConsole = require('react-dev-utils/clearConsole');
const openBrowser = require('react-dev-utils/openBrowser');
const createDevServerConfig = require('../config/webpackDevServer.config');
const highlightElmCompilerErrors = require('./utils/highlightElmCompilerErrors');
const paths = require('../config/paths');

if (fs.existsSync('elm-package.json') === false) {
Expand Down Expand Up @@ -89,7 +100,10 @@ function createCompiler(webpack, config, appName, urls) {
// We have switched off the default Webpack output in WebpackDevServer
// options so we are going to "massage" the warnings and errors and present
// them in a readable focused way.
const messages = formatWebpackMessages(stats.toJson({}, true));
const messages = highlightElmCompilerErrors(
formatWebpackMessages(stats.toJson({}, true))
);

const isSuccessful = !messages.errors.length && !messages.warnings.length;
if (isSuccessful) {
console.log(chalk.green('Compiled successfully!'));
Expand Down
19 changes: 19 additions & 0 deletions scripts/utils/highlightElmCompilerErrors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

var chalk = require('chalk');

module.exports = function highlightElmCompilerErrors(messages) {
var errors = messages.errors;
var warnings = messages.warnings;
return errors.length > 0
? {
errors: errors.map(x =>
x
.replace(/(--\s[A-Z\s]+-+\s.*\.elm\n)/g, chalk.cyan('$1'))
.replace(/(\n\s*)(\^+)/g, '$1' + chalk.red('$2'))
.replace(/(\d+\|)(>)/g, '$1' + chalk.bold(chalk.red('$2')))
),
warnings: warnings
}
: messages;
};
6 changes: 6 additions & 0 deletions template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ You can find the most recent version of this guide [here](https://github.com/hal
- [repl](#repl)
- [make](#make)
- [reactor](#reactor)
- [Turning off Elm Debugger](#turning-off-elm-debugger)
- [Changing the Page `<title>`](#changing-the-page-title)
- [Adding a Stylesheet](#adding-a-stylesheet)
- [Adding Images and Fonts](#adding-images-and-fonts)
Expand Down Expand Up @@ -134,6 +135,11 @@ Alias for [elm-make](http://guide.elm-lang.org/get_started.html#elm-make)
#### `reactor`
Alias for [elm-reactor](http://guide.elm-lang.org/get_started.html#elm-reactor)


## Turning off Elm Debugger

To turn off Elm Debugger, set `ELM_DEBUGGER` environment variable to `false`

## Changing the Page `<title>`

You can find the source HTML file in the `public` folder of the generated project. You may edit the `<title>` tag in it to change the title from “Elm App” to anything else.
Expand Down
Loading

0 comments on commit 09b3989

Please sign in to comment.