From 8e15dc2072220828ce9a1c8a98be6610180b72a2 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Tue, 6 Dec 2016 01:37:34 +0100 Subject: [PATCH 01/31] Watch test files (so they retrigger webpack) This will not process them for now... --- .../react-dev-utils/WatchTestFilesPlugin.js | 39 +++++++++++++++++++ packages/react-dev-utils/package.json | 2 + .../config/webpack.config.dev.js | 10 ++++- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 packages/react-dev-utils/WatchTestFilesPlugin.js diff --git a/packages/react-dev-utils/WatchTestFilesPlugin.js b/packages/react-dev-utils/WatchTestFilesPlugin.js new file mode 100644 index 00000000000..039a66501e6 --- /dev/null +++ b/packages/react-dev-utils/WatchTestFilesPlugin.js @@ -0,0 +1,39 @@ +var glob = require('glob'); +var path = require('path'); + +function computeGlob(pattern, options) { + return new Promise((resolve, reject) => { + glob(pattern, options || {}, (err, matches) => { + if (err) { + return reject(err); + } + resolve(matches); + }); + }); +} + +function WatchTestFilesPlugin(testGlobs) { + this.testGlobs = testGlobs || []; +} + +WatchTestFilesPlugin.prototype.apply = function(compiler) { + compiler.plugin('emit', (compilation, callback) => { + console.log() + Promise.all(this.testGlobs.map(globPattern => + computeGlob(globPattern, { + cwd: compiler.options.context, + ignore: 'node_modules/**', + }) + )) + .then(globLists => [].concat.apply([], globLists)) + .then(testFiles => { + testFiles.forEach(testFile => { + compilation.fileDependencies.push(path.join(compiler.options.context, testFile)); + }); + }) + .then(callback) + .catch(console.error); + }); +}; + +module.exports = WatchTestFilesPlugin; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 5d8051a8830..9811f261f06 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -20,12 +20,14 @@ "openBrowser.js", "prompt.js", "WatchMissingNodeModulesPlugin.js", + "WatchTestFilesPlugin.js", "webpackHotDevClient.js" ], "dependencies": { "ansi-html": "0.0.5", "chalk": "1.1.3", "escape-string-regexp": "1.0.5", + "glob": "^7.1.1", "html-entities": "1.2.0", "opn": "4.0.2", "sockjs-client": "1.0.3", diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 96fd632b795..7677b8b5011 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -15,6 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin'); var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); +var WatchTestFilesPlugin = require('react-dev-utils/WatchTestFilesPlugin'); var getClientEnvironment = require('./env'); var paths = require('./paths'); @@ -226,7 +227,14 @@ module.exports = { // to restart the development server for Webpack to discover it. This plugin // makes the discovery automatic so you don't have to restart. // See https://github.com/facebookincubator/create-react-app/issues/186 - new WatchMissingNodeModulesPlugin(paths.appNodeModules) + new WatchMissingNodeModulesPlugin(paths.appNodeModules), + // Tests won't have any linting unless they go through webpack. + // This plugin makes webpack aware of them without emitting them. + // See https://github.com/facebookincubator/create-react-app/issues/1169 + new WatchTestFilesPlugin([ + '**/__tests__/**', + '**/*.test.js', + ]), ], // 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. From 19f86d155d465235077a1342f7cb3ffb4fecd89b Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Tue, 6 Dec 2016 02:23:54 +0100 Subject: [PATCH 02/31] Structure where to put upcoming module addition --- .../react-dev-utils/WatchTestFilesPlugin.js | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/react-dev-utils/WatchTestFilesPlugin.js b/packages/react-dev-utils/WatchTestFilesPlugin.js index 039a66501e6..9ce5c41d185 100644 --- a/packages/react-dev-utils/WatchTestFilesPlugin.js +++ b/packages/react-dev-utils/WatchTestFilesPlugin.js @@ -12,27 +12,40 @@ function computeGlob(pattern, options) { }); } +function getGlobs(patterns, cwd) { + return Promise.all(patterns.map(globPattern => + computeGlob(globPattern, { + cwd: cwd, + ignore: 'node_modules/**', + }) + )) + .then(globLists => [].concat.apply([], globLists)) +} + function WatchTestFilesPlugin(testGlobs) { this.testGlobs = testGlobs || []; } WatchTestFilesPlugin.prototype.apply = function(compiler) { - compiler.plugin('emit', (compilation, callback) => { - console.log() - Promise.all(this.testGlobs.map(globPattern => - computeGlob(globPattern, { - cwd: compiler.options.context, - ignore: 'node_modules/**', - }) - )) - .then(globLists => [].concat.apply([], globLists)) - .then(testFiles => { - testFiles.forEach(testFile => { - compilation.fileDependencies.push(path.join(compiler.options.context, testFile)); - }); + var testFiles = []; + compiler.plugin('make', (compilation, callback) => { + getGlobs(this.testGlobs, compiler.options.context) + .then(foundFiles => { + testFiles = foundFiles; + return Promise.all( + testFiles.map(filename => new Promise((resolve, reject) => { + // TODO: add to modules + resolve(filename); + })) + ) }) - .then(callback) - .catch(console.error); + .then(callback); + }); + compiler.plugin('emit', (compilation, callback) => { + testFiles.forEach(testFile => { + compilation.fileDependencies.push(path.join(compiler.options.context, testFile)); + }); + callback(); }); }; From e0d373b213faaa3961e7c29d6e430caeca8339b4 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Thu, 8 Dec 2016 18:08:50 +0100 Subject: [PATCH 03/31] Create some sub compilations for tests This result will not be used for serving the pages but will be used for tracking lint/type errors in those files --- .../react-dev-utils/WatchTestFilesPlugin.js | 60 +++++++++++++------ packages/react-dev-utils/package.json | 6 ++ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/packages/react-dev-utils/WatchTestFilesPlugin.js b/packages/react-dev-utils/WatchTestFilesPlugin.js index 9ce5c41d185..b7b25fd4713 100644 --- a/packages/react-dev-utils/WatchTestFilesPlugin.js +++ b/packages/react-dev-utils/WatchTestFilesPlugin.js @@ -1,5 +1,7 @@ var glob = require('glob'); var path = require('path'); +var os = require('os'); +var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); function computeGlob(pattern, options) { return new Promise((resolve, reject) => { @@ -26,26 +28,48 @@ function WatchTestFilesPlugin(testGlobs) { this.testGlobs = testGlobs || []; } -WatchTestFilesPlugin.prototype.apply = function(compiler) { - var testFiles = []; - compiler.plugin('make', (compilation, callback) => { - getGlobs(this.testGlobs, compiler.options.context) - .then(foundFiles => { - testFiles = foundFiles; - return Promise.all( - testFiles.map(filename => new Promise((resolve, reject) => { - // TODO: add to modules - resolve(filename); - })) - ) - }) - .then(callback); +function compileTestFile(compiler, compilation, context, testFile) { + var outputOptions = { + filename: path.join(os.tmpdir(), '__compiledTests__', testFile), + publicPath: compilation.outputOptions.publicPath, + }; + var compilerName = "WatchTestFiles compilation for " + testFile; + var childCompiler = compilation.createChildCompiler(compilerName, outputOptions); + childCompiler.context = context; + childCompiler.apply( + new SingleEntryPlugin(context, path.join(compiler.options.context, testFile)) + ); + return new Promise((resolve, reject) => { + childCompiler.runAsChild((err, entries, childCompilation) => { + if (err) { + return reject(err); + } + resolve({ + errors: childCompilation.errors, + warnings: childCompilation.warnings, + }); + }); }); +} + +WatchTestFilesPlugin.prototype.apply = function(compiler) { compiler.plugin('emit', (compilation, callback) => { - testFiles.forEach(testFile => { - compilation.fileDependencies.push(path.join(compiler.options.context, testFile)); - }); - callback(); + getGlobs(this.testGlobs, compiler.options.context) + .then(foundFiles => Promise.all( + foundFiles.map(filename => { + // Add them to the list of watched files (for auto-reloading) + compilation.fileDependencies.push(path.join(compiler.options.context, filename)); + // Create and run a sub-compiler for the file to send it through the loaders + return compileTestFile(compiler, compilation, compiler.context, filename) + }) + )) + .then((results) => { + var errors = results.reduce((list, res) => list.concat(res.errors || []), []); + var warnings = results.reduce((list, res) => list.concat(res.warnings || []), []); + compilation.errors = compilation.errors.concat(errors); + compilation.warnings = compilation.warnings.concat(warnings); + callback(); + }, callback); }); }; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 9811f261f06..fce93d7f170 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -32,5 +32,11 @@ "opn": "4.0.2", "sockjs-client": "1.0.3", "strip-ansi": "3.0.1" + }, + "devDependencies": { + "webpack": "1.14.0" + }, + "peerDependencies": { + "webpack": "1.14.0" } } From e332fe5101e8dff0de7e4385b034ada8fd7d1afd Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 19:23:23 +0100 Subject: [PATCH 04/31] Setup a plugin to run a flow checking lifecycle This will only run if a @flow annotation is seen: - At the first @flow file found: - Write a .flowconfig if none exists - [Not done yet] Run flow-typed install - [Not done yet] Start a flow instance - When a file with an @flow comment changes during a compilation: - [Not done yet] Run a flow check - If there are some errors in flow: - If CI: output as an error - If not CI: output as a warning --- .../react-dev-utils/FlowTypecheckPlugin.js | 83 +++++++++++++++++++ packages/react-dev-utils/package.json | 1 + .../config/webpack.config.dev.js | 13 ++- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/react-dev-utils/FlowTypecheckPlugin.js diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js new file mode 100644 index 00000000000..ff4d8bd3500 --- /dev/null +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -0,0 +1,83 @@ +var fs = require('fs'); +var path = require('path'); + +function FlowTypecheckPlugin(options) { + this.options = options || {}; + // If flow is globally present in the project, this will stay across compilations + this._flowInitialized = false; + // If flow should run in a current compilation + this._flowShouldRun = false; + // Stores the last flow output + this._flowOutput = ""; +} + +FlowTypecheckPlugin.prototype.apply = function(compiler) { + compiler.plugin('compilation', function(compilation, params) { + // Detect the presence of flow and initialize it + compilation.plugin('normal-module-loader', function(loaderContext, module) { + // We're only checking the presence of flow in non-node_modules + // (some dependencies may keep their flow comments, we don't want to match them) + if (module.resource.indexOf("node_modules") < 0) { + // We use webpack's cached FileSystem to avoid slowing down compilation + loaderContext.fs.readFile(module.resource, function(err, data) { + if (data && data.toString().indexOf('@flow') >= 0) { + if (!this._flowInitialized) { + this._initializeFlow(compiler.options.context); + this._flowInitialized = true; + } + this._flowShouldRun = true; + } + }.bind(this)); + } + }.bind(this)) + }.bind(this)); + + // While emitting run a flow check if flow has been detected + compiler.plugin('emit', function(compilation, callback) { + // Only if a file with @ flow has been changed + if (this._flowShouldRun) { + this._flowOutput = this._flowCheck(); + this._flowShouldRun = false; + } + if (this._flowOutput.length > 0) { + // In a CI, we wish to get flow breaking the build so we write errors here + if (process.env.CI) { + compilation.errors.push(this._flowOutput); + } else { + compilation.warnings.push(this._flowOutput); + } + } + callback(); + }.bind(this)); +}; + +// This initializer will run once per webpack run (runs once across all compilations) +FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath) { + const flowconfigPath = path.join(projectPath, '.flowconfig'); + fs.exists(flowconfigPath, function(exists) { + if (!exists) { + fs.writeFile(flowconfigPath, (this.options.flowconfig || []).join('\n')); + } + }.bind(this)); + // TODO: run flow-typed + // TODO: start a flow instance +}; + +// This check will run each time a compilation sees a file with @ flow change +FlowTypecheckPlugin.prototype._flowCheck = function() { + // TODO: run a single flow check + return ` +src/App.js:11 + 11: f("abc"); + ^^^^^^^^ function call + 11: f("abc"); + ^^^^^ string. This type is incompatible with + 7: function f(x: number) { + ^^^^^^ number + + +Found 1 error + `; +}; + +module.exports = FlowTypecheckPlugin; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 5d8051a8830..0befc5d4dc4 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -12,6 +12,7 @@ }, "files": [ "clearConsole.js", + "FlowTypecheckPlugin.js", "checkRequiredFiles.js", "formatWebpackMessages.js", "getProcessForPort.js", diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 96fd632b795..97fdb25913d 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -15,6 +15,7 @@ var HtmlWebpackPlugin = require('html-webpack-plugin'); var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); +var FlowTypecheckPlugin = require('react-dev-utils/FlowTypecheckPlugin'); var getClientEnvironment = require('./env'); var paths = require('./paths'); @@ -226,7 +227,17 @@ module.exports = { // to restart the development server for Webpack to discover it. This plugin // makes the discovery automatic so you don't have to restart. // See https://github.com/facebookincubator/create-react-app/issues/186 - new WatchMissingNodeModulesPlugin(paths.appNodeModules) + new WatchMissingNodeModulesPlugin(paths.appNodeModules), + // Trigger some typechecking if a file matches with an @ flow comment + new FlowTypecheckPlugin({ + flowconfig: [ + '[ignore]', + '/node_modules/fbjs/.*', + '', + '[libs]', + './flow-typed' + ] + }) ], // 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. From eddd11b6ba03803b579ab1901db2cb61dffff3a7 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 19:35:16 +0100 Subject: [PATCH 05/31] Make flow fail on CI builds We don't need complex logic with process.env on the plugin side, we let the build script figure this out! --- packages/react-dev-utils/FlowTypecheckPlugin.js | 8 ++------ packages/react-scripts/config/webpack.config.prod.js | 5 ++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index ff4d8bd3500..e815d7b14ed 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -39,13 +39,9 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { this._flowOutput = this._flowCheck(); this._flowShouldRun = false; } + // Output a warning if flow failed if (this._flowOutput.length > 0) { - // In a CI, we wish to get flow breaking the build so we write errors here - if (process.env.CI) { - compilation.errors.push(this._flowOutput); - } else { - compilation.warnings.push(this._flowOutput); - } + compilation.warnings.push(this._flowOutput); } callback(); }.bind(this)); diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 71509658eb7..bf98cfac6a7 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -16,6 +16,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin'); var ManifestPlugin = require('webpack-manifest-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); +var FlowTypecheckPlugin = require('react-dev-utils/FlowTypecheckPlugin'); var url = require('url'); var paths = require('./paths'); var getClientEnvironment = require('./env'); @@ -277,7 +278,9 @@ module.exports = { // Generate and inject subresources hashes in the final `index.html`. new SubresourceIntegrityPlugin({ hashFuncNames: ['sha256', 'sha384'] - }) + }), + // Run Flow only if we see some @ flow annotations, will error on CI + new FlowTypecheckPlugin({}) ], // 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. From 097d7d97c74e0228023fe10c0d1e479a3032e566 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 22:05:36 +0100 Subject: [PATCH 06/31] Run flow as a server and check with status --- .../react-dev-utils/FlowTypecheckPlugin.js | 80 ++++++++++++++----- packages/react-dev-utils/package.json | 1 + .../config/webpack.config.dev.js | 3 +- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index e815d7b14ed..6dc631aa7e9 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -1,5 +1,7 @@ var fs = require('fs'); var path = require('path'); +var flowBinPath = require('flow-bin'); +var childProcess = require('child_process'); function FlowTypecheckPlugin(options) { this.options = options || {}; @@ -9,6 +11,8 @@ function FlowTypecheckPlugin(options) { this._flowShouldRun = false; // Stores the last flow output this._flowOutput = ""; + // Flow server process + this._flowServer = null; } FlowTypecheckPlugin.prototype.apply = function(compiler) { @@ -36,14 +40,27 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { compiler.plugin('emit', function(compilation, callback) { // Only if a file with @ flow has been changed if (this._flowShouldRun) { - this._flowOutput = this._flowCheck(); this._flowShouldRun = false; + var version = this.options.flowVersion || 'latest'; + this._flowCheck(compiler.options.context, version, function(err, flowOutput) { + if (err) { + compilation.errors.push(err.message); + return callback(); + } + this._flowOutput = flowOutput; + // Output a warning if flow failed + if (this._flowOutput.indexOf('No errors!') < 0) { + compilation.warnings.push(this._flowOutput); + } + callback(); + }.bind(this)); + } else { + // Output a warning if flow failed in a previous run + if (this._flowOutput.length > 0 && this._flowOutput.indexOf('No errors!') < 0) { + compilation.warnings.push(this._flowOutput); + } + callback(); } - // Output a warning if flow failed - if (this._flowOutput.length > 0) { - compilation.warnings.push(this._flowOutput); - } - callback(); }.bind(this)); }; @@ -56,24 +73,45 @@ FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath) { } }.bind(this)); // TODO: run flow-typed - // TODO: start a flow instance + this._flowServer = childProcess.spawn( + flowBinPath, + ['server'], + { cwd: projectPath } + ); }; // This check will run each time a compilation sees a file with @ flow change -FlowTypecheckPlugin.prototype._flowCheck = function() { - // TODO: run a single flow check - return ` -src/App.js:11 - 11: f("abc"); - ^^^^^^^^ function call - 11: f("abc"); - ^^^^^ string. This type is incompatible with - 7: function f(x: number) { - ^^^^^^ number - - -Found 1 error - `; +FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb) { + var flowOutput = ""; + var flowErrOutput = ""; + var statusCheck = childProcess.spawn( + flowBinPath, + ['status', '--no-auto-start', '--color=always'], + { cwd: projectPath } + ); + statusCheck.stdout.on('data', function(chunk) { + flowOutput += chunk.toString(); + }); + statusCheck.stderr.on('data', function(chunk) { + flowErrOutput += chunk.toString(); + }); + statusCheck.on('close', function() { + if (flowErrOutput.length > 0) { + if (flowErrOutput.indexOf("There is no Flow server running") >= 0) { + return cb(new Error( + 'flow server: unexpectedly died.\n' + + 'This is likely due to a version mismatch between a global flow ' + + 'server (that you or your IDE may try to run) and react-script\'s ' + + 'flow server.\n' + + 'You should run: \n' + + 'npm install -g flow-bin@' + flowVersion + )); + } else if(flowErrOutput.indexOf("still initializing") < 0) { + return cb(new Error(flowErrOutput)); + } + } + cb(null, flowOutput); + }); }; module.exports = FlowTypecheckPlugin; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 0befc5d4dc4..18f5ced7c05 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -27,6 +27,7 @@ "ansi-html": "0.0.5", "chalk": "1.1.3", "escape-string-regexp": "1.0.5", + "flow-bin": "^0.36.0", "html-entities": "1.2.0", "opn": "4.0.2", "sockjs-client": "1.0.3", diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 97fdb25913d..20f46521887 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -236,7 +236,8 @@ module.exports = { '', '[libs]', './flow-typed' - ] + ], + flowVersion: "0.36.0", }) ], // Some libraries import Node modules but don't use them in the browser. From 8ce4c6f3ece18914c96331a09baaf859c329e1f9 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 22:20:25 +0100 Subject: [PATCH 07/31] Add flow ignore comment to the start hints --- packages/react-scripts/scripts/start.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js index 752bd8a08a8..c640b5b6f15 100644 --- a/packages/react-scripts/scripts/start.js +++ b/packages/react-scripts/scripts/start.js @@ -131,6 +131,7 @@ function setupCompiler(host, port, protocol) { 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.'); + console.log('Use ' + chalk.yellow('// $FlowFixMe') + ' to ignore flow-related warnings on the next line.'); } }); } From 79911f824892d75d09b00e6ae671e99d054b3f97 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 23:11:52 +0100 Subject: [PATCH 08/31] Add flow-typed resolution at start --- .../react-dev-utils/FlowTypecheckPlugin.js | 47 ++++++++++++++----- packages/react-dev-utils/package.json | 1 + .../config/webpack.config.dev.js | 3 ++ .../config/webpack.config.prod.js | 7 ++- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 6dc631aa7e9..d5042538449 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -1,7 +1,8 @@ var fs = require('fs'); var path = require('path'); -var flowBinPath = require('flow-bin'); var childProcess = require('child_process'); +var flowBinPath = require('flow-bin'); +const flowTypedPath = path.join(__dirname, 'node_modules', '.bin', 'flow-typed'); function FlowTypecheckPlugin(options) { this.options = options || {}; @@ -10,12 +11,14 @@ function FlowTypecheckPlugin(options) { // If flow should run in a current compilation this._flowShouldRun = false; // Stores the last flow output - this._flowOutput = ""; + this._flowOutput = ''; // Flow server process this._flowServer = null; + this._flowServerStderr = ''; } FlowTypecheckPlugin.prototype.apply = function(compiler) { + var version = this.options.flowVersion || 'latest'; compiler.plugin('compilation', function(compilation, params) { // Detect the presence of flow and initialize it compilation.plugin('normal-module-loader', function(loaderContext, module) { @@ -26,7 +29,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { loaderContext.fs.readFile(module.resource, function(err, data) { if (data && data.toString().indexOf('@flow') >= 0) { if (!this._flowInitialized) { - this._initializeFlow(compiler.options.context); + this._initializeFlow(compiler.options.context, version); this._flowInitialized = true; } this._flowShouldRun = true; @@ -41,7 +44,6 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // Only if a file with @ flow has been changed if (this._flowShouldRun) { this._flowShouldRun = false; - var version = this.options.flowVersion || 'latest'; this._flowCheck(compiler.options.context, version, function(err, flowOutput) { if (err) { compilation.errors.push(err.message); @@ -65,19 +67,40 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { }; // This initializer will run once per webpack run (runs once across all compilations) -FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath) { +FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersion) { const flowconfigPath = path.join(projectPath, '.flowconfig'); fs.exists(flowconfigPath, function(exists) { if (!exists) { fs.writeFile(flowconfigPath, (this.options.flowconfig || []).join('\n')); } }.bind(this)); - // TODO: run flow-typed - this._flowServer = childProcess.spawn( - flowBinPath, - ['server'], + childProcess.exec( + flowTypedPath + ' install --overwrite --flowVersion=' + flowVersion, { cwd: projectPath } ); + var otherTypeDefs = this.options.otherFlowTypedDefs; + Object.keys(otherTypeDefs || {}).forEach(function(packageName) { + childProcess.exec( + flowTypedPath + ' install ' + packageName + '@' + otherTypeDefs[packageName] + ' --overwrite --flowVersion=' + flowVersion, + { cwd: projectPath } + ); + }) + function spawnServer() { + this._flowServer = childProcess.spawn( + flowBinPath, + ['server'], + { cwd: projectPath } + ); + this._flowServer.stderr.on('data', function(chunk) { + this._flowServerStderr += chunk.toString(); + }.bind(this)); + this._flowServer.on('exit', function() { + if (this._flowServerStderr.indexOf('Lib files changed')) { + spawnServer(); + } + }.bind(this)); + }; + spawnServer.call(this); }; // This check will run each time a compilation sees a file with @ flow change @@ -99,7 +122,9 @@ FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb if (flowErrOutput.length > 0) { if (flowErrOutput.indexOf("There is no Flow server running") >= 0) { return cb(new Error( - 'flow server: unexpectedly died.\n' + + 'flow server: unexpectedly died.\n\n' + + this._flowServerStderr + + '\n' + 'This is likely due to a version mismatch between a global flow ' + 'server (that you or your IDE may try to run) and react-script\'s ' + 'flow server.\n' + @@ -111,7 +136,7 @@ FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb } } cb(null, flowOutput); - }); + }.bind(this)); }; module.exports = FlowTypecheckPlugin; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 18f5ced7c05..5036d70cd54 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -28,6 +28,7 @@ "chalk": "1.1.3", "escape-string-regexp": "1.0.5", "flow-bin": "^0.36.0", + "flow-typed": "^2.0.0", "html-entities": "1.2.0", "opn": "4.0.2", "sockjs-client": "1.0.3", diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 20f46521887..d53fd975433 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -238,6 +238,9 @@ module.exports = { './flow-typed' ], flowVersion: "0.36.0", + otherFlowTypedDefs: { + jest: "17.0.0" + } }) ], // Some libraries import Node modules but don't use them in the browser. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index bf98cfac6a7..b8ea5db40b8 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -280,7 +280,12 @@ module.exports = { hashFuncNames: ['sha256', 'sha384'] }), // Run Flow only if we see some @ flow annotations, will error on CI - new FlowTypecheckPlugin({}) + new FlowTypecheckPlugin({ + flowVersion: "0.36.0", + otherFlowTypedDefs: { + jest: "17.0.0" + } + }) ], // 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. From e4b915cd6c12167a010e833f2351b7400f7efbcc Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 23:12:36 +0100 Subject: [PATCH 09/31] Reset server stderr on restart --- packages/react-dev-utils/FlowTypecheckPlugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index d5042538449..5435607bd3e 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -96,6 +96,7 @@ FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersio }.bind(this)); this._flowServer.on('exit', function() { if (this._flowServerStderr.indexOf('Lib files changed')) { + this._flowServerStderr = ""; spawnServer(); } }.bind(this)); From d724efef22ceb97df1dce0140a4b925632606bab Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 23:35:05 +0100 Subject: [PATCH 10/31] Move options as explicit props of the plugin --- .../react-dev-utils/FlowTypecheckPlugin.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 5435607bd3e..fe5fe3db985 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -5,7 +5,15 @@ var flowBinPath = require('flow-bin'); const flowTypedPath = path.join(__dirname, 'node_modules', '.bin', 'flow-typed'); function FlowTypecheckPlugin(options) { - this.options = options || {}; + options = options || {}; + // The flow-bin version + this.flowVersion = options.flowVersion || 'latest'; + // Contents of the generated .flowconfig if it doesn't exist + this.flowconfig = options.flowconfig || []; + // Load up other flow-typed defs outside of the package.json (implicit packages behind react-scripts) + // Key is the package name, value is the version number + this.otherFlowTypedDefs = options.otherFlowTypedDefs || {}; + // If flow is globally present in the project, this will stay across compilations this._flowInitialized = false; // If flow should run in a current compilation @@ -18,7 +26,6 @@ function FlowTypecheckPlugin(options) { } FlowTypecheckPlugin.prototype.apply = function(compiler) { - var version = this.options.flowVersion || 'latest'; compiler.plugin('compilation', function(compilation, params) { // Detect the presence of flow and initialize it compilation.plugin('normal-module-loader', function(loaderContext, module) { @@ -29,7 +36,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { loaderContext.fs.readFile(module.resource, function(err, data) { if (data && data.toString().indexOf('@flow') >= 0) { if (!this._flowInitialized) { - this._initializeFlow(compiler.options.context, version); + this._initializeFlow(compiler.options.context, this.flowVersion); this._flowInitialized = true; } this._flowShouldRun = true; @@ -44,7 +51,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // Only if a file with @ flow has been changed if (this._flowShouldRun) { this._flowShouldRun = false; - this._flowCheck(compiler.options.context, version, function(err, flowOutput) { + this._flowCheck(compiler.options.context, this.flowVersion, function(err, flowOutput) { if (err) { compilation.errors.push(err.message); return callback(); @@ -71,17 +78,16 @@ FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersio const flowconfigPath = path.join(projectPath, '.flowconfig'); fs.exists(flowconfigPath, function(exists) { if (!exists) { - fs.writeFile(flowconfigPath, (this.options.flowconfig || []).join('\n')); + fs.writeFile(flowconfigPath, this.flowconfig.join('\n')); } }.bind(this)); childProcess.exec( flowTypedPath + ' install --overwrite --flowVersion=' + flowVersion, { cwd: projectPath } ); - var otherTypeDefs = this.options.otherFlowTypedDefs; - Object.keys(otherTypeDefs || {}).forEach(function(packageName) { + Object.keys(this.otherFlowTypedDefs).forEach(function(packageName) { childProcess.exec( - flowTypedPath + ' install ' + packageName + '@' + otherTypeDefs[packageName] + ' --overwrite --flowVersion=' + flowVersion, + flowTypedPath + ' install ' + packageName + '@' + this.otherFlowTypedDefs[packageName] + ' --overwrite --flowVersion=' + flowVersion, { cwd: projectPath } ); }) From a4e56321c8f70e4e0a2f001b394b16f29b3000a1 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Sun, 4 Dec 2016 23:40:34 +0100 Subject: [PATCH 11/31] Use arrow functions --- .../react-dev-utils/FlowTypecheckPlugin.js | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index fe5fe3db985..62f9110492c 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -26,14 +26,14 @@ function FlowTypecheckPlugin(options) { } FlowTypecheckPlugin.prototype.apply = function(compiler) { - compiler.plugin('compilation', function(compilation, params) { + compiler.plugin('compilation', (compilation, params) => { // Detect the presence of flow and initialize it - compilation.plugin('normal-module-loader', function(loaderContext, module) { + compilation.plugin('normal-module-loader', (loaderContext, module) => { // We're only checking the presence of flow in non-node_modules // (some dependencies may keep their flow comments, we don't want to match them) if (module.resource.indexOf("node_modules") < 0) { // We use webpack's cached FileSystem to avoid slowing down compilation - loaderContext.fs.readFile(module.resource, function(err, data) { + loaderContext.fs.readFile(module.resource, (err, data) => { if (data && data.toString().indexOf('@flow') >= 0) { if (!this._flowInitialized) { this._initializeFlow(compiler.options.context, this.flowVersion); @@ -41,17 +41,17 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { } this._flowShouldRun = true; } - }.bind(this)); + }); } - }.bind(this)) - }.bind(this)); + }) + }); // While emitting run a flow check if flow has been detected - compiler.plugin('emit', function(compilation, callback) { + compiler.plugin('emit', (compilation, callback) => { // Only if a file with @ flow has been changed if (this._flowShouldRun) { this._flowShouldRun = false; - this._flowCheck(compiler.options.context, this.flowVersion, function(err, flowOutput) { + this._flowCheck(compiler.options.context, this.flowVersion, (err, flowOutput) => { if (err) { compilation.errors.push(err.message); return callback(); @@ -62,7 +62,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { compilation.warnings.push(this._flowOutput); } callback(); - }.bind(this)); + }); } else { // Output a warning if flow failed in a previous run if (this._flowOutput.length > 0 && this._flowOutput.indexOf('No errors!') < 0) { @@ -70,22 +70,22 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { } callback(); } - }.bind(this)); + }); }; // This initializer will run once per webpack run (runs once across all compilations) FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersion) { const flowconfigPath = path.join(projectPath, '.flowconfig'); - fs.exists(flowconfigPath, function(exists) { + fs.exists(flowconfigPath, (exists) => { if (!exists) { fs.writeFile(flowconfigPath, this.flowconfig.join('\n')); } - }.bind(this)); + }); childProcess.exec( flowTypedPath + ' install --overwrite --flowVersion=' + flowVersion, { cwd: projectPath } ); - Object.keys(this.otherFlowTypedDefs).forEach(function(packageName) { + Object.keys(this.otherFlowTypedDefs).forEach((packageName) => { childProcess.exec( flowTypedPath + ' install ' + packageName + '@' + this.otherFlowTypedDefs[packageName] + ' --overwrite --flowVersion=' + flowVersion, { cwd: projectPath } @@ -97,15 +97,15 @@ FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersio ['server'], { cwd: projectPath } ); - this._flowServer.stderr.on('data', function(chunk) { + this._flowServer.stderr.on('data', (chunk) => { this._flowServerStderr += chunk.toString(); - }.bind(this)); - this._flowServer.on('exit', function() { + }); + this._flowServer.on('exit', () => { if (this._flowServerStderr.indexOf('Lib files changed')) { this._flowServerStderr = ""; spawnServer(); } - }.bind(this)); + }); }; spawnServer.call(this); }; @@ -119,13 +119,13 @@ FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb ['status', '--no-auto-start', '--color=always'], { cwd: projectPath } ); - statusCheck.stdout.on('data', function(chunk) { + statusCheck.stdout.on('data', (chunk) => { flowOutput += chunk.toString(); }); - statusCheck.stderr.on('data', function(chunk) { + statusCheck.stderr.on('data', (chunk) => { flowErrOutput += chunk.toString(); }); - statusCheck.on('close', function() { + statusCheck.on('close', () => { if (flowErrOutput.length > 0) { if (flowErrOutput.indexOf("There is no Flow server running") >= 0) { return cb(new Error( @@ -143,7 +143,7 @@ FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb } } cb(null, flowOutput); - }.bind(this)); + }); }; module.exports = FlowTypecheckPlugin; From 64f36ec78dd679e393514e1f896a438bcacaa35b Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 00:42:11 +0100 Subject: [PATCH 12/31] Refactor using promises and no class attrs --- .../react-dev-utils/FlowTypecheckPlugin.js | 196 +++++++++--------- 1 file changed, 97 insertions(+), 99 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 62f9110492c..fa6ee8ec06f 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -4,6 +4,81 @@ var childProcess = require('child_process'); var flowBinPath = require('flow-bin'); const flowTypedPath = path.join(__dirname, 'node_modules', '.bin', 'flow-typed'); +function execOneTime(command, args, options) { + return new Promise((resolve) => { + childProcess.exec( + command + ' ' + (args || []).join(' '), + (options || {}), + (error, stdout, stderr) => { + resolve({ + error: error, + stdout: stdout, + stderr: stderr + }); + } + ) + }); +} + +function writeFileIfDoesNotExist(path, data) { + return new Promise((resolve, reject) => { + fs.exists(path, exists => { + if (!exists) { + fs.writeFile(path, data, err => { + if (err) { + reject(err); + } + resolve(data); + }); + } + }); + }); +} + +function initializeFlow(projectPath, flowVersion, flowconfig, otherFlowTypedDefs) { + const flowconfigPath = path.join(projectPath, '.flowconfig'); + Promise.all( + [ + writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), + execOneTime( + flowTypedPath, + ['install', '--overwrite', '--flowVersion=' + flowVersion], + { cwd: projectPath } + ) + ].concat( + Object.keys(otherFlowTypedDefs).map((packageName) => execOneTime( + flowTypedPath, + [ + 'install', + packageName + '@' + otherFlowTypedDefs[packageName], + '--overwrite', + '--flowVersion=' + flowVersion + ], + { cwd: projectPath } + )) + ) + ); +} + +function flowCheck(projectPath, flowVersion) { + return execOneTime( + flowBinPath, + ['status', '--color=always'], + { cwd: projectPath } + ) + .then(res => { + var flowOutput = res.stdout; + var flowErrOutput = res.stderr; + if (flowErrOutput.length > 0) { + if(flowErrOutput.indexOf("still initializing") < 0) { + return Promise.reject(new Error('flow server:\n' + flowErrOutput)); + } + } + return flowOutput; + }); +} + + function FlowTypecheckPlugin(options) { options = options || {}; // The flow-bin version @@ -13,19 +88,12 @@ function FlowTypecheckPlugin(options) { // Load up other flow-typed defs outside of the package.json (implicit packages behind react-scripts) // Key is the package name, value is the version number this.otherFlowTypedDefs = options.otherFlowTypedDefs || {}; - - // If flow is globally present in the project, this will stay across compilations - this._flowInitialized = false; - // If flow should run in a current compilation - this._flowShouldRun = false; - // Stores the last flow output - this._flowOutput = ''; - // Flow server process - this._flowServer = null; - this._flowServerStderr = ''; } FlowTypecheckPlugin.prototype.apply = function(compiler) { + var flowInitialized = false; + var flowShouldRun = false; + var flowOutput = ''; compiler.plugin('compilation', (compilation, params) => { // Detect the presence of flow and initialize it compilation.plugin('normal-module-loader', (loaderContext, module) => { @@ -35,11 +103,14 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // We use webpack's cached FileSystem to avoid slowing down compilation loaderContext.fs.readFile(module.resource, (err, data) => { if (data && data.toString().indexOf('@flow') >= 0) { - if (!this._flowInitialized) { - this._initializeFlow(compiler.options.context, this.flowVersion); - this._flowInitialized = true; + if (!flowInitialized) { + initializeFlow( + compiler.options.context, this.flowVersion, + this.flowconfig, this.otherFlowTypedDefs + ); + flowInitialized = true; } - this._flowShouldRun = true; + flowShouldRun = true; } }); } @@ -49,101 +120,28 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // While emitting run a flow check if flow has been detected compiler.plugin('emit', (compilation, callback) => { // Only if a file with @ flow has been changed - if (this._flowShouldRun) { - this._flowShouldRun = false; - this._flowCheck(compiler.options.context, this.flowVersion, (err, flowOutput) => { - if (err) { - compilation.errors.push(err.message); - return callback(); - } - this._flowOutput = flowOutput; + if (flowShouldRun) { + flowShouldRun = false; + flowCheck(compiler.options.context, this.flowVersion) + .then(newOutput => { + flowOutput = newOutput; // Output a warning if flow failed - if (this._flowOutput.indexOf('No errors!') < 0) { - compilation.warnings.push(this._flowOutput); + if (flowOutput.indexOf('No errors!') < 0) { + compilation.warnings.push(flowOutput); } callback(); + }, (err) => { + compilation.errors.push(err.message); + callback(); }); } else { // Output a warning if flow failed in a previous run - if (this._flowOutput.length > 0 && this._flowOutput.indexOf('No errors!') < 0) { - compilation.warnings.push(this._flowOutput); + if (flowOutput.length > 0 && flowOutput.indexOf('No errors!') < 0) { + compilation.warnings.push(flowOutput); } callback(); } }); }; -// This initializer will run once per webpack run (runs once across all compilations) -FlowTypecheckPlugin.prototype._initializeFlow = function(projectPath, flowVersion) { - const flowconfigPath = path.join(projectPath, '.flowconfig'); - fs.exists(flowconfigPath, (exists) => { - if (!exists) { - fs.writeFile(flowconfigPath, this.flowconfig.join('\n')); - } - }); - childProcess.exec( - flowTypedPath + ' install --overwrite --flowVersion=' + flowVersion, - { cwd: projectPath } - ); - Object.keys(this.otherFlowTypedDefs).forEach((packageName) => { - childProcess.exec( - flowTypedPath + ' install ' + packageName + '@' + this.otherFlowTypedDefs[packageName] + ' --overwrite --flowVersion=' + flowVersion, - { cwd: projectPath } - ); - }) - function spawnServer() { - this._flowServer = childProcess.spawn( - flowBinPath, - ['server'], - { cwd: projectPath } - ); - this._flowServer.stderr.on('data', (chunk) => { - this._flowServerStderr += chunk.toString(); - }); - this._flowServer.on('exit', () => { - if (this._flowServerStderr.indexOf('Lib files changed')) { - this._flowServerStderr = ""; - spawnServer(); - } - }); - }; - spawnServer.call(this); -}; - -// This check will run each time a compilation sees a file with @ flow change -FlowTypecheckPlugin.prototype._flowCheck = function(projectPath, flowVersion, cb) { - var flowOutput = ""; - var flowErrOutput = ""; - var statusCheck = childProcess.spawn( - flowBinPath, - ['status', '--no-auto-start', '--color=always'], - { cwd: projectPath } - ); - statusCheck.stdout.on('data', (chunk) => { - flowOutput += chunk.toString(); - }); - statusCheck.stderr.on('data', (chunk) => { - flowErrOutput += chunk.toString(); - }); - statusCheck.on('close', () => { - if (flowErrOutput.length > 0) { - if (flowErrOutput.indexOf("There is no Flow server running") >= 0) { - return cb(new Error( - 'flow server: unexpectedly died.\n\n' + - this._flowServerStderr + - '\n' + - 'This is likely due to a version mismatch between a global flow ' + - 'server (that you or your IDE may try to run) and react-script\'s ' + - 'flow server.\n' + - 'You should run: \n' + - 'npm install -g flow-bin@' + flowVersion - )); - } else if(flowErrOutput.indexOf("still initializing") < 0) { - return cb(new Error(flowErrOutput)); - } - } - cb(null, flowOutput); - }); -}; - module.exports = FlowTypecheckPlugin; From bf66320e50dbac099d8f7ff1f8607a5df3f07730 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 01:47:54 +0100 Subject: [PATCH 13/31] Add documentation for out-of-the-box flow --- packages/react-scripts/template/README.md | 25 +++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index bdb88df6023..e3ef399bfc5 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -444,28 +444,23 @@ Now you are ready to use the imported React Bootstrap components within your com ## Adding Flow -Flow typing is currently [not supported out of the box](https://github.com/facebookincubator/create-react-app/issues/72) with the default `.flowconfig` generated by Flow. If you run it, you might get errors like this: +Flow typing is now supported out of the box. All you have to do is add the `/* @flow */` comment on top of files you +want to typecheck. If no `.flowconfig` is present, one will be generated for you. The script will also download type +definitions from [flow-typed](https://github.com/flowtype/flow-typed) automatically. -```js -node_modules/fbjs/lib/Deferred.js.flow:60 - 60: Promise.prototype.done.apply(this._promise, arguments); - ^^^^ property `done`. Property not found in -495: declare class Promise<+R> { - ^ Promise. See lib: /private/tmp/flow/flowlib_34952d31/core.js:495 - -node_modules/fbjs/lib/shallowEqual.js.flow:29 - 29: return x !== 0 || 1 / (x: $FlowIssue) === 1 / (y: $FlowIssue); - ^^^^^^^^^^ identifier `$FlowIssue`. Could not resolve name -``` - -To fix this, change your `.flowconfig` to look like this: +If you don't have the auto-generated `.flowconfig` because you had flow setup before, you can copy it from here: ```ini [ignore] /node_modules/fbjs/.* + +[libs] +./flow-typed ``` -Re-run flow, and you shouldn’t get any extra issues. +Flow errors will show up alongside eslint errors as you work on your application. + +>Note: If your global flow installation version differs from react-scripts's flow version, you may experience slower compilation times while going back and forth between your app and your IDE (that may use your global flow). Ensure you have the same `flow-bin` version [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-dev-utils/package.json). ## Adding Custom Environment Variables From 7e65c5d3e8abec1cdfd5768275c8d8f770b24ea2 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 16:26:12 +0100 Subject: [PATCH 14/31] Schedule project init to complete before 1st check --- .../react-dev-utils/FlowTypecheckPlugin.js | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index fa6ee8ec06f..c246d782919 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -30,6 +30,8 @@ function writeFileIfDoesNotExist(path, data) { } resolve(data); }); + } else { + resolve(data); } }); }); @@ -37,7 +39,7 @@ function writeFileIfDoesNotExist(path, data) { function initializeFlow(projectPath, flowVersion, flowconfig, otherFlowTypedDefs) { const flowconfigPath = path.join(projectPath, '.flowconfig'); - Promise.all( + return Promise.all( [ writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), execOneTime( @@ -91,7 +93,9 @@ function FlowTypecheckPlugin(options) { } FlowTypecheckPlugin.prototype.apply = function(compiler) { + var flowActiveOnProject = false; var flowInitialized = false; + var flowInitializationPromise; var flowShouldRun = false; var flowOutput = ''; compiler.plugin('compilation', (compilation, params) => { @@ -103,12 +107,20 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // We use webpack's cached FileSystem to avoid slowing down compilation loaderContext.fs.readFile(module.resource, (err, data) => { if (data && data.toString().indexOf('@flow') >= 0) { - if (!flowInitialized) { - initializeFlow( + if (!flowActiveOnProject) { + flowInitializationPromise = initializeFlow( compiler.options.context, this.flowVersion, this.flowconfig, this.otherFlowTypedDefs - ); - flowInitialized = true; + ) + .then(() => { + flowInitialized = true; + }, (e) => { + loaderContext.emitWarning(new Error( + 'Flow project initialization warning:\n' + + e.message + )); + }); + flowActiveOnProject = true; } flowShouldRun = true; } @@ -122,7 +134,10 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { // Only if a file with @ flow has been changed if (flowShouldRun) { flowShouldRun = false; - flowCheck(compiler.options.context, this.flowVersion) + (flowInitialized ? + Promise.resolve() : + flowInitializationPromise) + .then(() => flowCheck(compiler.options.context, this.flowVersion)) .then(newOutput => { flowOutput = newOutput; // Output a warning if flow failed From c001833c4ab8a16d8b7f54fda95e85d379700ace Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 16:59:17 +0100 Subject: [PATCH 15/31] Process code-based flow checks --- .../react-dev-utils/FlowTypecheckPlugin.js | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index c246d782919..23211f19e11 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -4,19 +4,44 @@ var childProcess = require('child_process'); var flowBinPath = require('flow-bin'); const flowTypedPath = path.join(__dirname, 'node_modules', '.bin', 'flow-typed'); +function stripFlowLoadingIndicators(message) { + var newMessage = message; + var launchingIndex = newMessage.indexOf("Launching Flow server for"); + if (launchingIndex >= 0) { + newMessage = newMessage.slice(0, launchingIndex); + } + var stillIndex = newMessage.indexOf("flow is still initializing"); + if (stillIndex >= 0) { + newMessage = newMessage.slice(0, stillIndex); + } + return newMessage; +} + function execOneTime(command, args, options) { - return new Promise((resolve) => { - childProcess.exec( - command + ' ' + (args || []).join(' '), - (options || {}), - (error, stdout, stderr) => { - resolve({ - error: error, - stdout: stdout, - stderr: stderr - }); + return new Promise((resolve, reject) => { + var stdout = new Buffer(""); + var stderr = new Buffer(""); + var oneTimeProcess = childProcess.spawn( + command, + args, + options + ); + oneTimeProcess.stdout.on('data', chunk => { + stdout = Buffer.concat([stdout, chunk]); + }); + oneTimeProcess.stderr.on('data', chunk => { + stderr = Buffer.concat([stderr, chunk]); + }); + oneTimeProcess.on('exit', code => { + switch (code) { + case 0: + return resolve(stdout); + default: + return reject(new Error( + Buffer.concat([stdout, stderr]).toString() + )); } - ) + }); }); } @@ -67,17 +92,7 @@ function flowCheck(projectPath, flowVersion) { flowBinPath, ['status', '--color=always'], { cwd: projectPath } - ) - .then(res => { - var flowOutput = res.stdout; - var flowErrOutput = res.stderr; - if (flowErrOutput.length > 0) { - if(flowErrOutput.indexOf("still initializing") < 0) { - return Promise.reject(new Error('flow server:\n' + flowErrOutput)); - } - } - return flowOutput; - }); + ); } @@ -97,9 +112,10 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { var flowInitialized = false; var flowInitializationPromise; var flowShouldRun = false; - var flowOutput = ''; + var flowErrorOutput = null; + + // During module traversal, assert the presence of an @ flow in a module compiler.plugin('compilation', (compilation, params) => { - // Detect the presence of flow and initialize it compilation.plugin('normal-module-loader', (loaderContext, module) => { // We're only checking the presence of flow in non-node_modules // (some dependencies may keep their flow comments, we don't want to match them) @@ -129,7 +145,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { }) }); - // While emitting run a flow check if flow has been detected + // While emitting, run a flow check if flow has been detected compiler.plugin('emit', (compilation, callback) => { // Only if a file with @ flow has been changed if (flowShouldRun) { @@ -138,21 +154,17 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { Promise.resolve() : flowInitializationPromise) .then(() => flowCheck(compiler.options.context, this.flowVersion)) - .then(newOutput => { - flowOutput = newOutput; - // Output a warning if flow failed - if (flowOutput.indexOf('No errors!') < 0) { - compilation.warnings.push(flowOutput); - } - callback(); - }, (err) => { - compilation.errors.push(err.message); - callback(); - }); + .then(() => { + flowErrorOutput = null; + }, error => { + flowErrorOutput = stripFlowLoadingIndicators(error.message); + compilation.warnings.push(flowErrorOutput); + }) + .then(callback); } else { // Output a warning if flow failed in a previous run - if (flowOutput.length > 0 && flowOutput.indexOf('No errors!') < 0) { - compilation.warnings.push(flowOutput); + if (flowErrorOutput) { + compilation.warnings.push(flowErrorOutput); } callback(); } From aa76ae5a86dd3fbe44533552c0a0c5e8fb4e004c Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 17:21:39 +0100 Subject: [PATCH 16/31] Get flow version internally without config --- .../react-dev-utils/FlowTypecheckPlugin.js | 75 ++++++++++++------- .../config/webpack.config.dev.js | 1 - .../config/webpack.config.prod.js | 1 - 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 23211f19e11..d28fe5d6dcc 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -62,32 +62,57 @@ function writeFileIfDoesNotExist(path, data) { }); } -function initializeFlow(projectPath, flowVersion, flowconfig, otherFlowTypedDefs) { - const flowconfigPath = path.join(projectPath, '.flowconfig'); - return Promise.all( - [ - writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), +function getFlowPath(globalInstall) { + return ( + globalInstall ? execOneTime( - flowTypedPath, - ['install', '--overwrite', '--flowVersion=' + flowVersion], - { cwd: projectPath } + '/bin/sh', + ['-c', 'which flow'] ) - ].concat( - Object.keys(otherFlowTypedDefs).map((packageName) => execOneTime( - flowTypedPath, - [ - 'install', - packageName + '@' + otherFlowTypedDefs[packageName], - '--overwrite', - '--flowVersion=' + flowVersion - ], - { cwd: projectPath } - )) + .then(rawPath => { + var path = rawPath.toString().replace('\n', ''); + return path.indexOf("not found") >= 0 ? + flowBinPath : + path + }) : + Promise.resolve(flowBinPath) + ) +} + +function getFlowVersion(options) { + return getFlowPath((options || {}).globalInstall) + .then(flowPath => execOneTime( + flowPath, + ['version', '--json'] + )) + .then(rawData => JSON.parse(rawData)) + .then(versionData => versionData.semver); +} + +function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { + const flowconfigPath = path.join(projectPath, '.flowconfig'); + return getFlowVersion().then(localVersion => Promise.all([ + writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), + execOneTime( + flowTypedPath, + ['install', '--overwrite', '--flowVersion=' + localVersion], + { cwd: projectPath } ) - ); + ].concat( + Object.keys(otherFlowTypedDefs).map((packageName) => execOneTime( + flowTypedPath, + [ + 'install', + packageName + '@' + otherFlowTypedDefs[packageName], + '--overwrite', + '--flowVersion=' + localVersion + ], + { cwd: projectPath } + )) + ))); } -function flowCheck(projectPath, flowVersion) { +function flowCheck(projectPath) { return execOneTime( flowBinPath, ['status', '--color=always'], @@ -95,11 +120,8 @@ function flowCheck(projectPath, flowVersion) { ); } - function FlowTypecheckPlugin(options) { options = options || {}; - // The flow-bin version - this.flowVersion = options.flowVersion || 'latest'; // Contents of the generated .flowconfig if it doesn't exist this.flowconfig = options.flowconfig || []; // Load up other flow-typed defs outside of the package.json (implicit packages behind react-scripts) @@ -125,8 +147,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { if (data && data.toString().indexOf('@flow') >= 0) { if (!flowActiveOnProject) { flowInitializationPromise = initializeFlow( - compiler.options.context, this.flowVersion, - this.flowconfig, this.otherFlowTypedDefs + compiler.options.context, this.flowconfig, this.otherFlowTypedDefs ) .then(() => { flowInitialized = true; @@ -153,7 +174,7 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { (flowInitialized ? Promise.resolve() : flowInitializationPromise) - .then(() => flowCheck(compiler.options.context, this.flowVersion)) + .then(() => flowCheck(compiler.options.context)) .then(() => { flowErrorOutput = null; }, error => { diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index d53fd975433..db8e51f1027 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -237,7 +237,6 @@ module.exports = { '[libs]', './flow-typed' ], - flowVersion: "0.36.0", otherFlowTypedDefs: { jest: "17.0.0" } diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index b8ea5db40b8..3f3a2d5aaa2 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -281,7 +281,6 @@ module.exports = { }), // Run Flow only if we see some @ flow annotations, will error on CI new FlowTypecheckPlugin({ - flowVersion: "0.36.0", otherFlowTypedDefs: { jest: "17.0.0" } From 3fc87cabfe3b374803985c196e8ff90f385eafc1 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 17:52:53 +0100 Subject: [PATCH 17/31] Check global flow version If a mismatch occurs, the DX will be bad, we should tell it to the user --- .../react-dev-utils/FlowTypecheckPlugin.js | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index d28fe5d6dcc..449ba717eab 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -14,6 +14,10 @@ function stripFlowLoadingIndicators(message) { if (stillIndex >= 0) { newMessage = newMessage.slice(0, stillIndex); } + var notRespIndex = newMessage.indexOf("The flow server is not responding"); + if (notRespIndex >= 0) { + newMessage = newMessage.slice(0, notRespIndex); + } return newMessage; } @@ -32,6 +36,7 @@ function execOneTime(command, args, options) { oneTimeProcess.stderr.on('data', chunk => { stderr = Buffer.concat([stderr, chunk]); }); + oneTimeProcess.on('error', error => reject(error)); oneTimeProcess.on('exit', code => { switch (code) { case 0: @@ -62,29 +67,11 @@ function writeFileIfDoesNotExist(path, data) { }); } -function getFlowPath(globalInstall) { - return ( - globalInstall ? - execOneTime( - '/bin/sh', - ['-c', 'which flow'] - ) - .then(rawPath => { - var path = rawPath.toString().replace('\n', ''); - return path.indexOf("not found") >= 0 ? - flowBinPath : - path - }) : - Promise.resolve(flowBinPath) - ) -} - function getFlowVersion(options) { - return getFlowPath((options || {}).globalInstall) - .then(flowPath => execOneTime( - flowPath, + return execOneTime( + (options || {}).global ? "flow" : flowBinPath, ['version', '--json'] - )) + ) .then(rawData => JSON.parse(rawData)) .then(versionData => versionData.semver); } @@ -92,6 +79,21 @@ function getFlowVersion(options) { function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { const flowconfigPath = path.join(projectPath, '.flowconfig'); return getFlowVersion().then(localVersion => Promise.all([ + getFlowVersion({global: true}).catch(() => localVersion) + .then(globalVersion => + globalVersion !== localVersion ? + Promise.reject(new Error( + 'Your global flow version does not match react-script\'s flow version.\n\n' + + 'Having two different versions is likely to restart your flow server ' + + 'regularly while you go back and forth between your App and your IDE ' + + '(that is likely to rely on your global flow installation). ' + + 'This will slow down significantly your development experience. ' + + 'In order to avoid this, ensure you have flow ' + localVersion + ' globally ' + + '(your current global version is ' + globalVersion + ').\n\n' + + 'Run `npm install -g flow-bin@' + localVersion + '` to fix this.' + )) : + true + ), writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), execOneTime( flowTypedPath, From 251bd9bcdd1219cc14423d836b8e693ef468f2b9 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 18:05:44 +0100 Subject: [PATCH 18/31] Add flow-typed to gitignore automatically --- .../react-dev-utils/FlowTypecheckPlugin.js | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 449ba717eab..8bbd67b34b2 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -56,7 +56,7 @@ function writeFileIfDoesNotExist(path, data) { if (!exists) { fs.writeFile(path, data, err => { if (err) { - reject(err); + return reject(err); } resolve(data); }); @@ -67,6 +67,37 @@ function writeFileIfDoesNotExist(path, data) { }); } +function writeInFileIfNotPresent(path, contentToAssert, contentToAppend) { + return new Promise((resolve, reject) => { + fs.exists(path, exists => { + if (!exists) { + fs.writeFile(path, contentToAppend, err => { + if (err) { + return reject(err); + } + resolve(contentToAppend); + }); + } else { + fs.readFile(path, (err, existingContent) => { + if (err) { + return reject(err); + } + if (existingContent.indexOf(contentToAssert) < 0) { + fs.appendFile(path, contentToAppend, err => { + if (err) { + return reject(err); + } + resolve(contentToAppend); + }); + } else { + resolve(contentToAppend); + } + }); + } + }); + }); +} + function getFlowVersion(options) { return execOneTime( (options || {}).global ? "flow" : flowBinPath, @@ -78,6 +109,7 @@ function getFlowVersion(options) { function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { const flowconfigPath = path.join(projectPath, '.flowconfig'); + const gitignorePath = path.join(projectPath, '.gitignore'); return getFlowVersion().then(localVersion => Promise.all([ getFlowVersion({global: true}).catch(() => localVersion) .then(globalVersion => @@ -95,6 +127,7 @@ function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { true ), writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), + writeInFileIfNotPresent(gitignorePath, 'flow-typed', 'flow-typed'), execOneTime( flowTypedPath, ['install', '--overwrite', '--flowVersion=' + localVersion], From 4ed28fd74c07baec8338e4ffd29126bb6ce3285b Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 18:56:27 +0100 Subject: [PATCH 19/31] Add optional flow to end to end testing --- tasks/e2e.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tasks/e2e.sh b/tasks/e2e.sh index ce7b50f35b5..7a47d99fef8 100755 --- a/tasks/e2e.sh +++ b/tasks/e2e.sh @@ -143,6 +143,7 @@ test -e build/static/js/*.js test -e build/static/css/*.css test -e build/static/media/*.svg test -e build/favicon.ico +test ! -e .flowconfig # Run tests with CI flag CI=true npm test @@ -152,6 +153,23 @@ CI=true npm test # Test the server npm start -- --smoke-test +# Test optional flow enabling +cp src/App.js src/App.backup.js +cp .gitignore .gitignore.backup +echo " +/* @flow */ +var wrong: string = 0; +" >> src/App.js +npm start -- --smoke-test || true +test -e .flowconfig +test -d flow-typed +cat .gitignore | grep flow-typed +rm src/App.js .gitignore +cp src/App.backup.js src/App.js +cp .gitignore.backup .gitignore +rm src/App.backup.js .gitignore.backup .flowconfig +rm -rf flow-typed + # ****************************************************************************** # Finally, let's check that everything still works after ejecting. # ****************************************************************************** @@ -173,6 +191,7 @@ test -e build/static/js/*.js test -e build/static/css/*.css test -e build/static/media/*.svg test -e build/favicon.ico +test ! -e .flowconfig # Run tests, overring the watch option to disable it. # `CI=true npm test` won't work here because `npm test` becomes just `jest`. @@ -185,6 +204,22 @@ npm test -- --watch=no # Test the server npm start -- --smoke-test +# Test optional flow enabling +cp src/App.js src/App.backup.js +cp .gitignore .gitignore.backup +echo " +/* @flow */ +var wrong: string = 0; +" >> src/App.js +npm start -- --smoke-test || true +test -e .flowconfig +test -d flow-typed +cat .gitignore | grep flow-typed +rm src/App.js .gitignore +cp src/App.backup.js src/App.js +cp .gitignore.backup .gitignore +rm src/App.backup.js .gitignore.backup .flowconfig +rm -rf flow-typed # ****************************************************************************** # Test --scripts-version with a version number From 911bf6d11b1c231ecbfc28dfb5e4364c159e5722 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 19:04:48 +0100 Subject: [PATCH 20/31] Run flow even if init failed --- packages/react-dev-utils/FlowTypecheckPlugin.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index 8bbd67b34b2..ff324069a90 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -184,13 +184,14 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { flowInitializationPromise = initializeFlow( compiler.options.context, this.flowconfig, this.otherFlowTypedDefs ) - .then(() => { - flowInitialized = true; - }, (e) => { + .catch(e => { loaderContext.emitWarning(new Error( 'Flow project initialization warning:\n' + e.message )); + }) + .then(() => { + flowInitialized = true; }); flowActiveOnProject = true; } From 3b06bf1ce8a87841883919896f8cb08c865d4a7c Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 21:38:02 +0100 Subject: [PATCH 21/31] Remove fbjs ignores now that it's fixed --- packages/react-scripts/config/webpack.config.dev.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index db8e51f1027..fef412cf5bd 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -231,9 +231,6 @@ module.exports = { // Trigger some typechecking if a file matches with an @ flow comment new FlowTypecheckPlugin({ flowconfig: [ - '[ignore]', - '/node_modules/fbjs/.*', - '', '[libs]', './flow-typed' ], From 52e207afd8545009ab08ff5396b009f4c0980932 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 22:38:20 +0100 Subject: [PATCH 22/31] Remove flow-typed lib import (known by default!) --- packages/react-scripts/config/webpack.config.dev.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index fef412cf5bd..c276a60be70 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -230,10 +230,6 @@ module.exports = { new WatchMissingNodeModulesPlugin(paths.appNodeModules), // Trigger some typechecking if a file matches with an @ flow comment new FlowTypecheckPlugin({ - flowconfig: [ - '[libs]', - './flow-typed' - ], otherFlowTypedDefs: { jest: "17.0.0" } From c0188d900b5c4b33dd6ab6f0c39195eba83e7be3 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 22:39:45 +0100 Subject: [PATCH 23/31] Remove info about the .flowconfig in the docs It's now useless --- packages/react-scripts/template/README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/react-scripts/template/README.md b/packages/react-scripts/template/README.md index e3ef399bfc5..6ca7c15be4f 100644 --- a/packages/react-scripts/template/README.md +++ b/packages/react-scripts/template/README.md @@ -448,16 +448,6 @@ Flow typing is now supported out of the box. All you have to do is add the `/* @ want to typecheck. If no `.flowconfig` is present, one will be generated for you. The script will also download type definitions from [flow-typed](https://github.com/flowtype/flow-typed) automatically. -If you don't have the auto-generated `.flowconfig` because you had flow setup before, you can copy it from here: - -```ini -[ignore] -/node_modules/fbjs/.* - -[libs] -./flow-typed -``` - Flow errors will show up alongside eslint errors as you work on your application. >Note: If your global flow installation version differs from react-scripts's flow version, you may experience slower compilation times while going back and forth between your app and your IDE (that may use your global flow). Ensure you have the same `flow-bin` version [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-dev-utils/package.json). From be43c83abe982dde33612175c79d4a73b63ef1a2 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Mon, 5 Dec 2016 23:33:29 +0100 Subject: [PATCH 24/31] In e2e: assert flow is correctly showing errors --- tasks/e2e.sh | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tasks/e2e.sh b/tasks/e2e.sh index 7a47d99fef8..5d9d5dbe172 100755 --- a/tasks/e2e.sh +++ b/tasks/e2e.sh @@ -159,12 +159,14 @@ cp .gitignore .gitignore.backup echo " /* @flow */ var wrong: string = 0; -" >> src/App.js -npm start -- --smoke-test || true +" > src/App.js +cat src/App.backup.js >> src/App.js +CI=true npm run build >> errors.log 2>> errors.log || true +cat errors.log | grep "This type is incompatible with" test -e .flowconfig test -d flow-typed cat .gitignore | grep flow-typed -rm src/App.js .gitignore +rm src/App.js .gitignore errors.log cp src/App.backup.js src/App.js cp .gitignore.backup .gitignore rm src/App.backup.js .gitignore.backup .flowconfig @@ -210,12 +212,13 @@ cp .gitignore .gitignore.backup echo " /* @flow */ var wrong: string = 0; -" >> src/App.js -npm start -- --smoke-test || true -test -e .flowconfig +" > src/App.js +cat src/App.backup.js >> src/App.js +CI=true npm run build >> errors.log 2>> errors.log || true +cat errors.log | grep "This type is incompatible with" test -d flow-typed cat .gitignore | grep flow-typed -rm src/App.js .gitignore +rm src/App.js .gitignore errors.log cp src/App.backup.js src/App.js cp .gitignore.backup .gitignore rm src/App.backup.js .gitignore.backup .flowconfig From 46829e3f048f16ce9673723f7968e0d37c36a997 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Thu, 8 Dec 2016 17:07:04 +0100 Subject: [PATCH 25/31] Only change gitignore if we create a flowconfig --- packages/react-dev-utils/FlowTypecheckPlugin.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index ff324069a90..a9fc4672940 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -58,10 +58,10 @@ function writeFileIfDoesNotExist(path, data) { if (err) { return reject(err); } - resolve(data); + resolve(true); }); } else { - resolve(data); + resolve(false); } }); }); @@ -75,7 +75,7 @@ function writeInFileIfNotPresent(path, contentToAssert, contentToAppend) { if (err) { return reject(err); } - resolve(contentToAppend); + resolve(true); }); } else { fs.readFile(path, (err, existingContent) => { @@ -87,10 +87,10 @@ function writeInFileIfNotPresent(path, contentToAssert, contentToAppend) { if (err) { return reject(err); } - resolve(contentToAppend); + resolve(true); }); } else { - resolve(contentToAppend); + resolve(false); } }); } @@ -126,8 +126,11 @@ function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { )) : true ), - writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')), - writeInFileIfNotPresent(gitignorePath, 'flow-typed', 'flow-typed'), + writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')) + .then(wroteFlowconfig => wroteFlowconfig ? + writeInFileIfNotPresent(gitignorePath, 'flow-typed', 'flow-typed/npm') : + false + ), execOneTime( flowTypedPath, ['install', '--overwrite', '--flowVersion=' + localVersion], From 74705ffa9f71251b03bc9d632e4b49da79590e10 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Thu, 8 Dec 2016 18:49:27 +0100 Subject: [PATCH 26/31] Only consider src tests --- packages/react-scripts/config/webpack.config.dev.js | 4 ++-- packages/react-scripts/config/webpack.config.prod.js | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index 7677b8b5011..f119b6e712f 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -232,8 +232,8 @@ module.exports = { // This plugin makes webpack aware of them without emitting them. // See https://github.com/facebookincubator/create-react-app/issues/1169 new WatchTestFilesPlugin([ - '**/__tests__/**', - '**/*.test.js', + 'src/**/__tests__/**', + 'src/**/*.test.js', ]), ], // Some libraries import Node modules but don't use them in the browser. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 71509658eb7..361442f8ee1 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -16,6 +16,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin'); var ManifestPlugin = require('webpack-manifest-plugin'); var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); var SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); +var WatchTestFilesPlugin = require('react-dev-utils/WatchTestFilesPlugin'); var url = require('url'); var paths = require('./paths'); var getClientEnvironment = require('./env'); @@ -277,7 +278,14 @@ module.exports = { // Generate and inject subresources hashes in the final `index.html`. new SubresourceIntegrityPlugin({ hashFuncNames: ['sha256', 'sha384'] - }) + }), + // Tests won't have any linting unless they go through webpack. + // This plugin makes webpack aware of them without emitting them. + // See https://github.com/facebookincubator/create-react-app/issues/1169 + new WatchTestFilesPlugin([ + 'src/**/__tests__/**', + 'src/**/*.test.js', + ]) ], // 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. From 7489f54e0c5e03880aeaa90c3f8401809d19ca8f Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Fri, 9 Dec 2016 20:07:24 +0100 Subject: [PATCH 27/31] Ignore directories in test glob --- packages/react-dev-utils/WatchTestFilesPlugin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-dev-utils/WatchTestFilesPlugin.js b/packages/react-dev-utils/WatchTestFilesPlugin.js index b7b25fd4713..65e16f76d44 100644 --- a/packages/react-dev-utils/WatchTestFilesPlugin.js +++ b/packages/react-dev-utils/WatchTestFilesPlugin.js @@ -18,7 +18,8 @@ function getGlobs(patterns, cwd) { return Promise.all(patterns.map(globPattern => computeGlob(globPattern, { cwd: cwd, - ignore: 'node_modules/**', + ignore: 'node_modules/**/*', + nodir: true, }) )) .then(globLists => [].concat.apply([], globLists)) From 58e82874a773647bb3dccf20ee3a5cb6a820d1f5 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Fri, 9 Dec 2016 20:07:24 +0100 Subject: [PATCH 28/31] Ignore directories in test glob --- packages/react-dev-utils/WatchTestFilesPlugin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-dev-utils/WatchTestFilesPlugin.js b/packages/react-dev-utils/WatchTestFilesPlugin.js index b7b25fd4713..65e16f76d44 100644 --- a/packages/react-dev-utils/WatchTestFilesPlugin.js +++ b/packages/react-dev-utils/WatchTestFilesPlugin.js @@ -18,7 +18,8 @@ function getGlobs(patterns, cwd) { return Promise.all(patterns.map(globPattern => computeGlob(globPattern, { cwd: cwd, - ignore: 'node_modules/**', + ignore: 'node_modules/**/*', + nodir: true, }) )) .then(globLists => [].concat.apply([], globLists)) From 3639147051111602a24bc095d6351aebda17a203 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Fri, 9 Dec 2016 20:29:13 +0100 Subject: [PATCH 29/31] Don't reinit flow in a child compilation --- packages/react-dev-utils/FlowTypecheckPlugin.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index a9fc4672940..f23e6b9e133 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -184,9 +184,11 @@ FlowTypecheckPlugin.prototype.apply = function(compiler) { loaderContext.fs.readFile(module.resource, (err, data) => { if (data && data.toString().indexOf('@flow') >= 0) { if (!flowActiveOnProject) { - flowInitializationPromise = initializeFlow( - compiler.options.context, this.flowconfig, this.otherFlowTypedDefs - ) + flowInitializationPromise = (!compiler.parentCompilation ? + initializeFlow( + compiler.options.context, this.flowconfig, this.otherFlowTypedDefs + ) : Promise.resolve() + ) .catch(e => { loaderContext.emitWarning(new Error( 'Flow project initialization warning:\n' + From 07a556e5b0aad5779edd63d16523d6c86ce2cab7 Mon Sep 17 00:00:00 2001 From: Robin Ricard Date: Fri, 9 Dec 2016 20:53:15 +0100 Subject: [PATCH 30/31] Refine globbing --- packages/react-scripts/config/webpack.config.dev.js | 5 +++-- packages/react-scripts/config/webpack.config.prod.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/react-scripts/config/webpack.config.dev.js b/packages/react-scripts/config/webpack.config.dev.js index f119b6e712f..2937b5b78e3 100644 --- a/packages/react-scripts/config/webpack.config.dev.js +++ b/packages/react-scripts/config/webpack.config.dev.js @@ -232,8 +232,9 @@ module.exports = { // This plugin makes webpack aware of them without emitting them. // See https://github.com/facebookincubator/create-react-app/issues/1169 new WatchTestFilesPlugin([ - 'src/**/__tests__/**', - 'src/**/*.test.js', + 'src/**/__tests__/**/*', + 'src/**/*.test.*', + '__tests__/**/*', ]), ], // Some libraries import Node modules but don't use them in the browser. diff --git a/packages/react-scripts/config/webpack.config.prod.js b/packages/react-scripts/config/webpack.config.prod.js index 361442f8ee1..b0426612df2 100644 --- a/packages/react-scripts/config/webpack.config.prod.js +++ b/packages/react-scripts/config/webpack.config.prod.js @@ -283,8 +283,9 @@ module.exports = { // This plugin makes webpack aware of them without emitting them. // See https://github.com/facebookincubator/create-react-app/issues/1169 new WatchTestFilesPlugin([ - 'src/**/__tests__/**', - 'src/**/*.test.js', + 'src/**/__tests__/**/*', + 'src/**/*.test.*', + '__tests__/**/*', ]) ], // Some libraries import Node modules but don't use them in the browser. From 1e21de786f0f7977658e3c7333830b467728eafb Mon Sep 17 00:00:00 2001 From: Gregor Weber Date: Sat, 18 Mar 2017 21:06:31 +0100 Subject: [PATCH 31/31] Don't put flow-typed on gitignore --- packages/react-dev-utils/FlowTypecheckPlugin.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/react-dev-utils/FlowTypecheckPlugin.js b/packages/react-dev-utils/FlowTypecheckPlugin.js index f23e6b9e133..a6b9e020f1b 100644 --- a/packages/react-dev-utils/FlowTypecheckPlugin.js +++ b/packages/react-dev-utils/FlowTypecheckPlugin.js @@ -108,8 +108,6 @@ function getFlowVersion(options) { } function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { - const flowconfigPath = path.join(projectPath, '.flowconfig'); - const gitignorePath = path.join(projectPath, '.gitignore'); return getFlowVersion().then(localVersion => Promise.all([ getFlowVersion({global: true}).catch(() => localVersion) .then(globalVersion => @@ -126,11 +124,6 @@ function initializeFlow(projectPath, flowconfig, otherFlowTypedDefs) { )) : true ), - writeFileIfDoesNotExist(flowconfigPath, flowconfig.join('\n')) - .then(wroteFlowconfig => wroteFlowconfig ? - writeInFileIfNotPresent(gitignorePath, 'flow-typed', 'flow-typed/npm') : - false - ), execOneTime( flowTypedPath, ['install', '--overwrite', '--flowVersion=' + localVersion],