-
-
Notifications
You must be signed in to change notification settings - Fork 26.9k
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
Add flow typechecking as a webpack plugin #1152
Closed
Closed
Changes from all commits
Commits
Show all changes
75 commits
Select commit
Hold shift + click to select a range
10ec304
Setup a plugin to run a flow checking lifecycle
rricard c7eacf8
Make flow fail on CI builds
rricard b0c39e7
Run flow as a server and check with status
rricard de16269
Add flow ignore comment to the start hints
rricard 31b2e04
Add flow-typed resolution at start
rricard d4763a5
Reset server stderr on restart
rricard 2f34a67
Move options as explicit props of the plugin
rricard 79b3b86
Use arrow functions
rricard d004f2d
Refactor using promises and no class attrs
rricard ec8d230
Add documentation for out-of-the-box flow
rricard bb39b19
Schedule project init to complete before 1st check
rricard 8878502
Process code-based flow checks
rricard 6a5cfc1
Get flow version internally without config
rricard 513c0a9
Check global flow version
rricard 719ac82
Add flow-typed to gitignore automatically
rricard c76f15e
Add optional flow to end to end testing
rricard ba7a05a
Run flow even if init failed
rricard 2362346
Remove fbjs ignores now that it's fixed
rricard f83fa6a
Remove flow-typed lib import (known by default!)
rricard f42c5cc
In e2e: assert flow is correctly showing errors
rricard f9ae1f0
Only change gitignore if we create a flowconfig
rricard c5e2102
Ensure the init promise is correctly triggered
rricard e4e2111
Don't create an uncatched promise
rricard f9bf33d
Don't reinit flow in a child compilation
rricard 4448e98
Remove flow-typed/ dir assertion
rricard db929a1
Reapply a lost change from #1233 during rebase
rricard eac318d
Consistent ESLint capitalization
rricard 054357f
Run flow version check before the rest
rricard b3e5609
Transmit via warnings why Flow was disabled
rricard cabeadb
Don't fail on tarball react-scripts
rricard fa6d3c8
Celebrate when the flow tests pass!
rricard 05ae460
Show npm commands in cyan
rricard e30a959
"Hide" flow err suppression in dev utils
rricard 3ed5f2c
Only highlight the npm word
rricard 026e581
Make flow-typed provision sequential
rricard 6e44124
Redisign & simplify flow e2e tests
rricard 3047218
Correctly ignore invalid comp errors
rricard 09c2c13
Add ref to an issue comment on the silencing
rricard 3949fb3
Remove debugging command in e2e.sh
rricard 159638b
Remove planning to add the feature in doc[ci skip]
rricard 5105c29
No build test for flow (duplicate)
rricard 4fa4569
Simpler second flow test
rricard ba483c6
After eject, the .flowconfig should stay
rricard a48e8c3
Use Flow 0.37.0
rricard 9e1957e
Directly target the flow-typed executable
rricard c834095
Revert "Directly target the flow-typed executable"
rricard f5d3982
Find flow-typed in the npm flat struct
rricard cfc1115
Define flow-typed path with project root
rricard 030f7c1
Resolve flow-typed via module resolution
rricard 37444f2
Remove unneeded complexity
rricard 08ca783
Make run flow-typed executable
rricard 51cca10
Don't put flow-typed on gitignore
Gregoor 7e33f9d
Merge branch 'master' into flow
rricard a3a78f7
Appease linter
Timer eb9e341
Update flow
Timer 137f59f
Add basic flow type checking
Timer 2fd2dbc
Adjust format function
Timer 7c5749d
Remove flow-typed stub
Timer f4299ef
Use access instead of stat to prevent dual-compile
Timer a3fb2df
Add some friendly comments
Timer de99610
Lock down flow version
Timer 29628ef
Update comment logs
Timer 30494dc
Remove old method
Timer f850266
Remove from e2e
Timer 8d1f515
Check flow version for performance
Timer 1df2cb5
Remove warning prefix for flow messages
Timer 70ad8dd
Update README
Timer 45841cd
Don't double compute
Timer dab6f9d
Fix linter errors
Timer 1cb808e
Use default perm check
Timer e94387c
Handle already running server since it exits with a non-zero
Timer 45e6c29
Reduce nesting
Timer fb4735c
Simulate a mutex for startFlow
Timer 800af94
Fix unhandled rejection
Timer ad625f1
Upgrade flow
Timer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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.