Skip to content

Commit aeb0841

Browse files
iansukiku-jw
authored andcommitted
Reapply "Speed up TypeScript v2 (facebook#6406)" (facebook#6586)
This reverts commit b2cf28b and reapplies facebook#6406.
1 parent a66bdb0 commit aeb0841

14 files changed

+220
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
'use strict';
9+
10+
var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
11+
12+
module.exports = ForkTsCheckerWebpackPlugin;

packages/react-dev-utils/README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,19 @@ printHostingInstructions(appPackage, publicUrl, publicPath, 'build', true);
325325

326326
Returns a Promise resolving to either `defaultPort` or next available port if the user confirms it is okay to do. If the port is taken and the user has refused to use another port, or if the terminal is not interactive and can’t present user with the choice, resolves to `null`.
327327

328-
##### `createCompiler(webpack: Function, config: Object, appName: string, urls: Object, useYarn: boolean): WebpackCompiler`
328+
##### `createCompiler(args: Object): WebpackCompiler`
329329

330-
Creates a Webpack compiler instance for WebpackDevServer with built-in helpful messages. Takes the `require('webpack')` entry point as the first argument. To provide the `urls` argument, use `prepareUrls()` described below.
330+
Creates a Webpack compiler instance for WebpackDevServer with built-in helpful messages.
331+
332+
The `args` object accepts a number of properties:
333+
334+
- **appName** `string`: The name that will be printed to the terminal.
335+
- **config** `Object`: The webpack configuration options to be provided to the webpack constructor.
336+
- **devSocket** `Object`: Required if `useTypeScript` is `true`. This object should include `errors` and `warnings` which are functions accepting an array of errors or warnings emitted by the type checking. This is useful when running `fork-ts-checker-webpack-plugin` with `async: true` to report errors that are emitted after the webpack build is complete.
337+
- **urls** `Object`: To provide the `urls` argument, use `prepareUrls()` described below.
338+
- **useYarn** `boolean`: If `true`, yarn instructions will be emitted in the terminal instead of npm.
339+
- **useTypeScript** `boolean`: If `true`, TypeScript type checking will be enabled. Be sure to provide the `devSocket` argument above if this is set to `true`.
340+
- **webpack** `function`: A reference to the webpack constructor.
331341

332342
##### `prepareProxy(proxySetting: string, appPublicFolder: string): Object`
333343

packages/react-dev-utils/WebpackDevServerUtils.js

+96-20
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,10 @@ const inquirer = require('inquirer');
1717
const clearConsole = require('./clearConsole');
1818
const formatWebpackMessages = require('./formatWebpackMessages');
1919
const getProcessForPort = require('./getProcessForPort');
20+
const typescriptFormatter = require('./typescriptFormatter');
21+
const forkTsCheckerWebpackPlugin = require('./ForkTsCheckerWebpackPlugin');
2022

2123
const isInteractive = process.stdout.isTTY;
22-
let handleCompile;
23-
24-
// You can safely remove this after ejecting.
25-
// We only use this block for testing of Create React App itself:
26-
const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
27-
if (isSmokeTest) {
28-
handleCompile = (err, stats) => {
29-
if (err || stats.hasErrors() || stats.hasWarnings()) {
30-
process.exit(1);
31-
} else {
32-
process.exit(0);
33-
}
34-
};
35-
}
3624

3725
function prepareUrls(protocol, host, port) {
3826
const formatUrl = hostname =>
@@ -113,12 +101,20 @@ function printInstructions(appName, urls, useYarn) {
113101
console.log();
114102
}
115103

116-
function createCompiler(webpack, config, appName, urls, useYarn) {
104+
function createCompiler({
105+
appName,
106+
config,
107+
devSocket,
108+
urls,
109+
useYarn,
110+
useTypeScript,
111+
webpack,
112+
}) {
117113
// "Compiler" is a low-level interface to Webpack.
118114
// It lets us listen to some events and provide our own custom messages.
119115
let compiler;
120116
try {
121-
compiler = webpack(config, handleCompile);
117+
compiler = webpack(config);
122118
} catch (err) {
123119
console.log(chalk.red('Failed to compile.'));
124120
console.log();
@@ -139,10 +135,35 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
139135
});
140136

141137
let isFirstCompile = true;
138+
let tsMessagesPromise;
139+
let tsMessagesResolver;
140+
141+
if (useTypeScript) {
142+
compiler.hooks.beforeCompile.tap('beforeCompile', () => {
143+
tsMessagesPromise = new Promise(resolve => {
144+
tsMessagesResolver = msgs => resolve(msgs);
145+
});
146+
});
147+
148+
forkTsCheckerWebpackPlugin
149+
.getCompilerHooks(compiler)
150+
.receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
151+
const allMsgs = [...diagnostics, ...lints];
152+
const format = message =>
153+
`${message.file}\n${typescriptFormatter(message, true)}`;
154+
155+
tsMessagesResolver({
156+
errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
157+
warnings: allMsgs
158+
.filter(msg => msg.severity === 'warning')
159+
.map(format),
160+
});
161+
});
162+
}
142163

143164
// "done" event fires when Webpack has finished recompiling the bundle.
144165
// Whether or not you have warnings or errors, you will get this event.
145-
compiler.hooks.done.tap('done', stats => {
166+
compiler.hooks.done.tap('done', async stats => {
146167
if (isInteractive) {
147168
clearConsole();
148169
}
@@ -152,9 +173,43 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
152173
// them in a readable focused way.
153174
// We only construct the warnings and errors for speed:
154175
// https://github.com/facebook/create-react-app/issues/4492#issuecomment-421959548
155-
const messages = formatWebpackMessages(
156-
stats.toJson({ all: false, warnings: true, errors: true })
157-
);
176+
const statsData = stats.toJson({
177+
all: false,
178+
warnings: true,
179+
errors: true,
180+
});
181+
182+
if (useTypeScript && statsData.errors.length === 0) {
183+
const delayedMsg = setTimeout(() => {
184+
console.log(
185+
chalk.yellow(
186+
'Files successfully emitted, waiting for typecheck results...'
187+
)
188+
);
189+
}, 100);
190+
191+
const messages = await tsMessagesPromise;
192+
clearTimeout(delayedMsg);
193+
statsData.errors.push(...messages.errors);
194+
statsData.warnings.push(...messages.warnings);
195+
196+
// Push errors and warnings into compilation result
197+
// to show them after page refresh triggered by user.
198+
stats.compilation.errors.push(...messages.errors);
199+
stats.compilation.warnings.push(...messages.warnings);
200+
201+
if (messages.errors.length > 0) {
202+
devSocket.errors(messages.errors);
203+
} else if (messages.warnings.length > 0) {
204+
devSocket.warnings(messages.warnings);
205+
}
206+
207+
if (isInteractive) {
208+
clearConsole();
209+
}
210+
}
211+
212+
const messages = formatWebpackMessages(statsData);
158213
const isSuccessful = !messages.errors.length && !messages.warnings.length;
159214
if (isSuccessful) {
160215
console.log(chalk.green('Compiled successfully!'));
@@ -194,6 +249,27 @@ function createCompiler(webpack, config, appName, urls, useYarn) {
194249
);
195250
}
196251
});
252+
253+
// You can safely remove this after ejecting.
254+
// We only use this block for testing of Create React App itself:
255+
const isSmokeTest = process.argv.some(
256+
arg => arg.indexOf('--smoke-test') > -1
257+
);
258+
if (isSmokeTest) {
259+
compiler.hooks.failed.tap('smokeTest', async () => {
260+
await tsMessagesPromise;
261+
process.exit(1);
262+
});
263+
compiler.hooks.done.tap('smokeTest', async stats => {
264+
await tsMessagesPromise;
265+
if (stats.hasErrors() || stats.hasWarnings()) {
266+
process.exit(1);
267+
} else {
268+
process.exit(0);
269+
}
270+
});
271+
}
272+
197273
return compiler;
198274
}
199275

packages/react-dev-utils/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"url": "https://github.com/facebook/create-react-app/issues"
99
},
1010
"engines": {
11-
"node": ">=6"
11+
"node": ">=8.10"
1212
},
1313
"files": [
1414
"browsersHelper.js",
@@ -20,6 +20,7 @@
2020
"eslintFormatter.js",
2121
"evalSourceMapMiddleware.js",
2222
"FileSizeReporter.js",
23+
"ForkTsCheckerWebpackPlugin.js",
2324
"formatWebpackMessages.js",
2425
"getCacheIdentifier.js",
2526
"getCSSModuleLocalIdent.js",
@@ -54,6 +55,7 @@
5455
"escape-string-regexp": "1.0.5",
5556
"filesize": "3.6.1",
5657
"find-up": "3.0.0",
58+
"fork-ts-checker-webpack-plugin": "1.0.0-alpha.6",
5759
"global-modules": "2.0.0",
5860
"globby": "8.0.2",
5961
"gzip-size": "5.0.0",

packages/react-dev-utils/typescriptFormatter.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@ function formatter(message, useColors) {
4545
}
4646

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

4950
return [
50-
messageColor.bold(`Type ${severity.toLowerCase()}: `) +
51+
messageColor.bold(`${types[message.type]} ${severity.toLowerCase()}: `) +
5152
(hasGetters ? message.getContent() : message.content) +
5253
' ' +
53-
messageColor.underline(`TS${message.code}`),
54+
messageColor.underline(
55+
(message.type === 'lint' ? 'Rule: ' : 'TS') + message.code
56+
),
5457
'',
5558
frame,
5659
].join(os.EOL);

packages/react-dev-utils/webpackHotDevClient.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ function handleSuccess() {
106106
tryApplyUpdates(function onHotUpdateSuccess() {
107107
// Only dismiss it when we're sure it's a hot update.
108108
// Otherwise it would flicker right before the reload.
109-
ErrorOverlay.dismissBuildError();
109+
tryDismissErrorOverlay();
110110
});
111111
}
112112
}
@@ -140,19 +140,15 @@ function handleWarnings(warnings) {
140140
}
141141
}
142142

