UNPKG

7.5 kBJavaScriptView Raw
1// @remove-file-on-eject
2/**
3 * Copyright (c) 2015-present, Facebook, Inc.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9'use strict';
10
11const chalk = require('react-dev-utils/chalk');
12const fs = require('fs');
13const resolve = require('resolve');
14const path = require('path');
15const paths = require('../../config/paths');
16const os = require('os');
17const immer = require('react-dev-utils/immer').produce;
18const globby = require('react-dev-utils/globby').sync;
19
20function writeJson(fileName, object) {
21 fs.writeFileSync(
22 fileName,
23 JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL
24 );
25}
26
27function verifyNoTypeScript() {
28 const typescriptFiles = globby(
29 ['**/*.(ts|tsx)', '!**/node_modules', '!**/*.d.ts'],
30 { cwd: paths.appSrc }
31 );
32 if (typescriptFiles.length > 0) {
33 console.warn(
34 chalk.yellow(
35 `We detected TypeScript in your project (${chalk.bold(
36 `src${path.sep}${typescriptFiles[0]}`
37 )}) and created a ${chalk.bold('tsconfig.json')} file for you.`
38 )
39 );
40 console.warn();
41 return false;
42 }
43 return true;
44}
45
46function verifyTypeScriptSetup() {
47 let firstTimeSetup = false;
48
49 if (!fs.existsSync(paths.appTsConfig)) {
50 if (verifyNoTypeScript()) {
51 return;
52 }
53 writeJson(paths.appTsConfig, {});
54 firstTimeSetup = true;
55 }
56
57 const isYarn = fs.existsSync(paths.yarnLockFile);
58
59 // Ensure typescript is installed
60 let ts;
61 try {
62 ts = require(resolve.sync('typescript', {
63 basedir: paths.appNodeModules,
64 }));
65 } catch (_) {
66 console.error(
67 chalk.bold.red(
68 `It looks like you're trying to use TypeScript but do not have ${chalk.bold(
69 'typescript'
70 )} installed.`
71 )
72 );
73 console.error(
74 chalk.bold(
75 'Please install',
76 chalk.cyan.bold('typescript'),
77 'by running',
78 chalk.cyan.bold(
79 isYarn ? 'yarn add typescript' : 'npm install typescript'
80 ) + '.'
81 )
82 );
83 console.error(
84 chalk.bold(
85 'If you are not trying to use TypeScript, please remove the ' +
86 chalk.cyan('tsconfig.json') +
87 ' file from your package root (and any TypeScript files).'
88 )
89 );
90 console.error();
91 process.exit(1);
92 }
93
94 const compilerOptions = {
95 // These are suggested values and will be set when not present in the
96 // tsconfig.json
97 // 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
98 target: {
99 parsedValue: ts.ScriptTarget.ES5,
100 suggested: 'es5',
101 },
102 lib: { suggested: ['dom', 'dom.iterable', 'esnext'] },
103 allowJs: { suggested: true },
104 skipLibCheck: { suggested: true },
105 esModuleInterop: { suggested: true },
106 allowSyntheticDefaultImports: { suggested: true },
107 strict: { suggested: true },
108 forceConsistentCasingInFileNames: { suggested: true },
109 // TODO: Enable for v4.0 (#6936)
110 // noFallthroughCasesInSwitch: { suggested: true },
111
112 // These values are required and cannot be changed by the user
113 // Keep this in sync with the webpack config
114 module: {
115 parsedValue: ts.ModuleKind.ESNext,
116 value: 'esnext',
117 reason: 'for import() and import/export',
118 },
119 moduleResolution: {
120 parsedValue: ts.ModuleResolutionKind.NodeJs,
121 value: 'node',
122 reason: 'to match webpack resolution',
123 },
124 resolveJsonModule: { value: true, reason: 'to match webpack loader' },
125 isolatedModules: { value: true, reason: 'implementation limitation' },
126 noEmit: { value: true },
127 jsx: {
128 parsedValue: ts.JsxEmit.Preserve,
129 value: 'preserve',
130 reason: 'JSX is compiled by Babel',
131 },
132 paths: { value: undefined, reason: 'aliased imports are not supported' },
133 };
134
135 const formatDiagnosticHost = {
136 getCanonicalFileName: fileName => fileName,
137 getCurrentDirectory: ts.sys.getCurrentDirectory,
138 getNewLine: () => os.EOL,
139 };
140
141 const messages = [];
142 let appTsConfig;
143 let parsedTsConfig;
144 let parsedCompilerOptions;
145 try {
146 const { config: readTsConfig, error } = ts.readConfigFile(
147 paths.appTsConfig,
148 ts.sys.readFile
149 );
150
151 if (error) {
152 throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost));
153 }
154
155 appTsConfig = readTsConfig;
156
157 // Get TS to parse and resolve any "extends"
158 // Calling this function also mutates the tsconfig above,
159 // adding in "include" and "exclude", but the compilerOptions remain untouched
160 let result;
161 parsedTsConfig = immer(readTsConfig, config => {
162 result = ts.parseJsonConfigFileContent(
163 config,
164 ts.sys,
165 path.dirname(paths.appTsConfig)
166 );
167 });
168
169 if (result.errors && result.errors.length) {
170 throw new Error(
171 ts.formatDiagnostic(result.errors[0], formatDiagnosticHost)
172 );
173 }
174
175 parsedCompilerOptions = result.options;
176 } catch (e) {
177 if (e && e.name === 'SyntaxError') {
178 console.error(
179 chalk.red.bold(
180 'Could not parse',
181 chalk.cyan('tsconfig.json') + '.',
182 'Please make sure it contains syntactically correct JSON.'
183 )
184 );
185 }
186
187 console.log(e && e.message ? `${e.message}` : '');
188 process.exit(1);
189 }
190
191 if (appTsConfig.compilerOptions == null) {
192 appTsConfig.compilerOptions = {};
193 firstTimeSetup = true;
194 }
195
196 for (const option of Object.keys(compilerOptions)) {
197 const { parsedValue, value, suggested, reason } = compilerOptions[option];
198
199 const valueToCheck = parsedValue === undefined ? value : parsedValue;
200 const coloredOption = chalk.cyan('compilerOptions.' + option);
201
202 if (suggested != null) {
203 if (parsedCompilerOptions[option] === undefined) {
204 appTsConfig.compilerOptions[option] = suggested;
205 messages.push(
206 `${coloredOption} to be ${chalk.bold(
207 'suggested'
208 )} value: ${chalk.cyan.bold(suggested)} (this can be changed)`
209 );
210 }
211 } else if (parsedCompilerOptions[option] !== valueToCheck) {
212 appTsConfig.compilerOptions[option] = value;
213 messages.push(
214 `${coloredOption} ${chalk.bold(
215 valueToCheck == null ? 'must not' : 'must'
216 )} be ${valueToCheck == null ? 'set' : chalk.cyan.bold(value)}` +
217 (reason != null ? ` (${reason})` : '')
218 );
219 }
220 }
221
222 // tsconfig will have the merged "include" and "exclude" by this point
223 if (parsedTsConfig.include == null) {
224 appTsConfig.include = ['src'];
225 messages.push(
226 `${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`
227 );
228 }
229
230 if (messages.length > 0) {
231 if (firstTimeSetup) {
232 console.log(
233 chalk.bold(
234 'Your',
235 chalk.cyan('tsconfig.json'),
236 'has been populated with default values.'
237 )
238 );
239 console.log();
240 } else {
241 console.warn(
242 chalk.bold(
243 'The following changes are being made to your',
244 chalk.cyan('tsconfig.json'),
245 'file:'
246 )
247 );
248 messages.forEach(message => {
249 console.warn(' - ' + message);
250 });
251 console.warn();
252 }
253 writeJson(paths.appTsConfig, appTsConfig);
254 }
255
256 // Reference `react-scripts` types
257 if (!fs.existsSync(paths.appTypeDeclarations)) {
258 fs.writeFileSync(
259 paths.appTypeDeclarations,
260 `/// <reference types="react-scripts" />${os.EOL}`
261 );
262 }
263}
264
265module.exports = verifyTypeScriptSetup;