Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Speed up TypeScript v2 #6406

Merged
merged 4 commits into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 96 additions & 20 deletions packages/react-dev-utils/WebpackDevServerUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,10 @@ const inquirer = require('inquirer');
const clearConsole = require('./clearConsole');
const formatWebpackMessages = require('./formatWebpackMessages');
const getProcessForPort = require('./getProcessForPort');
const typescriptFormatter = require('./typescriptFormatter');
const forkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

const isInteractive = process.stdout.isTTY;
let handleCompile;

// You can safely remove this after ejecting.
// We only use this block for testing of Create React App itself:
const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
if (isSmokeTest) {
handleCompile = (err, stats) => {
if (err || stats.hasErrors() || stats.hasWarnings()) {
process.exit(1);
} else {
process.exit(0);
}
};
}

function prepareUrls(protocol, host, port) {
const formatUrl = hostname =>
Expand Down Expand Up @@ -113,12 +101,20 @@ function printInstructions(appName, urls, useYarn) {
console.log();
}

function createCompiler(webpack, config, appName, urls, useYarn) {
function createCompiler(
ianschmitz marked this conversation as resolved.
Show resolved Hide resolved
webpack,
config,
appName,
urls,
useYarn,
useTypeScript,
devSocket
) {
// "Compiler" is a low-level interface to Webpack.
// It lets us listen to some events and provide our own custom messages.
let compiler;
try {
compiler = webpack(config, handleCompile);
compiler = webpack(config);
} catch (err) {
console.log(chalk.red('Failed to compile.'));
console.log();
Expand All @@ -139,10 +135,35 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
});

let isFirstCompile = true;
let tsMessagesPromise;
let tsMessagesResolver;

if (useTypeScript) {
compiler.hooks.beforeCompile.tap('beforeCompile', () => {
tsMessagesPromise = new Promise(resolve => {
tsMessagesResolver = msgs => resolve(msgs);
});
});

forkTsCheckerWebpackPlugin
.getCompilerHooks(compiler)
.receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
const allMsgs = [...diagnostics, ...lints];
const format = message =>
`${message.file}\n${typescriptFormatter(message, true)}`;

tsMessagesResolver({
errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
warnings: allMsgs
.filter(msg => msg.severity === 'warning')
.map(format),
});
});
}

// "done" event fires when Webpack has finished recompiling the bundle.
// Whether or not you have warnings or errors, you will get this event.
compiler.hooks.done.tap('done', stats => {
compiler.hooks.done.tap('done', async stats => {
if (isInteractive) {
clearConsole();
}
Expand All @@ -152,9 +173,43 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
// them in a readable focused way.
// We only construct the warnings and errors for speed:
// https://github.com/facebook/create-react-app/issues/4492#issuecomment-421959548
const messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
);
const statsData = stats.toJson({
all: false,
warnings: true,
errors: true,
});

if (useTypeScript && statsData.errors.length === 0) {
const delayedMsg = setTimeout(() => {
console.log(
chalk.yellow(
'Files successfully emitted, waiting for typecheck results...'
)
);
}, 100);

const messages = await tsMessagesPromise;
clearTimeout(delayedMsg);
statsData.errors.push(...messages.errors);
statsData.warnings.push(...messages.warnings);

// Push errors and warnings into compilation result
// to show them after page refresh triggered by user.
stats.compilation.errors.push(...messages.errors);
stats.compilation.warnings.push(...messages.warnings);

if (messages.errors.length > 0) {
devSocket.errors(messages.errors);
} else if (messages.warnings.length > 0) {
devSocket.warnings(messages.warnings);
}

if (isInteractive) {
clearConsole();
}
}

const messages = formatWebpackMessages(statsData);
const isSuccessful = !messages.errors.length && !messages.warnings.length;
if (isSuccessful) {
console.log(chalk.green('Compiled successfully!'));
Expand Down Expand Up @@ -194,6 +249,27 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
);
}
});

// You can safely remove this after ejecting.
// We only use this block for testing of Create React App itself:
const isSmokeTest = process.argv.some(
arg => arg.indexOf('--smoke-test') > -1
);
if (isSmokeTest) {
compiler.hooks.failed.tap('smokeTest', async () => {
await tsMessagesPromise;
process.exit(1);
});
compiler.hooks.done.tap('smokeTest', async stats => {
await tsMessagesPromise;
if (stats.hasErrors() || stats.hasWarnings()) {
process.exit(1);
} else {
process.exit(0);
}
});
}

return compiler;
}

Expand Down
7 changes: 5 additions & 2 deletions packages/react-dev-utils/typescriptFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,15 @@ function formatter(message, useColors) {
}

const severity = hasGetters ? message.getSeverity() : message.severity;
const types = { diagnostic: 'TypeScript', lint: 'TSLint' };