143+
printWarnings();
144+
143145
// Attempt to apply hot updates or reload.
144146
if (isHotUpdate) {
145147
tryApplyUpdates(function onSuccessfulHotUpdate() {
146-
// Only print warnings if we aren't refreshing the page.
147-
// Otherwise they'll disappear right away anyway.
148-
printWarnings();
149148
// Only dismiss it when we're sure it's a hot update.
150149
// Otherwise it would flicker right before the reload.
151-
ErrorOverlay.dismissBuildError();
150+
tryDismissErrorOverlay();
152151
});
153-
} else {
154-
// Print initial warnings immediately.
155-
printWarnings();
156152
}
157153
}
158154

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

182+
function tryDismissErrorOverlay() {
183+
if (!hasCompileErrors) {
184+
ErrorOverlay.dismissBuildError();
185+
}
186+
}
187+
186188
// There is a newer version of the code available.
187189
function handleAvailableHash(hash) {
188190
// Update last known compilation hash.

packages/react-scripts/config/webpack.config.js

+5-11
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent')
2929
const paths = require('./paths');
3030
const getClientEnvironment = require('./env');
3131
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
32-
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin-alt');
32+
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
3333
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
3434
// @remove-on-eject-begin
3535
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
@@ -617,17 +617,10 @@ module.exports = function(webpackEnv) {
617617
typescript: resolve.sync('typescript', {
618618
basedir: paths.appNodeModules,
619619
}),
620-
async: false,
620+
async: isEnvDevelopment,
621+
useTypescriptIncrementalApi: true,
621622
checkSyntacticErrors: true,
622623
tsconfig: paths.appTsConfig,
623-
compilerOptions: {
624-
module: 'esnext',
625-
moduleResolution: 'node',
626-
resolveJsonModule: true,
627-
isolatedModules: true,
628-
noEmit: true,
629-
jsx: 'preserve',
630-
},
631624
reportFiles: [
632625
'**',
633626
'!**/*.json',
@@ -638,7 +631,8 @@ module.exports = function(webpackEnv) {
638631
],
639632
watch: paths.appSrc,
640633
silent: true,
641-
formatter: typescriptFormatter,
634+
// The formatter is invoked directly in WebpackDevServerUtils during development
635+
formatter: isEnvProduction ? typescriptFormatter : undefined,
642636
}),
643637
].filter(Boolean),
644638
// Some libraries import Node modules but don't use them in the browser.

