From f6a123545f67f1bdecef96e27334cecdb3c215f1 Mon Sep 17 00:00:00 2001 From: Ian Schmitz Date: Mon, 22 Oct 2018 20:31:23 -0700 Subject: [PATCH 1/3] Use TS to resolve tsconfig extends --- .../scripts/utils/verifyTypeScriptSetup.js | 80 ++++++++++++++----- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index 2205d56634e..1b56b729e35 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -19,25 +19,6 @@ 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; @@ -86,8 +67,44 @@ 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 messages = []; let tsconfig; + let parsedOptions; try { const { config, error } = ts.readConfigFile( paths.appTsConfig, @@ -99,6 +116,21 @@ function verifyTypeScriptSetup() { } tsconfig = config; + + // 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 + const result = ts.parseJsonConfigFileContent( + config, + ts.sys, + path.dirname(paths.appTsConfig) + ); + + if (result.errors && result.errors.length) { + throw result.errors[0]; + } + + parsedOptions = result.options; } catch (_) { console.error( chalk.red.bold( @@ -116,9 +148,12 @@ function verifyTypeScriptSetup() { } 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) { + if (parsedOptions[option] === undefined) { tsconfig.compilerOptions[option] = suggested; messages.push( `${chalk.cyan('compilerOptions.' + option)} to be ${chalk.bold( @@ -126,7 +161,7 @@ function verifyTypeScriptSetup() { )} value: ${chalk.cyan.bold(suggested)} (this can be changed)` ); } - } else if (tsconfig.compilerOptions[option] !== value) { + } else if (parsedOptions[option] !== valueToCheck) { tsconfig.compilerOptions[option] = value; messages.push( `${chalk.cyan('compilerOptions.' + option)} ${chalk.bold( @@ -137,6 +172,7 @@ function verifyTypeScriptSetup() { } } + // tsconfig will have the merged "include" and "exclude" by this point if (tsconfig.include == null) { tsconfig.include = ['src']; messages.push( From 1feaa5cdb376ec73c4de4fe5ab55b085f2ec934e Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 Oct 2018 15:29:39 -0400 Subject: [PATCH 2/3] Prevent modifications to original tsconfig --- packages/react-dev-utils/immer.js | 12 +++++ packages/react-dev-utils/package.json | 2 + .../scripts/utils/verifyTypeScriptSetup.js | 47 ++++++++++--------- 3 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 packages/react-dev-utils/immer.js diff --git a/packages/react-dev-utils/immer.js b/packages/react-dev-utils/immer.js new file mode 100644 index 00000000000..ab019eef891 --- /dev/null +++ b/packages/react-dev-utils/immer.js @@ -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; diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index d7bb290de2c..7c3748af901 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -24,6 +24,7 @@ "getCSSModuleLocalIdent.js", "getProcessForPort.js", "ignoredFiles.js", + "immer.js", "InlineChunkHtmlPlugin.js", "inquirer.js", "InterpolateHtmlPlugin.js", @@ -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", diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index 1b56b729e35..fccf657c9d6 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -14,6 +14,7 @@ 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); @@ -103,10 +104,11 @@ function verifyTypeScriptSetup() { }; const messages = []; - let tsconfig; - let parsedOptions; + let appTsConfig; + let parsedTsConfig; + let parsedCompilerOptions; try { - const { config, error } = ts.readConfigFile( + const { config: readTsConfig, error } = ts.readConfigFile( paths.appTsConfig, ts.sys.readFile ); @@ -115,22 +117,25 @@ function verifyTypeScriptSetup() { throw error; } - tsconfig = config; + 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 - const result = ts.parseJsonConfigFileContent( - config, - ts.sys, - path.dirname(paths.appTsConfig) - ); + let result; + parsedTsConfig = immer(readTsConfig, config => { + result = ts.parseJsonConfigFileContent( + config, + ts.sys, + path.dirname(paths.appTsConfig) + ); + }); if (result.errors && result.errors.length) { throw result.errors[0]; } - parsedOptions = result.options; + parsedCompilerOptions = result.options; } catch (_) { console.error( chalk.red.bold( @@ -142,8 +147,8 @@ function verifyTypeScriptSetup() { process.exit(1); } - if (tsconfig.compilerOptions == null) { - tsconfig.compilerOptions = {}; + if (appTsConfig.compilerOptions == null) { + appTsConfig.compilerOptions = {}; firstTimeSetup = true; } @@ -153,16 +158,16 @@ function verifyTypeScriptSetup() { const valueToCheck = parsedValue === undefined ? value : parsedValue; if (suggested != null) { - if (parsedOptions[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 (parsedOptions[option] !== valueToCheck) { - tsconfig.compilerOptions[option] = value; + } else if (parsedCompilerOptions[option] !== valueToCheck) { + appTsConfig.compilerOptions[option] = value; messages.push( `${chalk.cyan('compilerOptions.' + option)} ${chalk.bold( 'must' @@ -173,14 +178,14 @@ function verifyTypeScriptSetup() { } // tsconfig will have the merged "include" and "exclude" by this point - if (tsconfig.include == null) { - tsconfig.include = ['src']; + 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`); } @@ -207,7 +212,7 @@ function verifyTypeScriptSetup() { }); console.warn(); } - writeJson(paths.appTsConfig, tsconfig); + writeJson(paths.appTsConfig, appTsConfig); } // Copy type declarations associated with this version of `react-scripts` From 38bd6342ac135b94683b7689db392bca112bc66a Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Tue, 23 Oct 2018 15:32:59 -0400 Subject: [PATCH 3/3] Print friendly error --- .../scripts/utils/verifyTypeScriptSetup.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index fccf657c9d6..50491825da3 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -103,6 +103,12 @@ function verifyTypeScriptSetup() { }, }; + const formatDiagnosticHost = { + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => os.EOL, + }; + const messages = []; let appTsConfig; let parsedTsConfig; @@ -114,7 +120,7 @@ function verifyTypeScriptSetup() { ); if (error) { - throw error; + throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost)); } appTsConfig = readTsConfig; @@ -132,11 +138,13 @@ function verifyTypeScriptSetup() { }); if (result.errors && result.errors.length) { - throw result.errors[0]; + throw new Error( + ts.formatDiagnostic(result.errors[0], formatDiagnosticHost) + ); } parsedCompilerOptions = result.options; - } catch (_) { + } catch (e) { console.error( chalk.red.bold( 'Could not parse', @@ -144,6 +152,7 @@ function verifyTypeScriptSetup() { 'Please make sure it contains syntactically correct JSON.' ) ); + console.error(e && e.message ? `Details: ${e.message}` : ''); process.exit(1); }