return [
messageColor.bold(`Type ${severity.toLowerCase()}: `) +
messageColor.bold(`${types[message.type]} ${severity.toLowerCase()}: `) +
(hasGetters ? message.getContent() : message.content) +
' ' +
messageColor.underline(`TS${message.code}`),
messageColor.underline(
(message.type === 'lint' ? 'Rule: ' : 'TS') + message.code
),
'',
frame,
].join(os.EOL);
Expand Down
18 changes: 10 additions & 8 deletions packages/react-dev-utils/webpackHotDevClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function handleSuccess() {
tryApplyUpdates(function onHotUpdateSuccess() {
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError();
tryDismissErrorOverlay();
});
}
}
Expand Down Expand Up @@ -140,19 +140,15 @@ function handleWarnings(warnings) {
}
}

printWarnings();

// Attempt to apply hot updates or reload.
if (isHotUpdate) {
tryApplyUpdates(function onSuccessfulHotUpdate() {
// Only print warnings if we aren't refreshing the page.
// Otherwise they'll disappear right away anyway.
printWarnings();
// Only dismiss it when we're sure it's a hot update.
// Otherwise it would flicker right before the reload.
ErrorOverlay.dismissBuildError();
tryDismissErrorOverlay();
});
} else {
// Print initial warnings immediately.
printWarnings();
}
}

Expand Down Expand Up @@ -183,6 +179,12 @@ function handleErrors(errors) {
// We will reload on next success instead.
}

function tryDismissErrorOverlay() {
if (!hasCompileErrors) {
ErrorOverlay.dismissBuildError();
}
}

// There is a newer version of the code available.
function handleAvailableHash(hash) {
// Update last known compilation hash.
Expand Down
16 changes: 5 additions & 11 deletions packages/react-scripts/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent')
const paths = require('./paths');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
Expand Down Expand Up @@ -625,17 +625,10 @@ module.exports = function(webpackEnv) {
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: false,
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
tsconfig: paths.appTsConfig,
compilerOptions: {
module: 'esnext',
moduleResolution: 'node',
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: 'preserve',
},
reportFiles: [
'**',
'!**/*.json',
Expand All @@ -646,7 +639,8 @@ module.exports = function(webpackEnv) {
],
watch: paths.appSrc,
silent: true,
formatter: typescriptFormatter,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
Expand Down
2 changes: 1 addition & 1 deletion packages/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"eslint-plugin-jsx-a11y": "6.1.2",
"eslint-plugin-react": "7.12.4",
"file-loader": "2.0.0",
"fork-ts-checker-webpack-plugin-alt": "0.4.14",
"fork-ts-checker-webpack-plugin": "1.0.0-alpha.6",
"fs-extra": "7.0.1",
"html-webpack-plugin": "4.0.0-alpha.2",
"identity-obj-proxy": "3.0.0",
Expand Down
17 changes: 16 additions & 1 deletion packages/react-scripts/scripts/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,24 @@ checkBrowsers(paths.appPath, isInteractive)
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const urls = prepareUrls(protocol, HOST, port);
const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
const compiler = createCompiler(
webpack,
config,
appName,
urls,
useYarn,
useTypeScript,
devSocket
);
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
Expand Down
Empty file.
33 changes: 33 additions & 0 deletions test/fixtures/typescript-typecheck/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const testSetup = require('../__shared__/test-setup');
const puppeteer = require('puppeteer');

const expectedErrorMsg = `Argument of type '123' is not assignable to parameter of type 'string'`;

test('shows error overlay in browser', async () => {
const { port, done } = await testSetup.scripts.start();

const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto(`http://localhost:${port}/`);
await page.waitForSelector('iframe', { timeout: 5000 });
const overlayMsg = await page.evaluate(() => {
const overlay = document.querySelector('iframe').contentWindow;
return overlay.document.body.innerHTML;
});
expect(overlayMsg).toContain(expectedErrorMsg);
} finally {
browser.close();
done();
}
});

test('shows error in console (dev mode)', async () => {
const { stderr } = await testSetup.scripts.start({ smoke: true });
expect(stderr).toContain(expectedErrorMsg);
});

test('shows error in console (prod mode)', async () => {
const { stderr } = await testSetup.scripts.build();
expect(stderr).toContain(expectedErrorMsg);
});
9 changes: 9 additions & 0 deletions test/fixtures/typescript-typecheck/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"dependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "*",
"react-dom": "*",
"typescript": "3.1.3"
}
}
13 changes: 13 additions & 0 deletions test/fixtures/typescript-typecheck/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as React from 'react';

class App extends React.Component {
render() {
return <div>{format(123)}</div>;
}
}

function format(value: string) {
return value;
}

export default App;
5 changes: 5 additions & 0 deletions test/fixtures/typescript-typecheck/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));