packages/react-scripts/package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"repository": "facebook/create-react-app",
66
"license": "MIT",
77
"engines": {
8-
"node": ">=6"
8+
"node": ">=8.10"
99
},
1010
"bugs": {
1111
"url": "https://github.com/facebook/create-react-app/issues"
@@ -45,7 +45,6 @@
4545
"eslint-plugin-jsx-a11y": "6.1.2",
4646
"eslint-plugin-react": "7.12.4",
4747
"file-loader": "2.0.0",
48-
"fork-ts-checker-webpack-plugin-alt": "0.4.14",
4948
"fs-extra": "7.0.1",
5049
"html-webpack-plugin": "4.0.0-alpha.2",
5150
"identity-obj-proxy": "3.0.0",

packages/react-scripts/scripts/start.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,24 @@ checkBrowsers(paths.appPath, isInteractive)
9494
const config = configFactory('development');
9595
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
9696
const appName = require(paths.appPackageJson).name;
97+
const useTypeScript = fs.existsSync(paths.appTsConfig);
9798
const urls = prepareUrls(protocol, HOST, port);
99+
const devSocket = {
100+
warnings: warnings =>
101+
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
102+
errors: errors =>
103+
devServer.sockWrite(devServer.sockets, 'errors', errors),
104+
};
98105
// Create a webpack compiler that is configured with custom messages.
99-
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
106+
const compiler = createCompiler({
107+
appName,
108+
config,
109+
devSocket,
110+
urls,
111+
useYarn,
112+
useTypeScript,
113+
webpack,
114+
});
100115
// Load proxy config
101116
const proxySetting = require(paths.appPackageJson).proxy;
102117
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);

