Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC of providing a watching build mode that writes to the disk in development #1616

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"screencast": "svg-term --cast hItN7sl5yfCPTHxvFg5glhhfp --out screencast.svg --window",
"test": "node packages/react-scripts/scripts/test.js --env=jsdom",
"format": "prettier --trailing-comma es5 --single-quote --write 'packages/*/*.js' 'packages/*/!(node_modules)/**/*.js'",
"precommit": "lint-staged"
"precommit": "lint-staged",
"watch": "cd packages/react-scripts && node scripts/watch.js"
},
"devDependencies": {
"eslint": "^4.4.1",
Expand All @@ -32,4 +33,4 @@
"git rm --cached"
]
}
}
}
29 changes: 29 additions & 0 deletions packages/react-dev-utils/cleanBuildFolder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use strict";
var recursive = require("recursive-readdir");
var path = require("path");
var fs = require("fs");
var difference = require("lodash/difference");

module.exports = function cleanBuildFolder(appBuild, stats) {
return new Promise(resolve => {
// Read the current contents of build folder
recursive(appBuild, (err, fileNames) => {
var assets = stats.toJson().assets;
var assetFileNames = assets
.map(asset => path.join(appBuild, asset.name)) // Get the full path from stats
.filter(Boolean);

// Check the differences in hash
var differences = difference(fileNames,assetFileNames)
.filter((names) => names.includes(path.join(appBuild, 'static')))
// Only delete in the static folder

differences.forEach(file => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
});
resolve(differences);
});
});
};
1 change: 1 addition & 0 deletions packages/react-scripts/bin/react-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];
switch (script) {
case 'build':
case 'eject':
case 'watch':
case 'start':
case 'test': {
const result = spawn.sync(
Expand Down
1 change: 1 addition & 0 deletions packages/react-scripts/scripts/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module.exports = function(
appPackage.scripts = {
start: 'react-scripts start',
build: 'react-scripts build',
watch: 'react-scripts watch',
test: 'react-scripts test --env=jsdom',
eject: 'react-scripts eject',
};
Expand Down
227 changes: 227 additions & 0 deletions packages/react-scripts/scripts/watch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// @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
'use strict';
process.env.NODE_ENV = 'development';

// Load environment variables from .env file. 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
require('dotenv').config({ silent: true });

const chalk = require('chalk');
const fs = require('fs-extra');
const webpack = require('webpack');

const config = require('../config/webpack.config.dev');
const paths = require('../config/paths');

const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const cleanBuildFolder = require('react-dev-utils/cleanBuildFolder');
const inquirer = require('inquirer');

const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;

const useYarn = fs.existsSync(paths.yarnLockFile);
const cli = useYarn ? 'yarn' : 'npm';
const isInteractive = process.stdout.isTTY;

// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}

const message = `Note that running in watch mode is slower and only recommended if you need to
serve the assets with your own back-end in development. It's not recommended if
your use case is creating a single page application. Use npm start if that's the case.
Also, don't deploy the code before running npm run build.`;

const question = `

Continue running in watching mode?

`;

clearConsoleIfInteractive();
inquirer
.prompt({
type: 'confirm',
name: 'shouldChangePort',
message: chalk.yellow(message) + question,
default: true,
})
.then(accept => {
if (accept) {
run();
} else {
process.exit();
}
});

function run() {
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
measureFileSizesBeforeBuild(paths.appBuild)
.then(previousFileSizes => {
// Start the webpack watch mode
watch(previousFileSizes);
})
.catch(console.log);
}

const watchConfig = Object.assign({}, config, {
watch: true,
output: Object.assign({}, config.output, {
path: paths.appBuild,
}),
});

function watch(previousFileSizes) {
clearConsoleIfInteractive();
const watcher = webpack(watchConfig, () => {});
const compiler = watcher.compiler;

console.log('Compiling ' + process.env.NODE_ENV + ' build...');
compiler.plugin('done', createCompilerDoneHandler(previousFileSizes));
compiler.plugin('invalid', handleCompilerInvalid);
}

function handleCompilerInvalid() {
clearConsoleIfInteractive();

console.log('Compiling...');
}

function createCompilerDoneHandler(previousFileSizes) {
return stats => {
clearConsoleIfInteractive();
cleanUpAndPrintMessages(stats, previousFileSizes);
};
}

function cleanUpAndPrintMessages(stats, previousFileSizes) {
cleanBuildFolder(paths.appBuild, stats)
.then(removedFiles => {
if (removedFiles.length) {
console.log(
'Deleting up old assets in the build folder:\n',
removedFiles.join('\n')
);
}
copyPublicFolder(); // Update public folder

const messages = formatWebpackMessages(stats.toJson({}, true));
const isSuccessful = !messages.errors.length && !messages.warnings.length;

if (isSuccessful) {
printWatchSuccessMessage(messages, stats, previousFileSizes);
return printWaitingChanges();
}

// If errors exist, only print errors.
if (messages.errors.length) {
printErrors(messages);
return printWaitingChanges();
}

// Print warnings if no errors were found.
if (messages.warnings.length) {
printWarnings(messages);
return printWaitingChanges();
}
})
.catch(error => {
console.error(error);
});
}

function printErrors(messages) {
console.log(chalk.red('Failed to compile.'));
console.log();
messages.errors.forEach(message => {
console.log(message);
console.log();
});
}

function printWatchSuccessMessage(messages, stats, previousFileSizes) {
console.log(
[
chalk.green(
'Successfully compiled a ' + process.env.NODE_ENV + ' build.'
),
'',
'You can access the compiled files in',
'',
' ' + chalk.yellow(paths.appBuild),
'',
'File sizes after gzip:',
'',
].join('\n')
);
printFileSizesAfterBuild(stats, previousFileSizes, paths.appBuild);
console.log();
console.log(
'To start a development server, run `' + chalk.cyan(cli + ' start') + '`.'
);
console.log(
'To create an optimized production build, run `' +
chalk.cyan(cli + ' build') +
'`.'
);
}

function printWarnings(messages) {
console.log(chalk.yellow('Compiled with warnings.'));
console.log();
messages.warnings.forEach(message => {
console.log(message);
console.log();
});
// Teach some ESLint tricks.
console.log('You may use special comments to disable some warnings.');
console.log(
'Use ' +
chalk.yellow('// eslint-disable-next-line') +
' to ignore the next line.'
);
console.log(
'Use ' +
chalk.yellow('/* eslint-disable */') +
' to ignore all warnings in a file.'
);
}

function clearConsoleIfInteractive() {
if (isInteractive) {
clearConsole();
}
}

function printWaitingChanges() {
console.log();
console.log(
'Waiting for changes in',
paths.appSrc.replace(process.cwd(), '.') + '/'
);
}

function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}