next
Version:
The React Framework
236 lines (235 loc) • 9.93 kB
JavaScript
import { dim } from '../../lib/picocolors';
import { consoleAsyncStorage } from '../app-render/console-async-storage.external';
import { workUnitAsyncStorage } from '../app-render/work-unit-async-storage.external';
import { getServerReact, getClientReact } from '../runtime-reacts.external';
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we may use later and want parity with the HIDDEN_STYLE value
const DIMMED_STYLE = 'dimmed';
const HIDDEN_STYLE = 'hidden';
let currentAbortedLogsStyle = 'dimmed';
export function setAbortedLogsStyle(style) {
currentAbortedLogsStyle = style;
}
const isColorSupported = dim('test') !== 'test';
// 50% opacity for dimmed text
const dimStyle = 'color: color(from currentColor xyz x y z / 0.5);';
const reactBadgeFormat = '\x1b[0m\x1b[7m%c%s\x1b[0m%c ';
function dimmedConsoleArgs(...inputArgs) {
if (!isColorSupported) {
return inputArgs;
}
const newArgs = inputArgs.slice(0);
let template = '';
let argumentsPointer = 0;
if (typeof inputArgs[0] === 'string') {
const originalTemplateString = inputArgs[0];
// Remove the original template string from the args.
newArgs.splice(argumentsPointer, 1);
argumentsPointer += 1;
let i = 0;
if (originalTemplateString.startsWith(reactBadgeFormat)) {
i = reactBadgeFormat.length;
// for `format` we already moved the pointer earlier
// style, badge, reset style
argumentsPointer += 3;
template += reactBadgeFormat;
// React's badge reset styles, reapply dimming
template += '\x1b[2m%c';
// argumentsPointer includes template
newArgs.splice(argumentsPointer - 1, 0, dimStyle);
// dim the badge
newArgs[0] += `;${dimStyle}`;
}
for(i; i < originalTemplateString.length; i++){
const currentChar = originalTemplateString[i];
if (currentChar !== '%') {
template += currentChar;
continue;
}
const nextChar = originalTemplateString[i + 1];
++i;
switch(nextChar){
case 'f':
case 'O':
case 'o':
case 'd':
case 's':
case 'i':
case 'c':
++argumentsPointer;
template += `%${nextChar}`;
break;
default:
template += `%${nextChar}`;
}
}
}
for(argumentsPointer; argumentsPointer < inputArgs.length; ++argumentsPointer){
const arg = inputArgs[argumentsPointer];
const argType = typeof arg;
if (argumentsPointer > 0) {
template += ' ';
}
switch(argType){
case 'boolean':
case 'string':
template += '%s';
break;
case 'bigint':
template += '%s';
break;
case 'number':
if (arg % 0) {
template += '%f';
} else {
template += '%d';
}
break;
case 'object':
template += '%O';
break;
case 'symbol':
case 'undefined':
case 'function':
template += '%s';
break;
default:
// deopt to string for new, unknown types
template += '%s';
}
}
template += '\x1b[22m';
return [
dim(`%c${template}`),
dimStyle,
...newArgs
];
}
function convertToDimmedArgs(methodName, args) {
switch(methodName){
case 'dir':
case 'dirxml':
case 'group':
case 'groupCollapsed':
case 'groupEnd':
case 'table':
{
// These methods cannot be colorized because they don't take a formatting string.
return args;
}
case 'assert':
{
// assert takes formatting options as the second argument.
return [
args[0]
].concat(...dimmedConsoleArgs(args[1], ...args.slice(2)));
}
case 'error':
case 'debug':
case 'info':
case 'log':
case 'trace':
case 'warn':
return dimmedConsoleArgs(args[0], ...args.slice(1));
default:
return methodName;
}
}
// Based on https://github.com/facebook/react/blob/28dc0776be2e1370fe217549d32aee2519f0cf05/packages/react-server/src/ReactFlightServer.js#L248
function patchConsoleMethod(methodName) {
const descriptor = Object.getOwnPropertyDescriptor(console, methodName);
if (descriptor && (descriptor.configurable || descriptor.writable) && typeof descriptor.value === 'function') {
const originalMethod = descriptor.value;
const originalName = Object.getOwnPropertyDescriptor(originalMethod, 'name');
const wrapperMethod = function(...args) {
var _getClientReact, _getServerReact;
const consoleStore = consoleAsyncStorage.getStore();
// First we see if there is a cache signal for our current scope. If we're in a client render it'll
// come from the client React cacheSignal implementation. If we are in a server render it'll come from
// the server React cacheSignal implementation. Any particular console call will be in one, the other, or neither
// scope and these signals return null if you are out of scope so this can be called from a single global patch
// and still work properly.
const signal = ((_getClientReact = getClientReact()) == null ? void 0 : _getClientReact.cacheSignal()) ?? ((_getServerReact = getServerReact()) == null ? void 0 : _getServerReact.cacheSignal());
if (signal) {
// We are in a React Server render and can consult the React cache signal to determine if logs
// are now dimmable.
if (signal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return;
}
return applyWithDimming.call(this, consoleStore, originalMethod, methodName, args);
} else if ((consoleStore == null ? void 0 : consoleStore.dim) === true) {
return applyWithDimming.call(this, consoleStore, originalMethod, methodName, args);
} else {
return originalMethod.apply(this, args);
}
}
// We need to fall back to checking the work unit store for two reasons.
// 1. Client React does not yet implement cacheSignal (it always returns null)
// 2. route.ts files aren't rendered with React but do have prerender semantics
// TODO in the future we should be able to remove this once there is a runnable cache
// scope independent of actual React rendering.
const workUnitStore = workUnitAsyncStorage.getStore();
switch(workUnitStore == null ? void 0 : workUnitStore.type){
case 'prerender':
case 'prerender-runtime':
// These can be hit in a route handler. In the future we can use potential React.createCache API
// to create a cache scope for arbitrary computation and can move over to cacheSignal exclusively.
// fallthrough
case 'prerender-client':
// This is a react-dom/server render and won't have a cacheSignal until React adds this for the client world.
const renderSignal = workUnitStore.renderSignal;
if (renderSignal.aborted) {
if (currentAbortedLogsStyle === HIDDEN_STYLE) {
return;
}
return applyWithDimming.call(this, consoleStore, originalMethod, methodName, args);
}
// intentional fallthrough
case 'prerender-legacy':
case 'prerender-ppr':
case 'cache':
case 'unstable-cache':
case 'private-cache':
case 'request':
case undefined:
if ((consoleStore == null ? void 0 : consoleStore.dim) === true) {
return applyWithDimming.call(this, consoleStore, originalMethod, methodName, args);
} else {
return originalMethod.apply(this, args);
}
default:
workUnitStore;
}
};
if (originalName) {
Object.defineProperty(wrapperMethod, 'name', originalName);
}
Object.defineProperty(console, methodName, {
value: wrapperMethod
});
}
}
function applyWithDimming(consoleStore, method, methodName, args) {
if ((consoleStore == null ? void 0 : consoleStore.dim) === true) {
return method.apply(this, convertToDimmedArgs(methodName, args));
} else {
return consoleAsyncStorage.run(DIMMED_STORE, method.bind(this, ...convertToDimmedArgs(methodName, args)));
}
}
const DIMMED_STORE = {
dim: true
};
patchConsoleMethod('error');
patchConsoleMethod('assert');
patchConsoleMethod('debug');
patchConsoleMethod('dir');
patchConsoleMethod('dirxml');
patchConsoleMethod('group');
patchConsoleMethod('groupCollapsed');
patchConsoleMethod('groupEnd');
patchConsoleMethod('info');
patchConsoleMethod('log');
patchConsoleMethod('table');
patchConsoleMethod('trace');
patchConsoleMethod('warn');
//# sourceMappingURL=console-dim.external.js.map