test/fixtures/typescript-typecheck/.disable-pnp

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const testSetup = require('../__shared__/test-setup');
2+
const puppeteer = require('puppeteer');
3+
4+
const expectedErrorMsg = `Argument of type '123' is not assignable to parameter of type 'string'`;
5+
6+
test('shows error overlay in browser', async () => {
7+
const { port, done } = await testSetup.scripts.start();
8+
9+
const browser = await puppeteer.launch({ headless: true });
10+
try {
11+
const page = await browser.newPage();
12+
await page.goto(`http://localhost:${port}/`);
13+
await page.waitForSelector('iframe', { timeout: 5000 });
14+
const overlayMsg = await page.evaluate(() => {
15+
const overlay = document.querySelector('iframe').contentWindow;
16+
return overlay.document.body.innerHTML;
17+
});
18+
expect(overlayMsg).toContain(expectedErrorMsg);
19+
} finally {
20+
browser.close();
21+
done();
22+
}
23+
});
24+
25+
test('shows error in console (dev mode)', async () => {
26+
const { stderr } = await testSetup.scripts.start({ smoke: true });
27+
expect(stderr).toContain(expectedErrorMsg);
28+
});
29+
30+
test('shows error in console (prod mode)', async () => {
31+
const { stderr } = await testSetup.scripts.build();
32+
expect(stderr).toContain(expectedErrorMsg);
33+
});

0 commit comments

Comments
 (0)