UNPKG

7.45 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.React,
129 suggested: 'react',
130 },
131 paths: { value: undefined, reason: 'aliased imports are not supported' },
132 };
133
134 const formatDiagnosticHost = {
135 getCanonicalFileName: fileName => fileName,
136 getCurrentDirectory: ts.sys.getCurrentDirectory,
137 getNewLine: () => os.EOL,
138 };
139
140 const messages = [];
141 let appTsConfig;
142 let parsedTsConfig;
143 let parsedCompilerOptions;
144 try {
145 const { config: readTsConfig, error } = ts.readConfigFile(
146 paths.appTsConfig,
147 ts.sys.readFile
148 );
149
150 if (error) {
151 throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost));
152 }
153
154 appTsConfig = readTsConfig;
155
156 // Get TS to parse and resolve any "extends"
157 // Calling this function also mutates the tsconfig above,
158 // adding in "include" and "exclude", but the compilerOptions remain untouched
159 let result;
160 parsedTsConfig = immer(readTsConfig, config => {
161 result = ts.parseJsonConfigFileContent(
162 config,
163 ts.sys,
164 path.dirname(paths.appTsConfig)
165 );
166 });
167
168 if (result.errors && result.errors.length) {
169 throw new Error(
170 ts.formatDiagnostic(result.errors[0], formatDiagnosticHost)
171 );
172 }
173
174 parsedCompilerOptions = result.options;
175 } catch (e) {
176 if (e && e.name === 'SyntaxError') {
177 console.error(
178 chalk.red.bold(
179 'Could not parse',
180 chalk.cyan('tsconfig.json') + '.',
181 'Please make sure it contains syntactically correct JSON.'
182 )
183 );
184 }
185
186 console.log(e && e.message ? `${e.message}` : '');
187 process.exit(1);
188 }
189
190 if (appTsConfig.compilerOptions == null) {
191 appTsConfig.compilerOptions = {};
192 firstTimeSetup = true;
193 }
194
195 for (const option of Object.keys(compilerOptions)) {
196 const { parsedValue, value, suggested, reason } = compilerOptions[option];
197
198 const valueToCheck = parsedValue === undefined ? value : parsedValue;
199 const coloredOption = chalk.cyan('compilerOptions.' + option);
200
201 if (suggested != null) {
202 if (parsedCompilerOptions[option] === undefined) {
203 appTsConfig.compilerOptions[option] = suggested;
204 messages.push(
205 `${coloredOption} to be ${chalk.bold(
206 'suggested'
207 )} value: ${chalk.cyan.bold(suggested)} (this can be changed)`
208 );
209 }
210 } else if (parsedCompilerOptions[option] !== valueToCheck) {
211 appTsConfig.compilerOptions[option] = value;
212 messages.push(
213 `${coloredOption} ${chalk.bold(
214 valueToCheck == null ? 'must not' : 'must'
215 )} be ${valueToCheck == null ? 'set' : chalk.cyan.bold(value)}` +
216 (reason != null ? ` (${reason})` : '')
217 );
218 }
219 }
220
221 // tsconfig will have the merged "include" and "exclude" by this point
222 if (parsedTsConfig.include == null) {
223 appTsConfig.include = ['src'];
224 messages.push(
225 `${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`
226 );
227 }
228
229 if (messages.length > 0) {
230 if (firstTimeSetup) {
231 console.log(
232 chalk.bold(
233 'Your',
234 chalk.cyan('tsconfig.json'),
235 'has been populated with default values.'
236 )
237 );
238 console.log();
239 } else {
240 console.warn(
241 chalk.bold(
242 'The following changes are being made to your',
243 chalk.cyan('tsconfig.json'),
244 'file:'
245 )
246 );
247 messages.forEach(message => {
248 console.warn(' - ' + message);
249 });
250 console.warn();
251 }
252 writeJson(paths.appTsConfig, appTsConfig);
253 }
254
255 // Reference `react-scripts` types
256 if (!fs.existsSync(paths.appTypeDeclarations)) {
257 fs.writeFileSync(
258 paths.appTypeDeclarations,
259 `/// <reference types="react-scripts" />${os.EOL}`
260 );
261 }
262}
263
264module.exports = verifyTypeScriptSetup;