Skip to content

Commit b20f4a3

Browse files
ianschmitznate770
authored andcommitted
Respect tsconfig.json extends when validating config (facebook#5537)
* Use TS to resolve tsconfig extends * Prevent modifications to original tsconfig * Print friendly error
1 parent d1fa628 commit b20f4a3

File tree

3 files changed

+100
-36
lines changed

3 files changed

+100
-36
lines changed

packages/react-dev-utils/immer.js

+12
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 immer = require('immer');
11+
12+
module.exports = immer;

packages/react-dev-utils/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"getCSSModuleLocalIdent.js",
2525
"getProcessForPort.js",
2626
"ignoredFiles.js",
27+
"immer.js",
2728
"InlineChunkHtmlPlugin.js",
2829
"inquirer.js",
2930
"InterpolateHtmlPlugin.js",
@@ -53,6 +54,7 @@
5354
"find-up": "3.0.0",
5455
"global-modules": "1.0.0",
5556
"gzip-size": "5.0.0",
57+
"immer": "1.7.2",
5658
"inquirer": "6.2.0",
5759
"is-root": "2.0.0",
5860
"loader-utils": "1.1.0",

packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js

+86-36
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,12 @@ const resolve = require('resolve');
1414
const path = require('path');
1515
const paths = require('../../config/paths');
1616
const os = require('os');
17+
const immer = require('react-dev-utils/immer').produce;
1718

1819
function writeJson(fileName, object) {
1920
fs.writeFileSync(fileName, JSON.stringify(object, null, 2) + os.EOL);
2021
}
2122

22-
const compilerOptions = {
23-
// These are suggested values and will be set when not present in the
24-
// tsconfig.json
25-
target: { suggested: 'es5' },
26-
allowJs: { suggested: true },
27-
skipLibCheck: { suggested: true },
28-
esModuleInterop: { suggested: true },
29-
allowSyntheticDefaultImports: { suggested: true },
30-
strict: { suggested: true },
31-
32-
// These values are required and cannot be changed by the user
33-
module: { value: 'esnext', reason: 'for import() and import/export' },
34-
moduleResolution: { value: 'node', reason: 'to match webpack resolution' },
35-
resolveJsonModule: { value: true, reason: 'to match webpack loader' },
36-
isolatedModules: { value: true, reason: 'implementation limitation' },
37-
noEmit: { value: true },
38-
jsx: { value: 'preserve', reason: 'JSX is compiled by Babel' },
39-
};
40-
4123
function verifyTypeScriptSetup() {
4224
let firstTimeSetup = false;
4325

@@ -86,48 +68,115 @@ function verifyTypeScriptSetup() {
8668
process.exit(1);
8769
}
8870

71+
const compilerOptions = {
72+
// These are suggested values and will be set when not present in the
73+
// tsconfig.json
74+
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
75+
target: {
76+
parsedValue: ts.ScriptTarget.ES5,
77+
suggested: 'es5',
78+
},
79+
allowJs: { suggested: true },
80+
skipLibCheck: { suggested: true },
81+
esModuleInterop: { suggested: true },
82+
allowSyntheticDefaultImports: { suggested: true },
83+
strict: { suggested: true },
84+
85+
// These values are required and cannot be changed by the user
86+
module: {
87+
parsedValue: ts.ModuleKind.ESNext,
88+
value: 'esnext',
89+
reason: 'for import() and import/export',
90+
},
91+
moduleResolution: {
92+
parsedValue: ts.ModuleResolutionKind.NodeJs,
93+
value: 'node',
94+
reason: 'to match webpack resolution',
95+
},
96+
resolveJsonModule: { value: true, reason: 'to match webpack loader' },
97+
isolatedModules: { value: true, reason: 'implementation limitation' },
98+
noEmit: { value: true },
99+
jsx: {
100+
parsedValue: ts.JsxEmit.Preserve,
101+
value: 'preserve',
102+
reason: 'JSX is compiled by Babel',
103+
},
104+
};
105+
106+
const formatDiagnosticHost = {
107+
getCanonicalFileName: fileName => fileName,
108+
getCurrentDirectory: ts.sys.getCurrentDirectory,
109+
getNewLine: () => os.EOL,
110+
};
111+
89112
const messages = [];
90-
let tsconfig;
113+
let appTsConfig;
114+
let parsedTsConfig;
115+
let parsedCompilerOptions;
91116
try {
92-
const { config, error } = ts.readConfigFile(
117+
const { config: readTsConfig, error } = ts.readConfigFile(
93118
paths.appTsConfig,
94119
ts.sys.readFile
95120
);
96121

97122
if (error) {
98-
throw error;
123+
throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost));
99124
}
100125

101-
tsconfig = config;
102-
} catch (_) {
126+
appTsConfig = readTsConfig;
127+
128+
// Get TS to parse and resolve any "extends"
129+
// Calling this function also mutates the tsconfig above,
130+
// adding in "include" and "exclude", but the compilerOptions remain untouched
131+
let result;
132+
parsedTsConfig = immer(readTsConfig, config => {
133+
result = ts.parseJsonConfigFileContent(
134+
config,
135+
ts.sys,
136+
path.dirname(paths.appTsConfig)
137+
);
138+
});
139+
140+
if (result.errors && result.errors.length) {
141+
throw new Error(
142+
ts.formatDiagnostic(result.errors[0], formatDiagnosticHost)
143+
);
144+
}
145+
146+
parsedCompilerOptions = result.options;
147+
} catch (e) {
103148
console.error(
104149
chalk.red.bold(
105150
'Could not parse',
106151
chalk.cyan('tsconfig.json') + '.',
107152
'Please make sure it contains syntactically correct JSON.'
108153
)
109154
);
155+
console.error(e && e.message ? `Details: ${e.message}` : '');
110156
process.exit(1);
111157
}
112158

113-
if (tsconfig.compilerOptions == null) {
114-
tsconfig.compilerOptions = {};
159+
if (appTsConfig.compilerOptions == null) {
160+
appTsConfig.compilerOptions = {};
115161
firstTimeSetup = true;
116162
}
117163

118164
for (const option of Object.keys(compilerOptions)) {
119-
const { value, suggested, reason } = compilerOptions[option];
165+
const { parsedValue, value, suggested, reason } = compilerOptions[option];
166+
167+
const valueToCheck = parsedValue === undefined ? value : parsedValue;
168+
120169
if (suggested != null) {
121-
if (tsconfig.compilerOptions[option] === undefined) {
122-
tsconfig.compilerOptions[option] = suggested;
170+
if (parsedCompilerOptions[option] === undefined) {
171+
appTsConfig.compilerOptions[option] = suggested;
123172
messages.push(
124173
`${chalk.cyan('compilerOptions.' + option)} to be ${chalk.bold(
125174
'suggested'
126175
)} value: ${chalk.cyan.bold(suggested)} (this can be changed)`
127176
);
128177
}
129-
} else if (tsconfig.compilerOptions[option] !== value) {
130-
tsconfig.compilerOptions[option] = value;
178+
} else if (parsedCompilerOptions[option] !== valueToCheck) {
179+
appTsConfig.compilerOptions[option] = value;
131180
messages.push(
132181
`${chalk.cyan('compilerOptions.' + option)} ${chalk.bold(
133182
'must'
@@ -137,14 +186,15 @@ function verifyTypeScriptSetup() {
137186
}
138187
}
139188

140-
if (tsconfig.include == null) {
141-
tsconfig.include = ['src'];
189+
// tsconfig will have the merged "include" and "exclude" by this point
190+
if (parsedTsConfig.include == null) {
191+
appTsConfig.include = ['src'];
142192
messages.push(
143193
`${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`
144194
);
145195
}
146-
if (tsconfig.exclude == null) {
147-
tsconfig.exclude = ['**/__tests__/**', '**/?*test.*', '**/?*spec.*'];
196+
if (parsedTsConfig.exclude == null) {
197+
appTsConfig.exclude = ['**/__tests__/**', '**/?*test.*', '**/?*spec.*'];
148198
messages.push(`${chalk.cyan('exclude')} should exclude test files`);
149199
}
150200

@@ -171,7 +221,7 @@ function verifyTypeScriptSetup() {
171221
});
172222
console.warn();
173223
}
174-
writeJson(paths.appTsConfig, tsconfig);
224+
writeJson(paths.appTsConfig, appTsConfig);
175225
}
176226

177227
// Copy type declarations associated with this version of `react-scripts`

0 commit comments

Comments
 (0)