Extract some utilities into a separate package (#723)

* Extract some utilities into a separate package

* Add utils dir to `files` in package.json

* Do not create an empty `utils` dir on eject
This commit is contained in:
Dan Abramov
2016-09-23 20:53:08 +01:00
committed by GitHub
parent c4a936ece2
commit e5bf5af296
21 changed files with 452 additions and 192 deletions

View File

@@ -27,11 +27,14 @@ var rimrafSync = require('rimraf').sync;
var webpack = require('webpack');
var config = require('../config/webpack.config.prod');
var paths = require('../config/paths');
var checkRequiredFiles = require('./utils/checkRequiredFiles');
var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
var recursive = require('recursive-readdir');
var stripAnsi = require('strip-ansi');
checkRequiredFiles();
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Input: /User/dan/app/build/static/js/main.82be8.js
// Output: /static/js/main.js

View File

@@ -7,10 +7,10 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
var createJestConfig = require('./utils/createJestConfig');
var createJestConfig = require('../utils/createJestConfig');
var fs = require('fs');
var path = require('path');
var prompt = require('./utils/prompt');
var prompt = require('react-dev-utils/prompt');
var rimrafSync = require('rimraf').sync;
var spawnSync = require('cross-spawn').sync;
@@ -31,6 +31,7 @@ prompt(
var files = [
'.babelrc',
'.eslintrc',
path.join('config', 'env.js'),
path.join('config', 'paths.js'),
path.join('config', 'polyfills.js'),
path.join('config', 'webpack.config.dev.js'),
@@ -39,13 +40,7 @@ prompt(
path.join('config', 'jest', 'FileStub.js'),
path.join('scripts', 'build.js'),
path.join('scripts', 'start.js'),
path.join('scripts', 'test.js'),
path.join('scripts', 'utils', 'checkRequiredFiles.js'),
path.join('scripts', 'utils', 'chrome.applescript'),
path.join('scripts', 'utils', 'getClientEnvironment.js'),
path.join('scripts', 'utils', 'InterpolateHtmlPlugin.js'),
path.join('scripts', 'utils', 'prompt.js'),
path.join('scripts', 'utils', 'WatchMissingNodeModulesPlugin.js')
path.join('scripts', 'test.js')
];
// Ensure that the app folder is clean and we won't override any files
@@ -65,7 +60,6 @@ prompt(
fs.mkdirSync(path.join(appPath, 'config'));
fs.mkdirSync(path.join(appPath, 'config', 'jest'));
fs.mkdirSync(path.join(appPath, 'scripts'));
fs.mkdirSync(path.join(appPath, 'scripts', 'utils'));
files.forEach(function(file) {
console.log('Copying ' + file + ' to ' + appPath);

View File

@@ -17,20 +17,25 @@ process.env.NODE_ENV = 'development';
// https://github.com/motdotla/dotenv
require('dotenv').config({silent: true});
var path = require('path');
var chalk = require('chalk');
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var historyApiFallback = require('connect-history-api-fallback');
var httpProxyMiddleware = require('http-proxy-middleware');
var execSync = require('child_process').execSync;
var opn = require('opn');
var detect = require('detect-port');
var checkRequiredFiles = require('./utils/checkRequiredFiles');
var prompt = require('./utils/prompt');
var clearConsole = require('react-dev-utils/clearConsole');
var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
var formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
var openBrowser = require('react-dev-utils/openBrowser');
var prompt = require('react-dev-utils/prompt');
var config = require('../config/webpack.config.dev');
var paths = require('../config/paths');
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
var DEFAULT_PORT = process.env.PORT || 3000;
var compiler;
@@ -49,40 +54,6 @@ if (isSmokeTest) {
};
}
// Some custom utilities to prettify Webpack output.
// This is a little hacky.
// It would be easier if webpack provided a rich error object.
var friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1;
}
function formatMessage(message) {
return message
// Make some common errors shorter:
.replace(
// Babel syntax error
'Module build failed: SyntaxError:',
friendlySyntaxErrorLabel
)
.replace(
// Webpack file not found error
/Module not found: Error: Cannot resolve 'file' or 'directory'/,
'Module not found:'
)
// Internal stacks are generally useless so we strip them
.replace(/^\s*at\s.*:\d+:\d+[\s\)]*\n/gm, '') // at ... ...:x:y
// Webpack loader names obscure CSS filenames
.replace('./~/css-loader!./~/postcss-loader!', '');
}
var isFirstClear = true;
function clearConsole() {
// On first run, clear completely so it doesn't show half screen on Windows.
// On next runs, use a different sequence that properly scrolls back.
process.stdout.write(isFirstClear ? '\x1bc' : '\x1b[2J\x1b[0f');
isFirstClear = false;
}
function setupCompiler(host, port, protocol) {
// "Compiler" is a low-level interface to Webpack.
// It lets us listen to some events and provide our own custom messages.
@@ -101,9 +72,12 @@ function setupCompiler(host, port, protocol) {
// Whether or not you have warnings or errors, you will get this event.
compiler.plugin('done', function(stats) {
clearConsole();
var hasErrors = stats.hasErrors();
var hasWarnings = stats.hasWarnings();
if (!hasErrors && !hasWarnings) {
// 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.
var messages = formatWebpackMessages(stats);
if (!messages.errors.length && !messages.warnings.length) {
console.log(chalk.green('Compiled successfully!'));
console.log();
console.log('The app is running at:');
@@ -113,41 +87,24 @@ function setupCompiler(host, port, protocol) {
console.log('Note that the development build is not optimized.');
console.log('To create a production build, use ' + chalk.cyan('npm run build') + '.');
console.log();
return;
}
// 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.
// We use stats.toJson({}, true) to make output more compact and readable:
// https://github.com/facebookincubator/create-react-app/issues/401#issuecomment-238291901
var json = stats.toJson({}, true);
var formattedErrors = json.errors.map(message =>
'Error in ' + formatMessage(message)
);
var formattedWarnings = json.warnings.map(message =>
'Warning in ' + formatMessage(message)
);
if (hasErrors) {
// If errors exist, only show errors.
if (messages.errors.length) {
console.log(chalk.red('Failed to compile.'));
console.log();
if (formattedErrors.some(isLikelyASyntaxError)) {
// If there are any syntax errors, show just them.
// This prevents a confusing ESLint parsing error
// preceding a much more useful Babel syntax error.
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
}
formattedErrors.forEach(message => {
messages.errors.forEach(message => {
console.log(message);
console.log();
});
// If errors exist, ignore warnings.
return;
}
if (hasWarnings) {
// Show warnings if no errors were found.
if (messages.warnings.length) {
console.log(chalk.yellow('Compiled with warnings.'));
console.log();
formattedWarnings.forEach(message => {
messages.warnings.forEach(message => {
console.log(message);
console.log();
});
@@ -159,30 +116,6 @@ function setupCompiler(host, port, protocol) {
});
}
function openBrowser(host, port, protocol) {
if (process.platform === 'darwin') {
try {
// Try our best to reuse existing tab
// on OS X Google Chrome with AppleScript
execSync('ps cax | grep "Google Chrome"');
execSync(
'osascript chrome.applescript ' + protocol + '://' + host + ':' + port + '/',
{cwd: path.join(__dirname, 'utils'), stdio: 'ignore'}
);
return;
} catch (err) {
// Ignore errors.
}
}
// Fallback to opn
// (It will always open new tab)
try {
opn(protocol + '://' + host + ':' + port + '/');
} catch (err) {
// Ignore errors.
}
}
// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
function onProxyError(proxy) {
@@ -314,14 +247,13 @@ function runDevServer(host, port, protocol) {
clearConsole();
console.log(chalk.cyan('Starting the development server...'));
console.log();
openBrowser(host, port, protocol);
openBrowser(protocol + '://' + host + ':' + port + '/');
});
}
function run(port) {
var protocol = process.env.HTTPS === 'true' ? "https" : "http";
var host = process.env.HOST || 'localhost';
checkRequiredFiles();
setupCompiler(host, port, protocol);
runDevServer(host, port, protocol);
}

View File

@@ -28,7 +28,7 @@ if (!process.env.CI) {
// @remove-on-eject-begin
// This is not necessary after eject because we embed config into package.json.
const createJestConfig = require('./utils/createJestConfig');
const createJestConfig = require('../utils/createJestConfig');
const path = require('path');
const paths = require('../config/paths');
argv.push('--config', JSON.stringify(createJestConfig(

View File

@@ -1,43 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
// This Webpack plugin lets us interpolate custom variables into `index.html`.
// Usage: `new InterpolateHtmlPlugin({ 'MY_VARIABLE': 42 })`
// Then, you can use %MY_VARIABLE% in your `index.html`.
// It works in tandem with HtmlWebpackPlugin.
// Learn more about creating plugins like this:
// https://github.com/ampedandwired/html-webpack-plugin#events
'use strict';
class InterpolateHtmlPlugin {
constructor(replacements) {
this.replacements = replacements;
}
apply(compiler) {
compiler.plugin('compilation', compilation => {
compilation.plugin('html-webpack-plugin-before-html-processing',
(data, callback) => {
// Run HTML through a series of user-specified string replacements.
Object.keys(this.replacements).forEach(key => {
const value = this.replacements[key];
data.html = data.html.replace('%' + key + '%', value);
});
callback(null, data);
}
);
});
}
}
module.exports = InterpolateHtmlPlugin;

View File

@@ -1,35 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
// This Webpack plugin ensures `npm install <library>` forces a project rebuild.
// Were not sure why this isn't Webpack's default behavior.
// See https://github.com/facebookincubator/create-react-app/issues/186.
function WatchMissingNodeModulesPlugin(nodeModulesPath) {
this.nodeModulesPath = nodeModulesPath;
}
WatchMissingNodeModulesPlugin.prototype.apply = function (compiler) {
compiler.plugin('emit', (compilation, callback) => {
var missingDeps = compilation.missingDependencies;
var nodeModulesPath = this.nodeModulesPath;
// If any missing files are expected to appear in node_modules...
if (missingDeps.some(file => file.indexOf(nodeModulesPath) !== -1)) {
// ...tell webpack to watch node_modules recursively until they appear.
compilation.contextDependencies.push(nodeModulesPath);
}
callback();
});
}
module.exports = WatchMissingNodeModulesPlugin;

View File

@@ -1,33 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const paths = require('../../config/paths');
function checkRequiredFiles() {
const filesPathToCheck = [paths.appHtml, paths.appIndexJs];
filesPathToCheck.forEach(filePath => {
try {
fs.accessSync(filePath, fs.F_OK);
} catch (err) {
const dirName = path.dirname(filePath);
const fileName = path.basename(filePath);
console.log(chalk.red('Could not find a required file.'));
console.log(chalk.red(' Name: ') + chalk.cyan(fileName));
console.log(chalk.red(' Searched in: ') + chalk.cyan(dirName));
process.exit(1);
}
});
}
module.exports = checkRequiredFiles;

View File

@@ -1,50 +0,0 @@
-- @remove-on-eject-begin
(*
Copyright (c) 2015-present, Facebook, Inc.
All rights reserved.
This source code is licensed under the BSD-style license found in the
-- LICENSE file in the root directory of this source tree. An additional grant
of patent rights can be found in the PATENTS file in the same directory.
*)
-- @remove-on-eject-end
on run argv
set theURL to item 1 of argv
tell application "Chrome"
if (count every window) = 0 then
make new window
end if
-- Find a tab currently running the debugger
set found to false
set theTabIndex to -1
repeat with theWindow in every window
set theTabIndex to 0
repeat with theTab in every tab of theWindow
set theTabIndex to theTabIndex + 1
if theTab's URL is theURL then
set found to true
exit repeat
end if
end repeat
if found then
exit repeat
end if
end repeat
if found then
tell theTab to reload
set index of theWindow to 1
set theWindow's active tab index to theTabIndex
else
tell window 1
activate
make new tab with properties {URL:theURL}
end tell
end if
end tell
end run

View File

@@ -1,43 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
const pathExists = require('path-exists');
const paths = require('../../config/paths');
module.exports = (resolve, rootDir, isEjecting) => {
const setupFiles = [resolve('config/polyfills.js')];
if (pathExists.sync(paths.testsSetup)) {
// Use this instead of `paths.testsSetup` to avoid putting
// an absolute filename into configuration after ejecting.
setupFiles.push('<rootDir>/src/setupTests.js');
}
const config = {
moduleFileExtensions: ['jsx', 'js', 'json'],
moduleNameMapper: {
'^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': resolve('config/jest/FileStub.js'),
'^.+\\.css$': resolve('config/jest/CSSStub.js')
},
setupFiles: setupFiles,
testPathIgnorePatterns: ['<rootDir>/(build|docs|node_modules)/'],
testEnvironment: 'node',
testRegex: '(/__tests__/.*|\\.(test|spec))\\.(js|jsx)$',
};
if (rootDir) {
config.rootDir = rootDir;
}
if (!isEjecting) {
// This is unnecessary after ejecting because Jest
// will just use .babelrc in the project folder.
config.scriptPreprocessor = resolve('config/jest/transform.js');
}
return config;
};

View File

@@ -1,38 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
var REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
return Object
.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce((env, key) => {
env['process.env.' + key] = JSON.stringify(process.env[key]);
return env;
}, {
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
'process.env.NODE_ENV': JSON.stringify(
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.
'process.env.PUBLIC_URL': JSON.stringify(publicUrl)
});
}
module.exports = getClientEnvironment;

View File

@@ -1,42 +0,0 @@
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
// @remove-on-eject-end
var rl = require('readline');
// Convention: "no" should be the conservative choice.
// If you mistype the answer, we'll always take it as a "no".
// You can control the behavior on <Enter> with `isYesDefault`.
module.exports = function (question, isYesDefault) {
if (typeof isYesDefault !== 'boolean') {
throw new Error('Provide explicit boolean isYesDefault as second argument.');
}
return new Promise(resolve => {
var rlInterface = rl.createInterface({
input: process.stdin,
output: process.stdout,
});
var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
var message = question + ' ' + hint + '\n';
rlInterface.question(message, function(answer) {
rlInterface.close();
var useDefault = answer.trim().length === 0;
if (useDefault) {
return resolve(isYesDefault);
}
var isYes = answer.match(/^(yes|y)$/i);
return resolve(isYes);
});
});
};