import type {
Issue,
PlainTraceItem,
StyledString,
TurbopackResult,
} from '../../../build/swc/types'
import { bold, green, magenta, red } from '../../../lib/picocolors'
import { deobfuscateText } from '../magic-identifier'
import type { EntryKey } from './entry-key'
import * as Log from '../../../build/output/log'
import type { NextConfigComplete } from '../../../server/config-shared'
type IssueKey = `${Issue['severity']}-${Issue['filePath']}-${string}-${string}`
export type IssuesMap = Map<IssueKey, Issue>
export type EntryIssuesMap = Map<EntryKey, IssuesMap>
export type TopLevelIssuesMap = IssuesMap
const VERBOSE_ISSUES = !!process.env.NEXT_TURBOPACK_VERBOSE_ISSUES
/**
* An error generated from emitted Turbopack issues. This can include build
* errors caused by issues with user code.
*/
export class ModuleBuildError extends Error {
name = 'ModuleBuildError'
}
/**
* Thin stopgap workaround layer to mimic existing wellknown-errors-plugin in webpack's build
* to emit certain type of errors into cli.
*/
export function isWellKnownError(issue: Issue): boolean {
const { title } = issue
const formattedTitle = renderStyledStringToErrorAnsi(title)
// TODO: add more well known errors
if (
formattedTitle.includes('Module not found') ||
formattedTitle.includes('Unknown module type')
) {
return true
}
return false
}
export function getIssueKey(issue: Issue): IssueKey {
return `${issue.severity}-${issue.filePath}-${JSON.stringify(
issue.title
)}-${JSON.stringify(issue.description)}`
}
export function processIssues(
currentEntryIssues: EntryIssuesMap,
key: EntryKey,
result: TurbopackResult,
throwIssue: boolean,
logErrors: boolean
) {
const newIssues = new Map<IssueKey, Issue>()
currentEntryIssues.set(key, newIssues)
const relevantIssues = new Set()
for (const issue of result.issues) {
if (
issue.severity !== 'error' &&
issue.severity !== 'fatal' &&
issue.severity !== 'warning'
)
continue
const issueKey = getIssueKey(issue)
newIssues.set(issueKey, issue)
if (issue.severity !== 'warning') {
if (throwIssue) {
const formatted = formatIssue(issue)
relevantIssues.add(formatted)
}
// if we throw the issue it will most likely get handed and logged elsewhere
else if (logErrors && isWellKnownError(issue)) {
const formatted = formatIssue(issue)
Log.error(formatted)
}
}
}
if (relevantIssues.size && throwIssue) {
throw new ModuleBuildError([...relevantIssues].join('\n\n'))
}
}
function formatFilePath(filePath: string): string {
return filePath
.replace('[project]/', './')
.replaceAll('/./', '/')
.replace('\\\\?\\', '')
}
export function formatIssue(issue: Issue) {
const { filePath, title, description, detail, source, importTraces } = issue
let { documentationLink } = issue
const formattedTitle = renderStyledStringToErrorAnsi(title).replace(
/\n/g,
'\n '
)
// TODO: Use error codes to identify these
// TODO: Generalize adapting Turbopack errors to Next.js errors
if (formattedTitle.includes('Module not found')) {
// For compatiblity with webpack
// TODO: include columns in webpack errors.
documentationLink = 'https://nextjs.org/docs/messages/module-not-found'
}
const formattedFilePath = formatFilePath(filePath)
let message = ''
if (source?.range) {
const { start } = source.range
message = `${formattedFilePath}:${start.line + 1}:${
start.column + 1
}\n${formattedTitle}`
} else if (formattedFilePath) {
message = `${formattedFilePath}\n${formattedTitle}`
} else {
message = formattedTitle
}
message += '\n'
if (issue.codeFrame) {
message += issue.codeFrame.trimEnd() + '\n\n'
}
if (description) {
if (
description.type === 'text' &&
description.value.includes(`Cannot find module 'sass'`)
) {
message +=
"To use Next.js' built-in Sass support, you first need to install `sass`.\n"
message += 'Run `npm i sass` or `yarn add sass` inside your workspace.\n'
message += '\nLearn more: https://nextjs.org/docs/messages/install-sass\n'
} else {
message += renderStyledStringToErrorAnsi(description) + '\n\n'
}
}
// TODO: make it easier to enable this for debugging
if (VERBOSE_ISSUES && detail) {
message += renderStyledStringToErrorAnsi(detail) + '\n\n'
}
// Render additional sources (e.g., generated code from a loader)
for (const additional of issue.additionalSources ?? []) {
if (additional.codeFrame) {
const additionalFilePath = formatFilePath(
additional.source.source.filePath
)
const loc = additional.source.range
? `:${additional.source.range.start.line + 1}:${additional.source.range.start.column + 1}`
: ''
message += `${additional.description}:\n${additionalFilePath}${loc}\n${additional.codeFrame.trimEnd()}\n\n`
}
}
if (importTraces?.length) {
// This is the same logic as in turbopack/crates/turbopack-cli-utils/src/issue.rs
// We end up with multiple traces when the file with the error is reachable from multiple
// different entry points (e.g. ssr, client)
message += `Import trace${importTraces.length > 1 ? 's' : ''}:\n`
const everyTraceHasADistinctRootLayer =
new Set(importTraces.map(leafLayerName).filter((l) => l != null)).size ===
importTraces.length
for (let i = 0; i < importTraces.length; i++) {
const trace = importTraces[i]
const layer = leafLayerName(trace)
let traceIndent = ' '
// If this is true, layer must be present
if (everyTraceHasADistinctRootLayer) {
message += ` ${layer}:\n`
} else {
if (importTraces.length > 1) {
// Otherwise use simple 1 based indices to disambiguate
message += ` #${i + 1}`
if (layer) {
message += ` [${layer}]`
}
message += ':\n'
} else if (layer) {
message += ` [${layer}]:\n`
} else {
// If there is a single trace and no layer name just don't indent it.
traceIndent = ' '
}
}
message += formatIssueTrace(trace, traceIndent, !identicalLayers(trace))
}
}
if (documentationLink) {
message += documentationLink + '\n\n'
}
return message
}
/** Returns the first present layer name in the trace */
function leafLayerName(items: PlainTraceItem[]): string | undefined {
for (const item of items) {
const layer = item.layer
if (layer != null) return layer
}
return undefined
}
/**
* Returns whether or not all items share the same layer.
* If a layer is absent we ignore it in this analysis
*/
function identicalLayers(items: PlainTraceItem[]): boolean {
const firstPresentLayer = items.findIndex((t) => t.layer != null)
if (firstPresentLayer === -1) return true // all layers are absent
const layer = items[firstPresentLayer].layer
for (let i = firstPresentLayer + 1; i < items.length; i++) {
const itemLayer = items[i].layer
if (itemLayer == null || itemLayer !== layer) {
return false
}
}
return true
}
function formatIssueTrace(
items: PlainTraceItem[],
indent: string,
printLayers: boolean
): string {
return `${items
.map((item) => {
let r = indent
if (item.fsName !== 'project') {
r += `[${item.fsName}]/`
} else {
// This is consistent with webpack's output
r += './'
}
r += item.path
if (printLayers && item.layer) {
r += ` [${item.layer}]`
}
return r
})
.join('\n')}\n\n`
}
export function isRelevantWarning(issue: Issue): boolean {
return issue.severity === 'warning' && !isNodeModulesIssue(issue)
}
function isNodeModulesIssue(issue: Issue): boolean {
if (issue.severity === 'warning' && issue.stage === 'config') {
// Override for the externalize issue
// `Package foo (serverExternalPackages or default list) can't be external`
if (
renderStyledStringToErrorAnsi(issue.title).includes("can't be external")
) {
return false
}
}
return (
issue.severity === 'warning' &&
(issue.filePath.match(/^(?:.*[\\/])?node_modules(?:[\\/].*)?$/) !== null ||
// Ignore Next.js itself when running next directly in the monorepo where it is not inside
// node_modules anyway.
// TODO(mischnic) prevent matches when this is published to npm
issue.filePath.startsWith('[project]/packages/next/'))
)
}
export function renderStyledStringToErrorAnsi(string: StyledString): string {
function applyDeobfuscation(str: string): string {
// Use shared deobfuscate function and apply magenta color to identifiers
const deobfuscated = deobfuscateText(str)
// Color any {...} wrapped identifiers with magenta
return deobfuscated.replace(/\{([^}]+)\}/g, (match) => magenta(match))
}
switch (string.type) {
case 'text':
return applyDeobfuscation(string.value)
case 'strong':
return bold(red(applyDeobfuscation(string.value)))
case 'code':
return green(applyDeobfuscation(string.value))
case 'line':
return string.value.map(renderStyledStringToErrorAnsi).join('')
case 'stack':
return string.value.map(renderStyledStringToErrorAnsi).join('\n')
default:
throw new Error('Unknown StyledString type', string)
}
}
export function isFileSystemCacheEnabledForDev(
config: NextConfigComplete
): boolean {
return config.experimental?.turbopackFileSystemCacheForDev || false
}