Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 26.9k
Add flow typechecking as a webpack plugin#1152
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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
10ec304c7eacf8b0c39e7de1626931b2e04d4763a52f34a6779b3b86d004f2dec8d230bb39b1988785026a5cfc1513c0a9719ac82c76f15eba7a05a2362346f83fa6af42c5ccf9ae1f0c5e2102e4e2111f9bf33d4448e98db929a1eac318d054357fb3e5609cabeadbfa6d3c805ae460e30a9593ed5f2c026e5816e44124304721809c2c133949fb3159638b5105c294fa4569ba483c6a48e8c39e1957ec834095f5d3982cfc1115030f7c137444f208ca78351cca107e33f9da3a78f7eb9e341137f59f2fd2dbc7c5749df4299efa3fb2dfde9961029628ef30494dcf8502668d1f5151df2cb570ad8dd45841cddab6f9d1cb808ee94387c45e6c29fb4735c800af94ad625f1File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| /** | ||
| * 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. | ||
| */ | ||
| 'use strict'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const chalk = require('chalk'); | ||
| const childProcess = require('child_process'); | ||
| const flowBinPath = require('flow-bin'); | ||
| function exec(command, args, options) { | ||
| 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('error', error => reject(error)); | ||
| oneTimeProcess.on('exit', code => { | ||
| switch (code) { | ||
| case 0: { | ||
| return resolve(stdout); | ||
| } | ||
| default: { | ||
| return reject(new Error(Buffer.concat([stdout, stderr]).toString())); | ||
| } | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
| function createVersionWarning(flowVersion) { | ||
| return 'Flow: ' + | ||
| chalk.red( | ||
| chalk.bold( | ||
| `Your global flow version is incompatible with this tool. | ||
| To fix warning, uninstall it or run \`npm install -g flow-bin@${flowVersion}\`.` | ||
| ) | ||
| ); | ||
| } | ||
| function formatFlowErrors(error) { | ||
| return error | ||
| .toString() | ||
| .split('\n') | ||
| .filter(line => { | ||
| return !(/flow is still initializing/.test(line) || | ||
| /Found \d+ error/.test(line) || | ||
| /The flow server is not responding/.test(line) || | ||
| /Going to launch a new one/.test(line) || | ||
| /The flow server is not responding/.test(line) || | ||
| /Spawned flow server/.test(line) || | ||
| /Logs will go to/.test(line) || | ||
| /version didn't match the client's/.test(line)); | ||
| }) | ||
| .map(line => line.replace(/^Error:\s*/, '')) | ||
| .join('\n'); | ||
| } | ||
| function getFlowVersion(global) { | ||
| return exec(global ? 'flow' : flowBinPath, ['version', '--json']) | ||
| .then(data => JSON.parse(data.toString('utf8')).semver || '0.0.0') | ||
| .catch(() => null); | ||
| } | ||
| class FlowTypecheckPlugin { | ||
| constructor() { | ||
| this.shouldRun = false; | ||
| this.flowStarted = false; | ||
| this.flowStarting = null; | ||
| this.flowVersion = require(path.join( | ||
| __dirname, | ||
| 'package.json' | ||
| )).dependencies['flow-bin']; | ||
| } | ||
| startFlow(cwd) { | ||
| if (this.flowStarted) { | ||
| return Promise.resolve(); | ||
| } | ||
| if (this.flowStarting != null) { | ||
| return this.flowStarting.then(err => { | ||
| // We need to do it like this because of unhandled rejections | ||
| // ... basically, we can't actually reject a promise unless someone | ||
| // has it handled -- which is only the case when we're returned from here | ||
| if (err != null) { | ||
| throw err; | ||
| } | ||
| }); | ||
| } | ||
| console.log(chalk.cyan('Starting the flow server ...')); | ||
| const flowConfigPath = path.join(cwd, '.flowconfig'); | ||
| let delegate; | ||
| this.flowStarting = new Promise(resolve => { | ||
| delegate = resolve; | ||
| }); | ||
| return getFlowVersion(true) | ||
| .then(globalVersion => { | ||
| if (globalVersion === null) return; | ||
| return getFlowVersion(false).then(ourVersion => { | ||
| if (globalVersion !== ourVersion) { | ||
| return Promise.reject('__FLOW_VERSION_MISMATCH__'); | ||
| } | ||
| }); | ||
| }) | ||
| .then( | ||
| () => new Promise(resolve => { | ||
| fs.access(flowConfigPath, err => { | ||
| if (err) { | ||
| resolve(exec(flowBinPath, ['init'], { cwd })); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| }); | ||
| }) | ||
| ) | ||
| .then(() => exec(flowBinPath, ['stop'], { | ||
| cwd, | ||
| })) | ||
| .then(() => exec(flowBinPath, ['start'], { cwd }).catch(err => { | ||
| if ( | ||
| typeof err.message === 'string' && | ||
| err.message.indexOf('There is already a server running') !== -1 | ||
| ) { | ||
| return true; | ||
| } else { | ||
| throw err; | ||
| } | ||
| })) | ||
| .then(() => { | ||
| this.flowStarted = true; | ||
| delegate(); | ||
| this.flowStarting = null; | ||
| }) | ||
| .catch(err => { | ||
| delegate(err); | ||
| this.flowStarting = null; | ||
| throw err; | ||
| }); | ||
| } | ||
| apply(compiler) { | ||
| compiler.plugin('compile', () => { | ||
| this.shouldRun = false; | ||
| }); | ||
| compiler.plugin('compilation', compilation => { | ||
| compilation.plugin('normal-module-loader', (loaderContext, module) => { | ||
| if ( | ||
| this.shouldRun || | ||
| module.resource.indexOf('node_modules') !== -1 || | ||
| !/[.]js(x)?$/.test(module.resource) | ||
| ) { | ||
| return; | ||
| } | ||
| const contents = loaderContext.fs.readFileSync(module.resource, 'utf8'); | ||
| if ( | ||
| /^\s*\/\/.*@flow/.test(contents) || /^\s*\/\*.*@flow/.test(contents) | ||
| ) { | ||
| this.shouldRun = true; | ||
| } | ||
| }); | ||
| }); | ||
| // Run lint checks | ||
| compiler.plugin('emit', (compilation, callback) => { | ||
| if (!this.shouldRun) { | ||
| callback(); | ||
| return; | ||
| } | ||
| const cwd = compiler.options.context; | ||
| const first = this.flowStarting == null && !this.flowStarted; | ||
| this.startFlow(cwd) | ||
| .then(() => { | ||
| if (first) { | ||
| console.log( | ||
| chalk.yellow( | ||
| 'Flow is initializing, ' + | ||
| chalk.bold('this might take a while...') | ||
| ) | ||
| ); | ||
| } else { | ||
| console.log('Running flow...'); | ||
| } | ||
| exec(flowBinPath, ['status', '--color=always'], { cwd }) | ||
| .then(() => { | ||
| callback(); | ||
| }) | ||
| .catch(e => { | ||
| compilation.warnings.push(formatFlowErrors(e)); | ||
| callback(); | ||
| }); | ||
| }) | ||
| .catch(e => { | ||
| if (e === '__FLOW_VERSION_MISMATCH__') { | ||
| compilation.warnings.push(createVersionWarning(this.flowVersion)); | ||
| } else { | ||
| compilation.warnings.push( | ||
| 'Flow: Type checking has been disabled due to an error in Flow.' | ||
| ); | ||
| } | ||
| callback(); | ||
| }); | ||
| }); | ||
| } | ||
| } | ||
| module.exports = FlowTypecheckPlugin; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,6 +16,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); | ||
| const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); | ||
| const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); | ||
| const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); | ||
| var FlowTypecheckPlugin = require('react-dev-utils/FlowTypecheckPlugin'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the other imports uses | ||
| const getClientEnvironment = require('./env'); | ||
| const paths = require('./paths'); | ||
| @@ -241,6 +242,8 @@ module.exports = { | ||
| // 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), | ||
| // Run Flow on files with the @flow header | ||
| 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. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this not slowing down the build? Seems suspicious to read every file if Webpack also does it by itself.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loaderContext.fs.readFileSync is webpack's memory file system, not the real filesystem.
We're basically doing cache[key].
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aaaah.