next
Version:
The React Framework
326 lines (325 loc) • 15.8 kB
JavaScript
import { readFileSync, writeFileSync } from 'fs';
import { bold, cyan, white } from '../picocolors';
import * as CommentJson from 'next/dist/compiled/comment-json';
import semver from 'next/dist/compiled/semver';
import os from 'os';
import { getTypeDefinitionGlobPatterns } from './type-paths';
import * as Log from '../../build/output/log';
import { defaultConfig } from '../../server/config-shared';
function getDesiredCompilerOptions(typescriptVersion, userTsConfig) {
var _userTsConfig_compilerOptions_module, _userTsConfig_compilerOptions, _userTsConfig_compilerOptions1;
// ModuleKind
const moduleKindESNext = 'esnext';
const moduleKindES2020 = 'es2020';
const moduleKindPreserve = 'preserve';
const moduleKindNodeNext = 'nodenext';
const moduleKindNode16 = 'node16';
const moduleKindCommonJS = 'commonjs';
const moduleKindAMD = 'amd';
// ModuleResolutionKind
const moduleResolutionKindBundler = 'bundler';
const moduleResolutionKindNode10 = 'node10';
const moduleResolutionKindNode12 = 'node12';
const moduleResolutionKindNodeJs = 'node';
// Jsx
const jsxEmitReactJSX = 'react-jsx';
return {
target: {
suggested: 'ES2017',
reason: 'For top-level `await`. Note: Next.js only polyfills for the esmodules target.'
},
// These are suggested values and will be set when not present in the
// tsconfig.json
lib: {
suggested: [
'dom',
'dom.iterable',
'esnext'
]
},
allowJs: {
suggested: true
},
skipLibCheck: {
suggested: true
},
strict: {
suggested: false
},
noEmit: {
suggested: true
},
incremental: {
suggested: true
},
// These values are required and cannot be changed by the user
// Keep this in sync with the webpack config
// 'parsedValue' matches the output value from ts.parseJsonConfigFileContent()
module: {
parsedValue: moduleKindESNext,
// All of these values work:
parsedValues: [
semver.gte(typescriptVersion, '5.4.0') && moduleKindPreserve,
moduleKindES2020,
moduleKindESNext,
moduleKindCommonJS,
moduleKindAMD,
moduleKindNodeNext,
moduleKindNode16
],
value: 'esnext',
reason: 'for dynamic import() support'
},
// TODO: Semver check not needed once Next.js repo uses 5.4.
...semver.gte(typescriptVersion, '5.4.0') && (userTsConfig == null ? void 0 : (_userTsConfig_compilerOptions = userTsConfig.compilerOptions) == null ? void 0 : (_userTsConfig_compilerOptions_module = _userTsConfig_compilerOptions.module) == null ? void 0 : _userTsConfig_compilerOptions_module.toLowerCase()) === moduleKindPreserve ? {
} : {
esModuleInterop: {
value: true,
reason: 'requirement for SWC / babel'
},
moduleResolution: {
// In TypeScript 5.0, `NodeJs` has renamed to `Node10`
parsedValue: moduleResolutionKindBundler,
// All of these values work:
parsedValues: [
moduleResolutionKindNode10,
moduleResolutionKindNodeJs,
// only newer TypeScript versions have this field, it
// will be filtered for new versions of TypeScript
moduleResolutionKindNode12,
moduleKindNode16,
moduleKindNodeNext,
moduleResolutionKindBundler
].filter((val)=>typeof val !== 'undefined'),
value: 'node',
reason: 'to match webpack resolution'
},
resolveJsonModule: {
value: true,
reason: 'to match webpack resolution'
}
},
...(userTsConfig == null ? void 0 : (_userTsConfig_compilerOptions1 = userTsConfig.compilerOptions) == null ? void 0 : _userTsConfig_compilerOptions1.verbatimModuleSyntax) === true ? undefined : {
isolatedModules: {
value: true,
reason: 'requirement for SWC / Babel'
}
},
jsx: {
parsedValue: jsxEmitReactJSX,
value: 'react-jsx',
reason: 'next.js uses the React automatic runtime'
}
};
}
export function getRequiredConfiguration(typescript) {
const res = {};
const typescriptVersion = typescript.version;
const desiredCompilerOptions = getDesiredCompilerOptions(typescriptVersion);
for (const optionKey of Object.keys(desiredCompilerOptions)){
const ev = desiredCompilerOptions[optionKey];
if (!('value' in ev)) {
continue;
}
const value = ev.parsedValue ?? ev.value;
// Convert string values back to TypeScript enum values
if (optionKey === 'module' && typeof value === 'string') {
const moduleMap = {
esnext: typescript.ModuleKind.ESNext,
es2020: typescript.ModuleKind.ES2020,
...typescript.ModuleKind.Preserve !== undefined ? {
preserve: typescript.ModuleKind.Preserve
} : {},
nodenext: typescript.ModuleKind.NodeNext,
node16: typescript.ModuleKind.Node16,
commonjs: typescript.ModuleKind.CommonJS,
amd: typescript.ModuleKind.AMD
};
res[optionKey] = moduleMap[value.toLowerCase()] ?? value;
} else if (optionKey === 'moduleResolution' && typeof value === 'string') {
const moduleResolutionMap = {
bundler: typescript.ModuleResolutionKind.Bundler,
node10: typescript.ModuleResolutionKind.Node10,
node12: typescript.ModuleResolutionKind.Node12,
node: typescript.ModuleResolutionKind.NodeJs
};
res[optionKey] = moduleResolutionMap[value.toLowerCase()] ?? value;
} else if (optionKey === 'jsx' && typeof value === 'string') {
const jsxMap = {
'react-jsx': typescript.JsxEmit.ReactJSX
};
res[optionKey] = jsxMap[value.toLowerCase()] ?? value;
} else {
res[optionKey] = value;
}
}
return res;
}
const localDevTestFilesExcludeAction = 'NEXT_PRIVATE_LOCAL_DEV_TEST_FILES_EXCLUDE';
export async function writeConfigurationDefaults(typescriptVersion, tsConfigPath, isFirstTimeSetup, hasAppDir, distDir, hasPagesDir, isolatedDevBuild) {
var _userTsConfig_compilerOptions;
if (isFirstTimeSetup) {
writeFileSync(tsConfigPath, '{}' + os.EOL);
}
const userTsConfigContent = readFileSync(tsConfigPath, {
encoding: 'utf8'
});
const userTsConfig = CommentJson.parse(userTsConfigContent);
// Bail automatic setup when the user has extended or referenced another config
if ('extends' in userTsConfig || 'references' in userTsConfig) {
return;
}
if ((userTsConfig == null ? void 0 : userTsConfig.compilerOptions) == null) {
userTsConfig.compilerOptions = {};
isFirstTimeSetup = true;
}
const desiredCompilerOptions = getDesiredCompilerOptions(typescriptVersion, userTsConfig);
const suggestedActions = [];
const requiredActions = [];
for(const optionKey in desiredCompilerOptions){
const check = desiredCompilerOptions[optionKey];
if ('suggested' in check) {
if (!(optionKey in (userTsConfig == null ? void 0 : userTsConfig.compilerOptions))) {
userTsConfig.compilerOptions[optionKey] = check.suggested;
suggestedActions.push(cyan(optionKey) + ' was set to ' + bold(check.suggested) + (check.reason ? ` (${check.reason})` : ''));
}
} else if ('value' in check) {
var _userTsConfig_compilerOptions1;
let existingValue = userTsConfig == null ? void 0 : (_userTsConfig_compilerOptions1 = userTsConfig.compilerOptions) == null ? void 0 : _userTsConfig_compilerOptions1[optionKey];
if (typeof existingValue === 'string') {
existingValue = existingValue.toLowerCase();
}
const shouldWriteRequiredValue = ()=>{
// Check if the option has multiple allowed values
if (check.parsedValues) {
return !check.parsedValues.includes(existingValue);
}
// Check if the option has a single parsed value
if (check.parsedValue) {
return check.parsedValue !== existingValue;
}
// Fall back to direct value comparison
return check.value !== existingValue;
};
if (shouldWriteRequiredValue()) {
if (!userTsConfig.compilerOptions) {
userTsConfig.compilerOptions = {};
}
userTsConfig.compilerOptions[optionKey] = check.value;
requiredActions.push(cyan(optionKey) + ' was set to ' + bold(check.value) + ` (${check.reason})`);
}
} else {
const _ = check;
}
}
const resolvedIsolatedDevBuild = isolatedDevBuild === undefined ? defaultConfig.experimental.isolatedDevBuild : isolatedDevBuild;
// Get type definition glob patterns using shared utility to ensure consistency
// with other TypeScript infrastructure (e.g., runTypeCheck.ts)
const nextAppTypes = getTypeDefinitionGlobPatterns(distDir, resolvedIsolatedDevBuild);
if (!('include' in userTsConfig)) {
userTsConfig.include = hasAppDir ? [
'next-env.d.ts',
...nextAppTypes,
'**/*.mts',
'**/*.ts',
'**/*.tsx'
] : [
'next-env.d.ts',
'**/*.mts',
'**/*.ts',
'**/*.tsx'
];
suggestedActions.push(cyan('include') + ' was set to ' + bold(hasAppDir ? `['next-env.d.ts', ${nextAppTypes.map((type)=>`'${type}'`).join(', ')}, '**/*.mts', '**/*.ts', '**/*.tsx']` : `['next-env.d.ts', '**/*.mts', '**/*.ts', '**/*.tsx']`));
} else if (hasAppDir) {
const missingFromResolved = [];
for (const type of nextAppTypes){
if (!userTsConfig.include.includes(type)) {
missingFromResolved.push(type);
}
}
if (missingFromResolved.length > 0) {
if (!Array.isArray(userTsConfig.include)) {
userTsConfig.include = [];
}
missingFromResolved.forEach((item)=>{
userTsConfig.include.push(item);
suggestedActions.push(cyan('include') + ' was updated to add ' + bold(`'${item}'`));
});
}
}
// Enable the Next.js typescript plugin.
if (hasAppDir) {
var _userTsConfig_compilerOptions2;
// Check if the config or the resolved config has the plugin already.
const plugins = [
...Array.isArray(userTsConfig == null ? void 0 : userTsConfig.plugins) ? userTsConfig.plugins : [],
...userTsConfig.compilerOptions && Array.isArray(userTsConfig.compilerOptions.plugins) ? userTsConfig.compilerOptions.plugins : []
];
const hasNextPlugin = plugins.some(({ name })=>name === 'next');
// If the TS config extends on another config, we can't add the `plugin` field
// because that will override the parent config's plugins.
// Instead we have to show a message to the user to add the plugin manually.
if (!userTsConfig.compilerOptions || plugins.length && !hasNextPlugin && 'extends' in userTsConfig && (!userTsConfig.compilerOptions || !userTsConfig.compilerOptions.plugins)) {
Log.info(`\nYour ${bold('tsconfig.json')} extends another configuration, which means we cannot add the Next.js TypeScript plugin automatically. To improve your development experience, we recommend adding the Next.js plugin (\`${cyan('"plugins": [{ "name": "next" }]')}\`) manually to your TypeScript configuration. Learn more: https://nextjs.org/docs/app/api-reference/config/typescript#the-typescript-plugin\n`);
} else if (!hasNextPlugin) {
if (!('plugins' in userTsConfig.compilerOptions)) {
userTsConfig.compilerOptions.plugins = [];
}
userTsConfig.compilerOptions.plugins.push({
name: 'next'
});
suggestedActions.push(cyan('plugins') + ' was updated to add ' + bold(`{ name: 'next' }`));
}
// If `strict` is set to `false` and `strictNullChecks` is set to `false`,
// then set `strictNullChecks` to `true`.
if (hasPagesDir && hasAppDir && !(userTsConfig == null ? void 0 : (_userTsConfig_compilerOptions2 = userTsConfig.compilerOptions) == null ? void 0 : _userTsConfig_compilerOptions2.strict) && !('strictNullChecks' in (userTsConfig == null ? void 0 : userTsConfig.compilerOptions))) {
userTsConfig.compilerOptions.strictNullChecks = true;
suggestedActions.push(cyan('strictNullChecks') + ' was set to ' + bold(`true`));
}
}
if (!('exclude' in userTsConfig)) {
userTsConfig.exclude = [
'node_modules'
];
suggestedActions.push(cyan('exclude') + ' was set to ' + bold(`['node_modules']`));
}
// During local development inside Next.js repo, exclude the test files coverage by the local tsconfig
if (process.env.NEXT_PRIVATE_LOCAL_DEV && userTsConfig.exclude) {
const tsGlob = '**/*.test.ts';
const tsxGlob = '**/*.test.tsx';
let hasUpdates = false;
if (!userTsConfig.exclude.includes(tsGlob)) {
userTsConfig.exclude.push(tsGlob);
hasUpdates = true;
}
if (!userTsConfig.exclude.includes(tsxGlob)) {
userTsConfig.exclude.push(tsxGlob);
hasUpdates = true;
}
if (hasUpdates) {
requiredActions.push(localDevTestFilesExcludeAction);
}
}
if (suggestedActions.length < 1 && requiredActions.length < 1) {
return;
}
writeFileSync(tsConfigPath, CommentJson.stringify(userTsConfig, null, 2) + os.EOL);
Log.info('');
if (isFirstTimeSetup) {
Log.info(`We detected TypeScript in your project and created a ${cyan('tsconfig.json')} file for you.`);
return;
}
Log.info(`We detected TypeScript in your project and reconfigured your ${cyan('tsconfig.json')} file for you.${((_userTsConfig_compilerOptions = userTsConfig.compilerOptions) == null ? void 0 : _userTsConfig_compilerOptions.strict) ? '' : ` Strict-mode is set to ${cyan('false')} by default.`}`);
if (suggestedActions.length) {
Log.info(`The following suggested values were added to your ${cyan('tsconfig.json')}. These values ${cyan('can be changed')} to fit your project's needs:\n`);
suggestedActions.forEach((action)=>Log.info(`\t- ${action}`));
Log.info('');
}
const requiredActionsToBeLogged = process.env.NEXT_PRIVATE_LOCAL_DEV ? requiredActions.filter((action)=>action !== localDevTestFilesExcludeAction) : requiredActions;
if (requiredActionsToBeLogged.length) {
Log.info(`The following ${white('mandatory changes')} were made to your ${cyan('tsconfig.json')}:\n`);
requiredActionsToBeLogged.forEach((action)=>Log.info(`\t- ${action}`));
Log.info('');
}
}
//# sourceMappingURL=writeConfigurationDefaults.js.map