.
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
// Logging server and browser logs to a file
|
||||
export class FileLogger {
|
||||
initialize(distDir, mcpServerEnabled) {
|
||||
this.logFilePath = path.join(distDir, 'logs', `next-development.log`);
|
||||
this.mcpServerEnabled = mcpServerEnabled;
|
||||
if (this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
// Only initialize if mcpServer is enabled
|
||||
if (!this.mcpServerEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Clean up the log file on each initialization
|
||||
// ensure the directory exists
|
||||
fs.mkdirSync(path.dirname(this.logFilePath), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(this.logFilePath, '');
|
||||
this.isInitialized = true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
formatTimestamp() {
|
||||
// Use performance.now() instead of Date.now() for avoid sync IO of cache components
|
||||
const now = performance.now();
|
||||
const hours = Math.floor(now / 3600000).toString().padStart(2, '0');
|
||||
const minutes = Math.floor(now % 3600000 / 60000).toString().padStart(2, '0');
|
||||
const seconds = Math.floor(now % 60000 / 1000).toString().padStart(2, '0');
|
||||
const milliseconds = Math.floor(now % 1000).toString().padStart(3, '0');
|
||||
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
|
||||
}
|
||||
formatLogEntry(entry) {
|
||||
const { timestamp, source, level, message } = entry;
|
||||
return JSON.stringify({
|
||||
timestamp,
|
||||
source,
|
||||
level,
|
||||
message
|
||||
}) + '\n';
|
||||
}
|
||||
scheduleFlush() {
|
||||
// Debounce the flush
|
||||
if (this.flushTimer) {
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
// Delay the log flush to ensure more logs can be batched together asynchronously
|
||||
this.flushTimer = setTimeout(()=>{
|
||||
this.flush();
|
||||
}, 100);
|
||||
}
|
||||
getLogQueue() {
|
||||
return this.logQueue;
|
||||
}
|
||||
flush() {
|
||||
if (this.logQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Only flush to disk if mcpServer is enabled
|
||||
if (!this.mcpServerEnabled) {
|
||||
this.logQueue.length = 0 // Clear the queue without GC overhead
|
||||
;
|
||||
this.flushTimer = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Ensure the directory exists before writing
|
||||
const logDir = path.dirname(this.logFilePath);
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, {
|
||||
recursive: true
|
||||
});
|
||||
}
|
||||
const logsToWrite = this.logQueue.join('');
|
||||
// Writing logs to files synchronously to ensure they're written before returning
|
||||
fs.appendFileSync(this.logFilePath, logsToWrite);
|
||||
this.logQueue.length = 0 // Clear the queue without GC overhead
|
||||
;
|
||||
} catch (error) {
|
||||
console.error('Failed to flush logs to file:', error);
|
||||
} finally{
|
||||
this.flushTimer = null;
|
||||
}
|
||||
}
|
||||
enqueueLog(formattedEntry) {
|
||||
this.logQueue.push(formattedEntry);
|
||||
// Cancel existing timer and start a new one to ensure all logs are flushed together
|
||||
if (this.flushTimer) {
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
this.scheduleFlush();
|
||||
}
|
||||
log(source, level, message) {
|
||||
// Don't log anything if mcpServer is disabled
|
||||
if (!this.mcpServerEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.isInitialized) {
|
||||
return;
|
||||
}
|
||||
const logEntry = {
|
||||
timestamp: this.formatTimestamp(),
|
||||
source,
|
||||
level,
|
||||
message
|
||||
};
|
||||
const formattedEntry = this.formatLogEntry(logEntry);
|
||||
this.enqueueLog(formattedEntry);
|
||||
}
|
||||
logServer(level, message) {
|
||||
this.log('Server', level, message);
|
||||
}
|
||||
logBrowser(level, message) {
|
||||
this.log('Browser', level, message);
|
||||
}
|
||||
// Force flush all queued logs immediately
|
||||
forceFlush() {
|
||||
if (this.flushTimer) {
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
this.flush();
|
||||
}
|
||||
// Cleanup method to flush logs on process exit
|
||||
destroy() {
|
||||
this.forceFlush();
|
||||
}
|
||||
constructor(){
|
||||
this.logFilePath = '';
|
||||
this.isInitialized = false;
|
||||
this.logQueue = [];
|
||||
this.flushTimer = null;
|
||||
this.mcpServerEnabled = false;
|
||||
}
|
||||
}
|
||||
// Singleton instance
|
||||
let fileLogger = null;
|
||||
export function getFileLogger() {
|
||||
if (!fileLogger || process.env.NODE_ENV === 'test') {
|
||||
fileLogger = new FileLogger();
|
||||
}
|
||||
return fileLogger;
|
||||
}
|
||||
// Only used for testing
|
||||
export function test__resetFileLogger() {
|
||||
if (fileLogger) {
|
||||
fileLogger.destroy();
|
||||
}
|
||||
fileLogger = null;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=file-logger.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+521
@@ -0,0 +1,521 @@
|
||||
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';
|
||||
import { formatConsoleArgs } from '../../../client/lib/console';
|
||||
import { getFileLogger } from './file-logger';
|
||||
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;
|
||||
}
|
||||
function cleanConsoleArgsForFileLogging(args) {
|
||||
/**
|
||||
* Use formatConsoleArgs to strip out background and color format specifiers
|
||||
* and keep only the original string content for file logging
|
||||
*/ try {
|
||||
return formatConsoleArgs(args);
|
||||
} catch {
|
||||
// Fallback to simple string conversion if formatting fails
|
||||
return args.map((arg)=>typeof arg === 'string' ? arg : util.inspect(arg, {
|
||||
depth: 2
|
||||
})).join(' ');
|
||||
}
|
||||
}
|
||||
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, isServerLog) {
|
||||
const consoleArgs = await prepareConsoleArgs(entry, ctx, distDir);
|
||||
const withStackEntry = await withLocation({
|
||||
original: consoleArgs,
|
||||
stack: entry.consoleMethodStack || null
|
||||
}, ctx, distDir, config);
|
||||
const consoleMethod = forwardConsole[entry.method] || forwardConsole.log;
|
||||
consoleMethod(browserPrefix, ...withStackEntry);
|
||||
// Process enqueued logs and write to file
|
||||
// Log to file with correct source based on context
|
||||
const fileLogger = getFileLogger();
|
||||
// Use cleaned console args to strip out background and color format specifiers
|
||||
const message = cleanConsoleArgsForFileLogging(consoleArgs);
|
||||
if (isServerLog) {
|
||||
fileLogger.logServer(entry.method.toUpperCase(), message);
|
||||
} else {
|
||||
fileLogger.logBrowser(entry.method.toUpperCase(), message);
|
||||
}
|
||||
}
|
||||
// Log levels from most severe to least severe
|
||||
// Lower index = more severe
|
||||
const LOG_LEVEL_PRIORITY = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
verbose: 2
|
||||
};
|
||||
// Map console methods to log levels
|
||||
const METHOD_TO_LEVEL = {
|
||||
error: 'error',
|
||||
warn: 'warn',
|
||||
info: 'verbose',
|
||||
log: 'verbose',
|
||||
debug: 'verbose',
|
||||
table: 'verbose',
|
||||
trace: 'verbose',
|
||||
dir: 'verbose',
|
||||
dirxml: 'verbose',
|
||||
assert: 'error',
|
||||
group: 'verbose',
|
||||
groupCollapsed: 'verbose',
|
||||
groupEnd: 'verbose'
|
||||
};
|
||||
function shouldShowEntry(entry, config) {
|
||||
// If config is false, don't show any entries
|
||||
if (config === false) {
|
||||
return false;
|
||||
}
|
||||
// Determine the effective minimum log level
|
||||
const minLevel = typeof config === 'string' ? config : 'verbose' // true means show everything
|
||||
;
|
||||
const minPriority = LOG_LEVEL_PRIORITY[minLevel];
|
||||
// formatted-error and any-logged-error are always treated as errors
|
||||
if (entry.kind === 'formatted-error' || entry.kind === 'any-logged-error') {
|
||||
return LOG_LEVEL_PRIORITY['error'] <= minPriority;
|
||||
}
|
||||
if (entry.kind === 'console') {
|
||||
const entryLevel = METHOD_TO_LEVEL[entry.method] || 'log';
|
||||
return LOG_LEVEL_PRIORITY[entryLevel] <= minPriority;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
export async function handleLog(entries, ctx, distDir, config) {
|
||||
// Determine the source based on the context
|
||||
const isServerLog = ctx.isServer || ctx.isEdgeServer;
|
||||
const browserPrefix = isServerLog ? cyan('[server]') : cyan('[browser]');
|
||||
const fileLogger = getFileLogger();
|
||||
for (const entry of entries){
|
||||
// Filter entries based on config mode
|
||||
if (!shouldShowEntry(entry, config)) {
|
||||
continue;
|
||||
}
|
||||
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, isServerLog);
|
||||
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);
|
||||
// Process enqueued logs and write to file
|
||||
fileLogger.logBrowser('ERROR', cleanConsoleArgsForFileLogging(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);
|
||||
// Process enqueued logs and write to file
|
||||
fileLogger.logBrowser('ERROR', cleanConsoleArgsForFileLogging(formattedArgs));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{}
|
||||
}
|
||||
} catch {
|
||||
switch(entry.kind){
|
||||
case 'any-logged-error':
|
||||
{
|
||||
const consoleArgs = await prepareConsoleErrorArgs(entry, ctx, distDir);
|
||||
forwardConsole.error(browserPrefix, ...consoleArgs);
|
||||
// Process enqueued logs and write to file
|
||||
fileLogger.logBrowser('ERROR', cleanConsoleArgsForFileLogging(consoleArgs));
|
||||
break;
|
||||
}
|
||||
case 'console':
|
||||
{
|
||||
const consoleMethod = forwardConsole[entry.method] || forwardConsole.log;
|
||||
const consoleArgs = await prepareConsoleArgs(entry, ctx, distDir);
|
||||
consoleMethod(browserPrefix, ...consoleArgs);
|
||||
// Process enqueued logs and write to file
|
||||
fileLogger.logBrowser('ERROR', cleanConsoleArgsForFileLogging(consoleArgs));
|
||||
break;
|
||||
}
|
||||
case 'formatted-error':
|
||||
{
|
||||
forwardConsole.error(browserPrefix, `${entry.prefix}\n`, entry.stack);
|
||||
// Process enqueued logs and write to file
|
||||
fileLogger.logBrowser('ERROR', cleanConsoleArgsForFileLogging([
|
||||
`${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);
|
||||
}
|
||||
// Handle client file logs (always logged regardless of terminal flag)
|
||||
export async function handleClientFileLogs(logs) {
|
||||
const fileLogger = getFileLogger();
|
||||
for (const log of logs){
|
||||
fileLogger.logBrowser(log.level, log.message);
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=receive-logs.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+190
@@ -0,0 +1,190 @@
|
||||
import { getOriginalStackFrames as getOriginalStackFramesWebpack } from '../middleware-webpack';
|
||||
import { getOriginalStackFrames as getOriginalStackFramesTurbopack } from '../middleware-turbopack';
|
||||
import { dim } from '../../../lib/picocolors';
|
||||
import { parseStack } from '../../lib/parse-stack';
|
||||
import path from 'path';
|
||||
import { LRUCache } from '../../lib/lru-cache';
|
||||
// TODO: handle server vs browser error source mapping correctly
|
||||
export async function mapFramesUsingBundler(frames, ctx) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// converts _next/static/chunks/... to file:///.next/static/chunks/... for parseStack
|
||||
// todo: where does next dev overlay handle this case and re-use that logic
|
||||
function preprocessStackTrace(stackTrace, distDir) {
|
||||
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(25);
|
||||
async function getSourceMappedStackFramesInternal(stackTrace, ctx, distDir, ignore = true) {
|
||||
try {
|
||||
var _filteredFrames_find;
|
||||
const normalizedStack = preprocessStackTrace(stackTrace, distDir);
|
||||
const frames = parseStack(normalizedStack, distDir);
|
||||
if (frames.length === 0) {
|
||||
return {
|
||||
kind: 'stack',
|
||||
stack: stackTrace
|
||||
};
|
||||
}
|
||||
const mappingResults = await mapFramesUsingBundler(frames, ctx);
|
||||
const processedFrames = mappingResults.map((result, index)=>({
|
||||
result,
|
||||
originalFrame: frames[index]
|
||||
})).map(({ result, originalFrame })=>{
|
||||
var _originalStackFrame_file;
|
||||
if (result.status === 'rejected') {
|
||||
return {
|
||||
kind: 'rejected',
|
||||
frameText: formatStackFrame(originalFrame),
|
||||
codeFrame: null
|
||||
};
|
||||
}
|
||||
const { originalStackFrame, originalCodeFrame } = result.value;
|
||||
if ((originalStackFrame == null ? void 0 : originalStackFrame.ignored) && ignore) {
|
||||
return {
|
||||
kind: 'ignored'
|
||||
};
|
||||
}
|
||||
// should we apply this generally to dev overlay (dev overlay does not ignore chrome-extension://)
|
||||
if (originalStackFrame == null ? void 0 : (_originalStackFrame_file = originalStackFrame.file) == null ? void 0 : _originalStackFrame_file.startsWith('chrome-extension://')) {
|
||||
return {
|
||||
kind: 'ignored'
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: 'success',
|
||||
// invariant: if result is not rejected and not ignored, then original stack frame exists
|
||||
// verifiable by tracing `getOriginalStackFrame`. The invariant exists because of bad types
|
||||
frameText: formatStackFrame(originalStackFrame),
|
||||
codeFrame: originalCodeFrame
|
||||
};
|
||||
});
|
||||
const allIgnored = processedFrames.every((frame)=>frame.kind === 'ignored');
|
||||
// we want to handle **all** ignored vs all/some rejected differently
|
||||
// if all are ignored we should show no frames
|
||||
// if all are rejected, we want to fallback to showing original stack frames
|
||||
if (allIgnored) {
|
||||
return {
|
||||
kind: 'all-ignored'
|
||||
};
|
||||
}
|
||||
const filteredFrames = processedFrames.filter((frame)=>frame.kind !== 'ignored');
|
||||
if (filteredFrames.length === 0) {
|
||||
return {
|
||||
kind: 'stack',
|
||||
stack: stackTrace
|
||||
};
|
||||
}
|
||||
const stackOutput = filteredFrames.map((frame)=>frame.frameText).join('\n');
|
||||
const firstFrameCode = (_filteredFrames_find = filteredFrames.find((frame)=>frame.codeFrame)) == null ? void 0 : _filteredFrames_find.codeFrame;
|
||||
if (firstFrameCode) {
|
||||
return {
|
||||
kind: 'with-frame-code',
|
||||
frameCode: firstFrameCode,
|
||||
stack: stackOutput,
|
||||
frames: filteredFrames
|
||||
};
|
||||
}
|
||||
// i don't think this a real case, but good for exhaustion
|
||||
return {
|
||||
kind: 'mapped-stack',
|
||||
stack: stackOutput,
|
||||
frames: filteredFrames
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
kind: 'stack',
|
||||
stack: stackTrace
|
||||
};
|
||||
}
|
||||
}
|
||||
// todo: cache the actual async call, not the wrapper with post processing
|
||||
export async function getSourceMappedStackFrames(stackTrace, ctx, distDir, 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) {
|
||||
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})`;
|
||||
}
|
||||
// appends the source mapped location of the console method
|
||||
export const withLocation = async ({ original, stack }, ctx, distDir, _config)=>{
|
||||
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)=>{
|
||||
if (mapped.kind !== 'mapped-stack' && mapped.kind !== 'with-frame-code') {
|
||||
return null;
|
||||
}
|
||||
const first = mapped.frames.at(0);
|
||||
if (!first) {
|
||||
return null;
|
||||
}
|
||||
// we don't want to show the name of parent function (at <fn> thing in stack), just source location for minimal noise
|
||||
const match = first.frameText.match(/\(([^)]+)\)/);
|
||||
const locationText = match ? match[1] : first.frameText;
|
||||
return locationText;
|
||||
};
|
||||
|
||||
//# sourceMappingURL=source-map.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user