UNPKG

next

Version:

The React Framework

173 lines (172 loc) • 8.71 kB
import { findSourceMap } from 'module'; import * as path from 'path'; import * as url from 'url'; import { SourceMapConsumer as SyncSourceMapConsumer } from 'next/dist/compiled/source-map'; import { parseStack } from '../client/components/react-dev-overlay/server/middleware'; import { getOriginalCodeFrame } from '../client/components/react-dev-overlay/server/shared'; import { workUnitAsyncStorage } from './app-render/work-unit-async-storage.external'; // TODO: Implement for Edge runtime const inspectSymbol = Symbol.for('nodejs.util.inspect.custom'); function frameToString(frame) { let sourceLocation = frame.lineNumber !== null ? `:${frame.lineNumber}` : ''; if (frame.column !== null && sourceLocation !== '') { sourceLocation += `:${frame.column}`; } const filePath = frame.file !== null && frame.file.startsWith('file://') && URL.canParse(frame.file) ? // In a multi-app repo, this leads to potentially larger file names but will make clicking snappy. // There's no tradeoff for the cases where `dir` in `next dev [dir]` is omitted // since relative to cwd is both the shortest and snappiest. path.relative(process.cwd(), url.fileURLToPath(frame.file)) : frame.file; return frame.methodName ? ` at ${frame.methodName} (${filePath}${sourceLocation})` : ` at ${filePath}${frame.lineNumber}:${frame.column}`; } function computeErrorName(error) { // TODO: Node.js seems to use a different algorithm // class ReadonlyRequestCookiesError extends Error {}` would read `ReadonlyRequestCookiesError: [...]` // in the stack i.e. seems like under certain conditions it favors the constructor name. return error.name || 'Error'; } function prepareUnsourcemappedStackTrace(error, structuredStackTrace) { const name = computeErrorName(error); const message = error.message || ''; let stack = name + ': ' + message; for(let i = 0; i < structuredStackTrace.length; i++){ stack += '\n at ' + structuredStackTrace[i].toString(); } return stack; } function shouldIgnoreListByDefault(file) { return file.startsWith('node:'); } function getSourcemappedFrameIfPossible(frame, sourceMapCache) { var _rawSourceMap_ignoreList, // default is not a valid identifier in JS so webpack uses a custom variable when it's an unnamed default export // Resolve it back to `default` for the method name if the source position didn't have the method. _frame_methodName_replace, _frame_methodName; if (frame.file === null) { return null; } const sourceMapCacheEntry = sourceMapCache.get(frame.file); let sourceMap; let rawSourceMap; if (sourceMapCacheEntry === undefined) { const moduleSourceMap = findSourceMap(frame.file); if (moduleSourceMap === undefined) { return null; } rawSourceMap = moduleSourceMap.payload; sourceMap = new SyncSourceMapConsumer(// @ts-expect-error -- Module.SourceMap['version'] is number but SyncSourceMapConsumer wants a string rawSourceMap); sourceMapCache.set(frame.file, { map: sourceMap, raw: rawSourceMap }); } else { sourceMap = sourceMapCacheEntry.map; rawSourceMap = sourceMapCacheEntry.raw; } const sourcePosition = sourceMap.originalPositionFor({ column: frame.column ?? 0, line: frame.lineNumber ?? 1 }); if (sourcePosition.source === null) { return null; } const sourceContent = sourceMap.sourceContentFor(sourcePosition.source, /* returnNullOnMissing */ true) ?? null; // TODO: O(n^2). Consider moving `ignoreList` into a Set const sourceIndex = rawSourceMap.sources.indexOf(sourcePosition.source); const ignored = ((_rawSourceMap_ignoreList = rawSourceMap.ignoreList) == null ? void 0 : _rawSourceMap_ignoreList.includes(sourceIndex)) ?? false; const originalFrame = { methodName: sourcePosition.name || ((_frame_methodName = frame.methodName) == null ? void 0 : (_frame_methodName_replace = _frame_methodName.replace('__WEBPACK_DEFAULT_EXPORT__', 'default')) == null ? void 0 : _frame_methodName_replace.replace('__webpack_exports__.', '')), column: sourcePosition.column, file: sourcePosition.source, lineNumber: sourcePosition.line, // TODO: c&p from async createOriginalStackFrame but why not frame.arguments? arguments: [], ignored }; const codeFrame = process.env.NODE_ENV !== 'production' ? getOriginalCodeFrame(originalFrame, sourceContent) : null; return { stack: originalFrame, code: codeFrame }; } function parseAndSourceMap(error) { // We overwrote Error.prepareStackTrace earlier so error.stack is not sourcemapped. let unparsedStack = String(error.stack); // We could just read it from `error.stack`. // This works around cases where a 3rd party `Error.prepareStackTrace` implementation // doesn't implement the name computation correctly. const errorName = computeErrorName(error); let idx = unparsedStack.indexOf('react-stack-bottom-frame'); if (idx !== -1) { idx = unparsedStack.lastIndexOf('\n', idx); } if (idx !== -1) { // Cut off everything after the bottom frame since it'll be React internals. unparsedStack = unparsedStack.slice(0, idx); } const unsourcemappedStack = parseStack(unparsedStack); const sourceMapCache = new Map(); let sourceMappedStack = ''; let sourceFrameDEV = null; for (const frame of unsourcemappedStack){ if (frame.file === null) { sourceMappedStack += '\n' + frameToString(frame); } else if (!shouldIgnoreListByDefault(frame.file)) { const sourcemappedFrame = getSourcemappedFrameIfPossible(frame, sourceMapCache); if (sourcemappedFrame === null) { sourceMappedStack += '\n' + frameToString(frame); } else { if (process.env.NODE_ENV !== 'production' && sourcemappedFrame.code !== null && sourceFrameDEV === null && // TODO: Is this the right choice? !sourcemappedFrame.stack.ignored) { sourceFrameDEV = sourcemappedFrame.code; } if (!sourcemappedFrame.stack.ignored) { // TODO: Consider what happens if every frame is ignore listed. sourceMappedStack += '\n' + frameToString(sourcemappedFrame.stack); } } } } return errorName + ': ' + error.message + sourceMappedStack + (sourceFrameDEV !== null ? '\n' + sourceFrameDEV : ''); } export function patchErrorInspect() { Error.prepareStackTrace = prepareUnsourcemappedStackTrace; // @ts-expect-error -- TODO upstream types // eslint-disable-next-line no-extend-native -- We're not extending but overriding. Error.prototype[inspectSymbol] = function(depth, inspectOptions, inspect) { // avoid false-positive dynamic i/o warnings e.g. due to usage of `Math.random` in `source-map`. return workUnitAsyncStorage.exit(()=>{ // Create a new Error object with the source mapping applied and then use native // Node.js formatting on the result. const newError = this.cause !== undefined ? new Error(this.message, { cause: this.cause }) : new Error(this.message); // TODO: Ensure `class MyError extends Error {}` prints `MyError` as the name newError.stack = parseAndSourceMap(this); for(const key in this){ if (!Object.prototype.hasOwnProperty.call(newError, key)) { // @ts-expect-error -- We're copying all enumerable properties. // So they definitely exist on `this` and obviously have no type on `newError` (yet) newError[key] = this[key]; } } const originalCustomInspect = newError[inspectSymbol]; // Prevent infinite recursion. // { customInspect: false } would result in `error.cause` not using our inspect. Object.defineProperty(newError, inspectSymbol, { value: undefined, enumerable: false, writable: true }); try { return inspect(newError, { ...inspectOptions, depth: (inspectOptions.depth ?? // Default in Node.js 2) - depth }); } finally{ newError[inspectSymbol] = originalCustomInspect; } }); }; } //# sourceMappingURL=patch-error-inspect.js.map