From 0fc95dd1d125c520e3bf01627b771edfe3102098 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Mon, 22 Oct 2018 08:15:41 -0400 Subject: [PATCH] Validate tsconfig when using TypeScript (#5524) * Sanity check TypeScript config * Check more options * Set all defaults and suggestions * Update docs * Update doc notes * Automatically copy react app declared types to project on start * Remove note about loaders.d.ts --- docusaurus/docs/adding-typescript.md | 26 +---- .../loaders.d.ts => config/react-app.d.ts} | 4 + packages/react-scripts/scripts/test.js | 2 + .../scripts/utils/verifyTypeScriptSetup.js | 106 ++++++++++++++++++ 4 files changed, 114 insertions(+), 24 deletions(-) rename packages/react-scripts/{template/src/loaders.d.ts => config/react-app.d.ts} (83%) diff --git a/docusaurus/docs/adding-typescript.md b/docusaurus/docs/adding-typescript.md index 2e0b6a3c974..bf99e677b05 100644 --- a/docusaurus/docs/adding-typescript.md +++ b/docusaurus/docs/adding-typescript.md @@ -11,30 +11,8 @@ To add TypeScript to a Create React App project, follow these steps: 1. Run `npm install --save typescript @types/react @types/react-dom @types/jest` (or `yarn add typescript @types/react @types/react-dom @types/jest`). 2. Rename the `.js` files you want to convert: use `.tsx` if they use JSX or `.ts` if not (e.g. `git mv src/index.js src/index.tsx`). - -3. Create a [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) at the root directory with the following content: - -```json -{ - "compilerOptions": { - "target": "es5", - "module": "esnext", - "moduleResolution": "node", - "lib": ["esnext", "dom", "dom.iterable"], - "allowJs": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "isolatedModules": true, - "jsx": "preserve", - "noEmit": true, - "skipLibCheck": true, - "strict": true - }, - "include": ["src"] -} -``` - -4. Copy [loaders.d.ts](https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/src/loaders.d.ts) from the template to your `src` directory. +3. Create a [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html) at the root directory with `{}` in it. +4. Restart your development server (if applicable). This will set sensible defaults and the required values in your [`tsconfig.json` file](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html). Type errors will show up in the same console as the build one. diff --git a/packages/react-scripts/template/src/loaders.d.ts b/packages/react-scripts/config/react-app.d.ts similarity index 83% rename from packages/react-scripts/template/src/loaders.d.ts rename to packages/react-scripts/config/react-app.d.ts index a683144ed66..5994114ffda 100644 --- a/packages/react-scripts/template/src/loaders.d.ts +++ b/packages/react-scripts/config/react-app.d.ts @@ -1,3 +1,7 @@ +// @remove-file-on-eject +// Do not edit this file. It's replaced every time you launch a toolbox action. +// If you need to add additional declarations, please do so in a new file. + declare module '*.json' { const value: any; export default value; diff --git a/packages/react-scripts/scripts/test.js b/packages/react-scripts/scripts/test.js index 9102960f2cd..ac07e4792af 100644 --- a/packages/react-scripts/scripts/test.js +++ b/packages/react-scripts/scripts/test.js @@ -28,6 +28,8 @@ const verifyPackageTree = require('./utils/verifyPackageTree'); if (process.env.SKIP_PREFLIGHT_CHECK !== 'true') { verifyPackageTree(); } +const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup'); +verifyTypeScriptSetup(); // @remove-on-eject-end const jest = require('jest'); diff --git a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js index 4b16a0b5a48..1698ef5ecbd 100644 --- a/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js +++ b/packages/react-scripts/scripts/utils/verifyTypeScriptSetup.js @@ -11,7 +11,13 @@ const chalk = require('chalk'); const fs = require('fs'); const resolve = require('resolve'); +const path = require('path'); const paths = require('../../config/paths'); +const os = require('os'); + +function writeJson(fileName, object) { + fs.writeFileSync(fileName, JSON.stringify(object, null, 2) + os.EOL); +} function verifyTypeScriptSetup() { if (!fs.existsSync(paths.appTsConfig)) { @@ -53,6 +59,106 @@ function verifyTypeScriptSetup() { console.error(); process.exit(1); } + + const messages = []; + let tsconfig; + try { + tsconfig = require(paths.appTsConfig); + } catch (_) { + console.error( + chalk.red.bold( + 'Could not parse', + chalk.cyan('tsconfig.json') + '.', + 'Please make sure it contains syntactically correct JSON.' + ) + ); + process.exit(1); + } + + if (tsconfig.compilerOptions == null) { + tsconfig.compilerOptions = {}; + } + + const compilerOptions = { + target: { suggested: 'es5' }, + allowJs: { suggested: true }, + skipLibCheck: { suggested: true }, + module: { value: 'esnext', reason: 'for import() and import/export' }, + moduleResolution: { value: 'node', reason: 'to match webpack resolution' }, + isolatedModules: { value: true, reason: 'implementation limitation' }, + noEmit: { value: true }, + jsx: { value: 'preserve', reason: 'JSX is compiled by Babel' }, + esModuleInterop: { value: true, reason: 'Babel compatibility' }, + allowSyntheticDefaultImports: { + value: true, + reason: 'Babel compatibility', + }, + strict: { suggested: true }, + }; + + for (const option of Object.keys(compilerOptions)) { + const { value, suggested, reason } = compilerOptions[option]; + if (suggested != null) { + if (tsconfig.compilerOptions[option] === undefined) { + tsconfig.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; + messages.push( + `${chalk.cyan('compilerOptions.' + option)} ${chalk.bold( + 'must' + )} be ${chalk.cyan.bold(value)}` + + (reason != null ? ` (${reason})` : '') + ); + } + } + + if (tsconfig.include == null) { + tsconfig.include = ['src']; + messages.push( + `${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}` + ); + } + if (tsconfig.exclude == null) { + tsconfig.exclude = ['**/__tests__/**', '**/?*(spec|test).*']; + messages.push(`${chalk.cyan('exclude')} should exclude test files`); + } + + if (messages.length > 0) { + console.warn( + chalk.bold( + 'The following changes are being made to your', + chalk.cyan('tsconfig.json'), + 'file:' + ) + ); + messages.forEach(message => { + console.warn(' - ' + message); + }); + console.warn(); + writeJson(paths.appTsConfig, tsconfig); + } + + // Copy type declarations associated with this version of `react-scripts` + const declaredTypes = path.resolve( + __dirname, + '..', + '..', + 'config', + 'react-app.d.ts' + ); + const declaredTypesContent = fs + .readFileSync(declaredTypes, 'utf8') + .replace(/\/\/ @remove-file-on-eject\r?\n/, ''); + fs.writeFileSync( + path.resolve(paths.appSrc, 'react-app.d.ts'), + declaredTypesContent + ); } module.exports = verifyTypeScriptSetup;