@sabertazimi/react-scripts
Version:
Configuration and scripts for Bod CLI.
300 lines (270 loc) • 8.36 kB
JavaScript
// @remove-file-on-eject
/**
* 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.
*/
const fs = require('node:fs')
const path = require('node:path')
const paths = require('../../config/paths')
const os = require('node:os')
const resolve = require('resolve')
const chalk = require('react-dev-utils/chalk')
const semver = require('semver')
const immer = require('react-dev-utils/immer').produce
const globby = require('react-dev-utils/globby').sync
const hasJsxRuntime = (() => {
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
return false
}
try {
require.resolve('react/jsx-runtime', { paths: [paths.appPath] })
return true
} catch (e) {
return false
}
})()
function writeJson(fileName, object) {
fs.writeFileSync(
fileName,
JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL
)
}
function verifyNoTypeScript() {
const typescriptFiles = globby(
['**/*.(ts|tsx)', '!**/node_modules', '!**/*.d.ts'],
{ cwd: paths.appSrc }
)
if (typescriptFiles.length > 0) {
console.warn(
chalk.yellow(
`We detected TypeScript in your project (${chalk.bold(
`src${path.sep}${typescriptFiles[0]}`
)}) and created a ${chalk.bold('tsconfig.json')} file for you.`
)
)
console.warn()
return false
}
return true
}
function verifyTypeScriptSetup() {
let firstTimeSetup = false
if (!fs.existsSync(paths.appTsConfig)) {
if (verifyNoTypeScript()) {
return
}
writeJson(paths.appTsConfig, {})
firstTimeSetup = true
}
const isYarn = fs.existsSync(paths.yarnLockFile)
// Ensure typescript is installed
let ts
try {
// TODO: Remove this hack once `globalThis` issue is resolved
// https://github.com/jsdom/jsdom/issues/2961
const globalThisWasDefined = !!global.globalThis
ts = require(
resolve.sync('typescript', {
basedir: paths.appNodeModules,
})
)
if (!globalThisWasDefined && !!global.globalThis) {
delete global.globalThis
}
} catch (_) {
console.error(
chalk.bold.red(
`It looks like you're trying to use TypeScript but do not have ${chalk.bold(
'typescript'
)} installed.`
)
)
console.error(
chalk.bold(
'Please install',
chalk.cyan.bold('typescript'),
'by running',
`${chalk.cyan.bold(
isYarn ? 'yarn add typescript' : 'npm install typescript'
)}.`
)
)
console.error(
chalk.bold(
`If you are not trying to use TypeScript, please remove the ${chalk.cyan(
'tsconfig.json'
)} file from your package root (and any TypeScript files).`
)
)
console.error()
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.ES6,
suggested: 'es6',
},
lib: { suggested: ['dom', 'dom.iterable', 'esnext'] },
allowJs: { suggested: true },
skipLibCheck: { suggested: true },
esModuleInterop: { suggested: true },
allowSyntheticDefaultImports: { suggested: true },
strict: { suggested: true },
forceConsistentCasingInFileNames: { suggested: true },
noFallthroughCasesInSwitch: { suggested: true },
// These values are required and cannot be changed by the user
// Keep this in sync with the webpack config
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:
hasJsxRuntime && semver.gte(ts.version, '4.1.0-beta')
? ts.JsxEmit.ReactJSX
: ts.JsxEmit.React,
value:
hasJsxRuntime && semver.gte(ts.version, '4.1.0-beta')
? 'react-jsx'
: 'react',
reason: 'to support the new JSX transform in React 17',
},
paths: { value: undefined, reason: 'aliased imports are not supported' },
}
const formatDiagnosticHost = {
getCanonicalFileName: fileName => fileName,
getCurrentDirectory: ts.sys.getCurrentDirectory,
getNewLine: () => os.EOL,
}
const messages = []
let appTsConfig
let parsedTsConfig
let parsedCompilerOptions
try {
const { config: readTsConfig, error } = ts.readConfigFile(
paths.appTsConfig,
ts.sys.readFile
)
if (error) {
throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost))
}
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) {
if (e && e.name === 'SyntaxError') {
console.error(
chalk.red.bold(
'Could not parse',
`${chalk.cyan('tsconfig.json')}.`,
'Please make sure it contains syntactically correct JSON.'
)
)
}
console.log(e && e.message ? `${e.message}` : '')
process.exit(1)
}
if (appTsConfig.compilerOptions == null) {
appTsConfig.compilerOptions = {}
firstTimeSetup = true
}
for (const option of Object.keys(compilerOptions)) {
const { parsedValue, value, suggested, reason } = compilerOptions[option]
const valueToCheck = parsedValue === undefined ? value : parsedValue
const coloredOption = chalk.cyan(`compilerOptions.${option}`)
if (suggested != null) {
if (parsedCompilerOptions[option] === undefined) {
appTsConfig = immer(appTsConfig, config => {
config.compilerOptions[option] = suggested
})
messages.push(
`${coloredOption} to be ${chalk.bold(
'suggested'
)} value: ${chalk.cyan.bold(suggested)} (this can be changed)`
)
}
} else if (parsedCompilerOptions[option] !== valueToCheck) {
appTsConfig = immer(appTsConfig, config => {
config.compilerOptions[option] = value
})
messages.push(
`${coloredOption} ${chalk.bold(
valueToCheck == null ? 'must not' : 'must'
)} be ${valueToCheck == null ? 'set' : chalk.cyan.bold(value)}${
reason != null ? ` (${reason})` : ''
}`
)
}
}
// tsconfig will have the merged "include" and "exclude" by this point
if (parsedTsConfig.include == null) {
appTsConfig = immer(appTsConfig, config => {
config.include = ['src']
})
messages.push(
`${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`
)
}
if (messages.length > 0) {
if (firstTimeSetup) {
console.log(
chalk.bold(
'Your',
chalk.cyan('tsconfig.json'),
'has been populated with default values.'
)
)
console.log()
} else {
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, appTsConfig)
}
// Reference `react-scripts` types
if (!fs.existsSync(paths.appTypeDeclarations)) {
fs.writeFileSync(
paths.appTypeDeclarations,
`/// <reference types="@sabertazimi/react-scripts" />${os.EOL}`
)
}
}
module.exports = verifyTypeScriptSetup