Skip to content

Commit

Permalink
Respect tsconfig.json extends when validating config (facebook#5537)
Browse files Browse the repository at this point in the history
* Use TS to resolve tsconfig extends

* Prevent modifications to original tsconfig

* Print friendly error
  • Loading branch information
ianschmitz authored and nate770 committed Oct 30, 2018
1 parent d1fa628 commit b20f4a3
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 36 deletions.
12 changes: 12 additions & 0 deletions packages/react-dev-utils/immer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var immer = require('immer');

module.exports = immer;
2 changes: 2 additions & 0 deletions packages/react-dev-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"getCSSModuleLocalIdent.js",
"getProcessForPort.js",
"ignoredFiles.js",
"immer.js",
"InlineChunkHtmlPlugin.js",
"inquirer.js",
"InterpolateHtmlPlugin.js",
Expand Down Expand Up @@ -53,6 +54,7 @@
"find-up": "3.0.0",
"global-modules": "1.0.0",
"gzip-size": "5.0.0",
"immer": "1.7.2",
"inquirer": "6.2.0",
"is-root": "2.0.0",
"loader-utils": "1.1.0",
Expand Down
122 changes: 86 additions & 36 deletions packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,12 @@ const resolve = require('resolve');
const path = require('path');
const paths = require('../../config/paths');
const os = require('os');
const immer = require('react-dev-utils/immer').produce;

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

const compilerOptions = {
// These are suggested values and will be set when not present in the
// tsconfig.json
target: { suggested: 'es5' },
allowJs: { suggested: true },
skipLibCheck: { suggested: true },
esModuleInterop: { suggested: true },
allowSyntheticDefaultImports: { suggested: true },
strict: { suggested: true },

// These values are required and cannot be changed by the user
module: { value: 'esnext', reason: 'for import() and import/export' },
moduleResolution: { value: 'node', reason: 'to match webpack resolution' },
resolveJsonModule: { value: true, reason: 'to match webpack loader' },
isolatedModules: { value: true, reason: 'implementation limitation' },
noEmit: { value: true },
jsx: { value: 'preserve', reason: 'JSX is compiled by Babel' },
};

function verifyTypeScriptSetup() {
let firstTimeSetup = false;

Expand Down Expand Up @@ -86,48 +68,115 @@ function verifyTypeScriptSetup() {
process.exit(1);
}

const compilerOptions = {
// These are suggested values and will be set when not present in the
// tsconfig.json
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
target: {
parsedValue: ts.ScriptTarget.ES5,
suggested: 'es5',
},
allowJs: { suggested: true },
skipLibCheck: { suggested: true },
esModuleInterop: { suggested: true },
allowSyntheticDefaultImports: { suggested: true },
strict: { suggested: true },

// These values are required and cannot be changed by the user
module: {
parsedValue: ts.ModuleKind.ESNext,
value: 'esnext',
reason: 'for import() and import/export',
},
moduleResolution: {
parsedValue: ts.ModuleResolutionKind.NodeJs,
value: 'node',
reason: 'to match webpack resolution',
},
resolveJsonModule: { value: true, reason: 'to match webpack loader' },
isolatedModules: { value: true, reason: 'implementation limitation' },
noEmit: { value: true },
jsx: {
parsedValue: ts.JsxEmit.Preserve,
value: 'preserve',
reason: 'JSX is compiled by Babel',
},
};

const formatDiagnosticHost = {
getCanonicalFileName: fileName => fileName,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => os.EOL,
};

const messages = [];
let tsconfig;
let appTsConfig;
let parsedTsConfig;
let parsedCompilerOptions;
try {
const { config, error } = ts.readConfigFile(
const { config: readTsConfig, error } = ts.readConfigFile(
paths.appTsConfig,
ts.sys.readFile
);

if (error) {
throw error;
throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost));
}

tsconfig = config;
} catch (_) {
appTsConfig = readTsConfig;

// Get TS to parse and resolve any "extends"
// Calling this function also mutates the tsconfig above,
// adding in "include" and "exclude", but the compilerOptions remain untouched
let result;
parsedTsConfig = immer(readTsConfig, config => {
result = ts.parseJsonConfigFileContent(
config,
ts.sys,
path.dirname(paths.appTsConfig)
);
});

if (result.errors && result.errors.length) {
throw new Error(
ts.formatDiagnostic(result.errors[0], formatDiagnosticHost)
);
}

parsedCompilerOptions = result.options;
} catch (e) {
console.error(
chalk.red.bold(
'Could not parse',
chalk.cyan('tsconfig.json') + '.',
'Please make sure it contains syntactically correct JSON.'
)
);
console.error(e && e.message ? `Details: ${e.message}` : '');
process.exit(1);
}

if (tsconfig.compilerOptions == null) {
tsconfig.compilerOptions = {};
if (appTsConfig.compilerOptions == null) {
appTsConfig.compilerOptions = {};
firstTimeSetup = true;
}

for (const option of Object.keys(compilerOptions)) {
const { value, suggested, reason } = compilerOptions[option];
const { parsedValue, value, suggested, reason } = compilerOptions[option];

const valueToCheck = parsedValue === undefined ? value : parsedValue;

if (suggested != null) {
if (tsconfig.compilerOptions[option] === undefined) {
tsconfig.compilerOptions[option] = suggested;
if (parsedCompilerOptions[option] === undefined) {
appTsConfig.compilerOptions[option] = suggested;
messages.push(
`${chalk.cyan('compilerOptions.' + option)} to be ${chalk.bold(
'suggested'
)} value: ${chalk.cyan.bold(suggested)} (this can be changed)`
);
}
} else if (tsconfig.compilerOptions[option] !== value) {
tsconfig.compilerOptions[option] = value;
} else if (parsedCompilerOptions[option] !== valueToCheck) {
appTsConfig.compilerOptions[option] = value;
messages.push(
`${chalk.cyan('compilerOptions.' + option)} ${chalk.bold(
'must'
Expand All @@ -137,14 +186,15 @@ function verifyTypeScriptSetup() {
}
}

if (tsconfig.include == null) {
tsconfig.include = ['src'];
// tsconfig will have the merged "include" and "exclude" by this point
if (parsedTsConfig.include == null) {
appTsConfig.include = ['src'];
messages.push(
`${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`
);
}
if (tsconfig.exclude == null) {
tsconfig.exclude = ['**/__tests__/**', '**/?*test.*', '**/?*spec.*'];
if (parsedTsConfig.exclude == null) {
appTsConfig.exclude = ['**/__tests__/**', '**/?*test.*', '**/?*spec.*'];
messages.push(`${chalk.cyan('exclude')} should exclude test files`);
}

Expand All @@ -171,7 +221,7 @@ function verifyTypeScriptSetup() {
});
console.warn();
}
writeJson(paths.appTsConfig, tsconfig);
writeJson(paths.appTsConfig, appTsConfig);
}

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

0 comments on commit b20f4a3

Please sign in to comment.