| | import { getOriginalStackFrames as getOriginalStackFramesWebpack } from '../middleware-webpack' |
| | import { getOriginalStackFrames as getOriginalStackFramesTurbopack } from '../middleware-turbopack' |
| | import type { Project } from '../../../build/swc/types' |
| | import { dim } from '../../../lib/picocolors' |
| | import { parseStack, type StackFrame } from '../../lib/parse-stack' |
| | import path from 'path' |
| | import { LRUCache } from '../../lib/lru-cache' |
| |
|
| | type WebpackMappingContext = { |
| | bundler: 'webpack' |
| | isServer: boolean |
| | isEdgeServer: boolean |
| | isAppDirectory: boolean |
| | clientStats: () => any |
| | serverStats: () => any |
| | edgeServerStats: () => any |
| | rootDirectory: string |
| | } |
| |
|
| | type TurbopackMappingContext = { |
| | bundler: 'turbopack' |
| | isServer: boolean |
| | isEdgeServer: boolean |
| | isAppDirectory: boolean |
| | project: Project |
| | projectPath: string |
| | } |
| |
|
| | export type MappingContext = WebpackMappingContext | TurbopackMappingContext |
| |
|
| | |
| | export async function mapFramesUsingBundler( |
| | frames: StackFrame[], |
| | ctx: MappingContext |
| | ) { |
| | switch (ctx.bundler) { |
| | case 'webpack': { |
| | const { |
| | isServer, |
| | isEdgeServer, |
| | isAppDirectory, |
| | clientStats, |
| | serverStats, |
| | edgeServerStats, |
| | rootDirectory, |
| | } = ctx |
| | const res = await getOriginalStackFramesWebpack({ |
| | isServer, |
| | isEdgeServer, |
| | isAppDirectory, |
| | frames, |
| | clientStats, |
| | serverStats, |
| | edgeServerStats, |
| | rootDirectory, |
| | }) |
| | return res |
| | } |
| | case 'turbopack': { |
| | const { project, projectPath, isServer, isEdgeServer, isAppDirectory } = |
| | ctx |
| | const res = await getOriginalStackFramesTurbopack({ |
| | project, |
| | projectPath, |
| | frames, |
| | isServer, |
| | isEdgeServer, |
| | isAppDirectory, |
| | }) |
| |
|
| | return res |
| | } |
| | default: { |
| | return null! |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | function preprocessStackTrace(stackTrace: string, distDir?: string): string { |
| | return stackTrace |
| | .split('\n') |
| | .map((line) => { |
| | const match = line.match(/^(\s*at\s+.*?)\s+\(([^)]+)\)$/) |
| | if (match) { |
| | const [, prefix, location] = match |
| |
|
| | if (location.startsWith('_next/static/') && distDir) { |
| | const normalizedDistDir = distDir |
| | .replace(/\\/g, '/') |
| | .replace(/\/$/, '') |
| |
|
| | const absolutePath = |
| | normalizedDistDir + '/' + location.slice('_next/'.length) |
| | const fileUrl = `file://${path.resolve(absolutePath)}` |
| |
|
| | return `${prefix} (${fileUrl})` |
| | } |
| | } |
| |
|
| | return line |
| | }) |
| | .join('\n') |
| | } |
| |
|
| | const cache = new LRUCache< |
| | Awaited<ReturnType<typeof getSourceMappedStackFramesInternal>> |
| | >(25) |
| | async function getSourceMappedStackFramesInternal( |
| | stackTrace: string, |
| | ctx: MappingContext, |
| | distDir: string, |
| | ignore = true |
| | ) { |
| | try { |
| | const normalizedStack = preprocessStackTrace(stackTrace, distDir) |
| | const frames = parseStack(normalizedStack, distDir) |
| |
|
| | if (frames.length === 0) { |
| | return { |
| | kind: 'stack' as const, |
| | stack: stackTrace, |
| | } |
| | } |
| |
|
| | const mappingResults = await mapFramesUsingBundler(frames, ctx) |
| |
|
| | const processedFrames = mappingResults |
| | .map((result, index) => ({ |
| | result, |
| | originalFrame: frames[index], |
| | })) |
| | .map(({ result, originalFrame }) => { |
| | if (result.status === 'rejected') { |
| | return { |
| | kind: 'rejected' as const, |
| | frameText: formatStackFrame(originalFrame), |
| | codeFrame: null, |
| | } |
| | } |
| |
|
| | const { originalStackFrame, originalCodeFrame } = result.value |
| | if (originalStackFrame?.ignored && ignore) { |
| | return { |
| | kind: 'ignored' as const, |
| | } |
| | } |
| |
|
| | |
| | if (originalStackFrame?.file?.startsWith('chrome-extension://')) { |
| | return { |
| | kind: 'ignored' as const, |
| | } |
| | } |
| |
|
| | return { |
| | kind: 'success' as const, |
| | |
| | |
| | frameText: formatStackFrame(originalStackFrame!), |
| | codeFrame: originalCodeFrame, |
| | } |
| | }) |
| |
|
| | const allIgnored = processedFrames.every( |
| | (frame) => frame.kind === 'ignored' |
| | ) |
| |
|
| | |
| | |
| | |
| | if (allIgnored) { |
| | return { |
| | kind: 'all-ignored' as const, |
| | } |
| | } |
| |
|
| | const filteredFrames = processedFrames.filter( |
| | (frame) => frame.kind !== 'ignored' |
| | ) |
| |
|
| | if (filteredFrames.length === 0) { |
| | return { |
| | kind: 'stack' as const, |
| | stack: stackTrace, |
| | } |
| | } |
| |
|
| | const stackOutput = filteredFrames |
| | .map((frame) => frame.frameText) |
| | .join('\n') |
| | const firstFrameCode = filteredFrames.find( |
| | (frame) => frame.codeFrame |
| | )?.codeFrame |
| |
|
| | if (firstFrameCode) { |
| | return { |
| | kind: 'with-frame-code' as const, |
| | frameCode: firstFrameCode, |
| | stack: stackOutput, |
| | frames: filteredFrames, |
| | } |
| | } |
| | |
| | return { |
| | kind: 'mapped-stack' as const, |
| | stack: stackOutput, |
| | frames: filteredFrames, |
| | } |
| | } catch (error) { |
| | return { |
| | kind: 'stack' as const, |
| | stack: stackTrace, |
| | } |
| | } |
| | } |
| |
|
| | |
| | export async function getSourceMappedStackFrames( |
| | stackTrace: string, |
| | ctx: MappingContext, |
| | distDir: string, |
| | ignore = true |
| | ) { |
| | const cacheKey = `sm_${stackTrace}-${ctx.bundler}-${ctx.isAppDirectory}-${ctx.isEdgeServer}-${ctx.isServer}-${distDir}-${ignore}` |
| |
|
| | const cacheItem = cache.get(cacheKey) |
| | if (cacheItem) { |
| | return cacheItem |
| | } |
| |
|
| | const result = await getSourceMappedStackFramesInternal( |
| | stackTrace, |
| | ctx, |
| | distDir, |
| | ignore |
| | ) |
| | cache.set(cacheKey, result) |
| | return result |
| | } |
| |
|
| | function formatStackFrame(frame: StackFrame): string { |
| | const functionName = frame.methodName || '<anonymous>' |
| | const location = |
| | frame.file && frame.line1 |
| | ? `${frame.file}:${frame.line1}${frame.column1 ? `:${frame.column1}` : ''}` |
| | : frame.file || '<unknown>' |
| |
|
| | return ` at ${functionName} (${location})` |
| | } |
| |
|
| | |
| | export const withLocation = async ( |
| | { |
| | original, |
| | stack, |
| | }: { |
| | original: Array<any> |
| | stack: string | null |
| | }, |
| | ctx: MappingContext, |
| | distDir: string, |
| | config: boolean | { logDepth?: number; showSourceLocation?: boolean } |
| | ) => { |
| | if (typeof config === 'object' && config.showSourceLocation === false) { |
| | return original |
| | } |
| | if (!stack) { |
| | return original |
| | } |
| |
|
| | const res = await getSourceMappedStackFrames(stack, ctx, distDir) |
| | const location = getConsoleLocation(res) |
| |
|
| | if (!location) { |
| | return original |
| | } |
| |
|
| | return [...original, dim(`(${location})`)] |
| | } |
| |
|
| | export const getConsoleLocation = ( |
| | mapped: Awaited<ReturnType<typeof getSourceMappedStackFrames>> |
| | ) => { |
| | if (mapped.kind !== 'mapped-stack' && mapped.kind !== 'with-frame-code') { |
| | return null |
| | } |
| |
|
| | const first = mapped.frames.at(0) |
| |
|
| | if (!first) { |
| | return null |
| | } |
| |
|
| | |
| | const match = first.frameText.match(/\(([^)]+)\)/) |
| | const locationText = match ? match[1] : first.frameText |
| | return locationText |
| | } |
| |
|