UNPKG

next

Version:

The React Framework

428 lines (427 loc) • 16.6 kB
import { cyan, dim, red, yellow } from '../../../lib/picocolors'; import util from 'util'; import { getConsoleLocation, getSourceMappedStackFrames, withLocation } from './source-map'; import { UNDEFINED_MARKER } from '../../../next-devtools/shared/forward-logs-shared'; export function restoreUndefined(x) { if (x === UNDEFINED_MARKER) return undefined; if (Array.isArray(x)) return x.map(restoreUndefined); if (x && typeof x === 'object') { for(let k in x){ x[k] = restoreUndefined(x[k]); } } return x; } const methods = [ 'log', 'info', 'warn', 'debug', 'table', 'error', 'assert', 'dir', 'dirxml', 'group', 'groupCollapsed', 'groupEnd' ]; const methodsToSkipInspect = new Set([ 'table', 'dir', 'dirxml', 'group', 'groupCollapsed', 'groupEnd' ]); // we aren't overriding console, we're just making a (slightly convoluted) helper for replaying user console methods const forwardConsole = { ...console, ...Object.fromEntries(methods.map((method)=>[ method, (...args)=>console[method](...args.map((arg)=>methodsToSkipInspect.has(method) || typeof arg !== 'object' || arg === null ? arg : util.inspect(arg, { depth: Infinity, colors: true }))) ])) }; async function deserializeArgData(arg) { try { // we want undefined to be represented as it would be in the browser from the user's perspective (otherwise it would be stripped away/shown as null) if (arg === UNDEFINED_MARKER) { return restoreUndefined(arg); } return restoreUndefined(JSON.parse(arg)); } catch { return arg; } } const colorError = (mapped, config)=>{ const colorFn = (config == null ? void 0 : config.applyColor) === undefined || config.applyColor ? red : (x)=>x; switch(mapped.kind){ case 'mapped-stack': case 'stack': { return ((config == null ? void 0 : config.prefix) ? colorFn(config == null ? void 0 : config.prefix) : '') + `\n${colorFn(mapped.stack)}`; } case 'with-frame-code': { return ((config == null ? void 0 : config.prefix) ? colorFn(config == null ? void 0 : config.prefix) : '') + `\n${colorFn(mapped.stack)}\n${mapped.frameCode}`; } // a more sophisticated version of this allows the user to config if they want ignored frames (but we need to be sure to source map them) case 'all-ignored': { return (config == null ? void 0 : config.prefix) ? colorFn(config == null ? void 0 : config.prefix) : ''; } default: {} } mapped; }; function processConsoleFormatStrings(args) { /** * this handles the case formatting is applied to the console log * otherwise we will see the format specifier directly in the terminal output */ if (args.length > 0 && typeof args[0] === 'string') { const formatString = args[0]; if (formatString.includes('%s') || formatString.includes('%d') || formatString.includes('%i') || formatString.includes('%f') || formatString.includes('%o') || formatString.includes('%O') || formatString.includes('%c')) { try { const formatted = util.format(...args); return [ formatted ]; } catch { return args; } } } return args; } // in the case of logging errors, we want to strip formatting // modifiers since we apply our own custom coloring to error // stacks and code blocks, and otherwise it would conflict // and cause awful output export function stripFormatSpecifiers(args) { if (args.length === 0 || typeof args[0] !== 'string') return args; const fmtIn = String(args[0]); const rest = args.slice(1); if (!fmtIn.includes('%')) return args; let fmtOut = ''; let argPtr = 0; for(let i = 0; i < fmtIn.length; i++){ if (fmtIn[i] !== '%') { fmtOut += fmtIn[i]; continue; } if (fmtIn[i + 1] === '%') { fmtOut += '%'; i++; continue; } const token = fmtIn[++i]; if (!token) { fmtOut += '%'; continue; } if ('csdifoOj'.includes(token) || token === 'O') { if (argPtr < rest.length) { if (token === 'c') { argPtr++; } else if (token === 'o' || token === 'O' || token === 'j') { const obj = rest[argPtr++]; fmtOut += util.inspect(obj, { depth: 2, colors: false }); } else { // string(...) is safe for remaining specifiers fmtOut += String(rest[argPtr++]); } } continue; } fmtOut += '%' + token; } const result = [ fmtOut ]; if (argPtr < rest.length) { result.push(...rest.slice(argPtr)); } return result; } async function prepareFormattedErrorArgs(entry, ctx, distDir) { const mapped = await getSourceMappedStackFrames(entry.stack, ctx, distDir); return [ colorError(mapped, { prefix: entry.prefix }) ]; } async function prepareConsoleArgs(entry, ctx, distDir) { const deserialized = await Promise.all(entry.args.map(async (arg)=>{ if (arg.kind === 'arg') { const data = await deserializeArgData(arg.data); if (entry.method === 'warn' && typeof data === 'string') { return yellow(data); } return data; } if (!arg.stack) return red(arg.prefix); const mapped = await getSourceMappedStackFrames(arg.stack, ctx, distDir); return colorError(mapped, { prefix: arg.prefix, applyColor: false }); })); return processConsoleFormatStrings(deserialized); } async function prepareConsoleErrorArgs(entry, ctx, distDir) { const deserialized = await Promise.all(entry.args.map(async (arg)=>{ if (arg.kind === 'arg') { if (arg.isRejectionMessage) return red(arg.data); return deserializeArgData(arg.data); } if (!arg.stack) return red(arg.prefix); const mapped = await getSourceMappedStackFrames(arg.stack, ctx, distDir); return colorError(mapped, { prefix: arg.prefix }); })); const mappedStack = await getSourceMappedStackFrames(entry.consoleErrorStack, ctx, distDir); /** * don't show the stack + codeblock when there are errors present, since: * - it will look overwhelming to see 2 stacks and 2 code blocks * - the user already knows where the console.error is at because we append the location */ const location = getConsoleLocation(mappedStack); if (entry.args.some((a)=>a.kind === 'formatted-error-arg')) { const result = stripFormatSpecifiers(deserialized); if (location) { result.push(dim(`(${location})`)); } return result; } const result = [ ...processConsoleFormatStrings(deserialized), colorError(mappedStack) ]; if (location) { result.push(dim(`(${location})`)); } return result; } async function handleTable(entry, browserPrefix, ctx, distDir) { const deserializedArgs = await Promise.all(entry.args.map(async (arg)=>{ if (arg.kind === 'formatted-error-arg') { return { stack: arg.stack }; } return deserializeArgData(arg.data); })); const location = await (async ()=>{ if (!entry.consoleMethodStack) { return; } const frames = await getSourceMappedStackFrames(entry.consoleMethodStack, ctx, distDir); return getConsoleLocation(frames); })(); // we can't inline pass browser prefix, but it looks better multiline for table anyways forwardConsole.log(browserPrefix); forwardConsole.table(...deserializedArgs); if (location) { forwardConsole.log(dim(`(${location})`)); } } async function handleTrace(entry, browserPrefix, ctx, distDir) { const deserializedArgs = await Promise.all(entry.args.map(async (arg)=>{ if (arg.kind === 'formatted-error-arg') { if (!arg.stack) return red(arg.prefix); const mapped = await getSourceMappedStackFrames(arg.stack, ctx, distDir); return colorError(mapped, { prefix: arg.prefix }); } return deserializeArgData(arg.data); })); if (!entry.consoleMethodStack) { forwardConsole.log(browserPrefix, ...deserializedArgs, '[Trace unavailable]'); return; } // TODO(rob): refactor so we can re-use result and not re-run the entire source map to avoid trivial post processing const [mapped, mappedIgnored] = await Promise.all([ getSourceMappedStackFrames(entry.consoleMethodStack, ctx, distDir, false), getSourceMappedStackFrames(entry.consoleMethodStack, ctx, distDir) ]); const location = getConsoleLocation(mappedIgnored); forwardConsole.log(browserPrefix, ...deserializedArgs, `\n${mapped.stack}`, ...location ? [ `\n${dim(`(${location})`)}` ] : []); } async function handleDir(entry, browserPrefix, ctx, distDir) { const loggableEntry = await prepareConsoleArgs(entry, ctx, distDir); const consoleMethod = forwardConsole[entry.method] || forwardConsole.log; if (entry.consoleMethodStack) { const mapped = await getSourceMappedStackFrames(entry.consoleMethodStack, ctx, distDir); const location = dim(`(${getConsoleLocation(mapped)})`); const originalWrite = process.stdout.write.bind(process.stdout); let captured = ''; process.stdout.write = (chunk)=>{ captured += chunk; return true; }; try { consoleMethod(...loggableEntry); } finally{ process.stdout.write = originalWrite; } const preserved = captured.replace(/\r?\n$/, ''); originalWrite(`${browserPrefix}${preserved} ${location}\n`); return; } consoleMethod(browserPrefix, ...loggableEntry); } async function handleDefaultConsole(entry, browserPrefix, ctx, distDir, config) { const loggableEntry = await prepareConsoleArgs(entry, ctx, distDir); const withStackEntry = await withLocation({ original: loggableEntry, stack: entry.consoleMethodStack || null }, ctx, distDir, config); const consoleMethod = forwardConsole[entry.method] || forwardConsole.log; consoleMethod(browserPrefix, ...withStackEntry); } export async function handleLog(entries, ctx, distDir, config) { const browserPrefix = cyan('[browser]'); for (const entry of entries){ try { switch(entry.kind){ case 'console': { switch(entry.method){ case 'table': { // timeout based abort on source mapping result await handleTable(entry, browserPrefix, ctx, distDir); break; } // ignore frames case 'trace': { await handleTrace(entry, browserPrefix, ctx, distDir); break; } case 'dir': { await handleDir(entry, browserPrefix, ctx, distDir); break; } case 'dirxml': { // xml log thing maybe needs an impl // fallthrough } case 'group': case 'groupCollapsed': case 'groupEnd': { // [browser] undefined (app/page.tsx:8:11) console.group // fallthrough } case 'assert': { // check console assert // fallthrough } case 'log': case 'info': case 'debug': case 'error': case 'warn': { await handleDefaultConsole(entry, browserPrefix, ctx, distDir, config); break; } default: { entry; } } break; } // any logged errors are anything that are logged as "red" in the browser but aren't only an Error (console.error, Promise.reject(100)) case 'any-logged-error': { const consoleArgs = await prepareConsoleErrorArgs(entry, ctx, distDir); forwardConsole.error(browserPrefix, ...consoleArgs); break; } // formatted error is an explicit error event (rejections, uncaught errors) case 'formatted-error': { const formattedArgs = await prepareFormattedErrorArgs(entry, ctx, distDir); forwardConsole.error(browserPrefix, ...formattedArgs); break; } default: {} } } catch { switch(entry.kind){ case 'any-logged-error': { const consoleArgs = await prepareConsoleErrorArgs(entry, ctx, distDir); forwardConsole.error(browserPrefix, ...consoleArgs); break; } case 'console': { const consoleMethod = forwardConsole[entry.method] || forwardConsole.log; const consoleArgs = await prepareConsoleArgs(entry, ctx, distDir); consoleMethod(browserPrefix, ...consoleArgs); break; } case 'formatted-error': { forwardConsole.error(browserPrefix, `${entry.prefix}\n`, entry.stack); break; } default: {} } } } } // the data is used later when we need to get sourcemaps for error stacks export async function receiveBrowserLogsWebpack(opts) { const { entries, router, sourceType, clientStats, serverStats, edgeServerStats, rootDirectory, distDir } = opts; const isAppDirectory = router === 'app'; const isServer = sourceType === 'server'; const isEdgeServer = sourceType === 'edge-server'; const ctx = { bundler: 'webpack', isServer, isEdgeServer, isAppDirectory, clientStats, serverStats, edgeServerStats, rootDirectory }; await handleLog(entries, ctx, distDir, opts.config); } export async function receiveBrowserLogsTurbopack(opts) { const { entries, router, sourceType, project, projectPath, distDir } = opts; const isAppDirectory = router === 'app'; const isServer = sourceType === 'server'; const isEdgeServer = sourceType === 'edge-server'; const ctx = { bundler: 'turbopack', project, projectPath, isServer, isEdgeServer, isAppDirectory }; await handleLog(entries, ctx, distDir, opts.config); } //# sourceMappingURL=receive-logs.js.map