import path from 'path'
import fs from 'fs'
import type { NextConfigComplete } from '../server/config-shared'
import * as Log from './output/log'
import { getTypeScriptConfiguration } from '../lib/typescript/getTypeScriptConfiguration'
import { readFileSync } from 'fs'
import isError from '../lib/is-error'
import { hasNecessaryDependencies } from '../lib/has-necessary-dependencies'
import { codeFrameColumns } from '../shared/lib/errors/code-frame'
let TSCONFIG_WARNED = false
export function parseJsonFile(filePath: string) {
const JSON5 =
require('next/dist/compiled/json5') as typeof import('next/dist/compiled/json5')
const contents = readFileSync(filePath, 'utf8')
// Special case an empty file
if (contents.trim() === '') {
return {}
}
try {
return JSON5.parse(contents)
} catch (err) {
if (!isError(err)) throw err
const codeFrame = codeFrameColumns(
String(contents),
{
start: {
line: (err as Error & { lineNumber?: number }).lineNumber || 1,
column:
(err as Error & { columnNumber?: number }).columnNumber ||
undefined,
},
},
{ message: err.message, color: true }
)
throw new Error(
`Failed to parse "${filePath}":\n${codeFrame ?? err.message}`
)
}
}
export type ResolvedBaseUrl =
| { baseUrl: string; isImplicit: boolean }
| undefined
export type JsConfig = { compilerOptions: Record<string, any> } | undefined
export default async function loadJsConfig(
dir: string,
config: NextConfigComplete
): Promise<{
useTypeScript: boolean
jsConfig: JsConfig
jsConfigPath?: string
resolvedBaseUrl: ResolvedBaseUrl
}> {
let typeScriptPath: string | undefined
try {
const deps = hasNecessaryDependencies(dir, [
{
pkg: 'typescript',
file: 'typescript/lib/typescript.js',
exportsRestrict: true,
},
])
typeScriptPath = deps.resolved.get('typescript')
} catch {}
const tsConfigFileName = config.typescript.tsconfigPath || 'tsconfig.json'
const tsConfigPath = path.join(dir, tsConfigFileName)
const useTypeScript = Boolean(typeScriptPath && fs.existsSync(tsConfigPath))
let implicitBaseurl
let jsConfig: { compilerOptions: Record<string, any> } | undefined
// jsconfig is a subset of tsconfig
if (useTypeScript) {
if (tsConfigFileName !== 'tsconfig.json' && TSCONFIG_WARNED === false) {
TSCONFIG_WARNED = true
Log.info(`Using tsconfig file: ${tsConfigFileName}`)
}
const ts = (await Promise.resolve(
require(typeScriptPath!)
)) as typeof import('typescript')
const tsConfig = await getTypeScriptConfiguration(ts, tsConfigPath, true)
jsConfig = { compilerOptions: tsConfig.options }
implicitBaseurl = path.dirname(tsConfigPath)
}
const jsConfigPath = path.join(dir, 'jsconfig.json')
if (!useTypeScript && fs.existsSync(jsConfigPath)) {
jsConfig = parseJsonFile(jsConfigPath)
implicitBaseurl = path.dirname(jsConfigPath)
}
let resolvedBaseUrl: ResolvedBaseUrl
if (jsConfig?.compilerOptions?.baseUrl) {
resolvedBaseUrl = {
baseUrl: path.resolve(dir, jsConfig.compilerOptions.baseUrl),
isImplicit: false,
}
} else {
// TypeScript 5.0+: `pathsBasePath` is the directory of the tsconfig that
// defines `paths`. For paths inherited from an extended base tsconfig (e.g.
// a workspace-root tsconfig.base.json for nx monorepo), this is the base
// config's directory — not the app tsconfig dir. Using it ensures JsConfigPathsPlugin
// joins path-mapping values against the correct base so `baseUrl` is not required
// for path aliases to work in webpack.
const pathsBasePath: string | undefined =
jsConfig?.compilerOptions?.pathsBasePath
const effectiveBaseUrl = pathsBasePath ?? implicitBaseurl
if (effectiveBaseUrl) {
resolvedBaseUrl = {
baseUrl: effectiveBaseUrl,
isImplicit: true,
}
}
}
return {
useTypeScript,
jsConfig,
resolvedBaseUrl,
jsConfigPath: useTypeScript
? tsConfigPath
: fs.existsSync(jsConfigPath)
? jsConfigPath
: undefined,
}
}