From f36a666fd3feb057ec8b489e174d32c4b63e250c Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 5 Feb 2024 08:56:04 +0000 Subject: [PATCH 1/2] decode magic identifiers when printing compile errors to the console --- .../lib/router-utils/setup-dev-bundler.ts | 22 ++++- .../next/src/shared/lib/magic-identifier.ts | 95 +++++++++++++++++++ 2 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 packages/next/src/shared/lib/magic-identifier.ts diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 0e22c87f0b87f..a5403300ee805 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -131,12 +131,16 @@ import type { ActionManifest } from '../../../build/webpack/plugins/flight-clien import { denormalizePagePath } from '../../../shared/lib/page-path/denormalize-page-path' import type { LoadableManifest } from '../../load-components' import { generateRandomActionKeyRaw } from '../../app-render/action-encryption-utils' -import { bold, green, red } from '../../../lib/picocolors' +import { bold, green, magenta, red } from '../../../lib/picocolors' import { writeFileAtomic } from '../../../lib/fs/write-atomic' import { PAGE_TYPES } from '../../../lib/page-types' import { trace } from '../../../trace' import type { VersionInfo } from '../../dev/parse-version-info' import type { NextFontManifest } from '../../../build/webpack/plugins/next-font-manifest-plugin' +import { + MAGIC_IDENTIFIER_REGEX, + decodeMagicIdentifier, +} from '../../../shared/lib/magic-identifier' const MILLISECONDS_IN_NANOSECOND = 1_000_000 const wsServer = new ws.Server({ noServer: true }) @@ -2679,13 +2683,23 @@ export async function setupDevBundler(opts: SetupOpts) { export type DevBundler = Awaited> function renderStyledStringToErrorAnsi(string: StyledString): string { + function decodeMagicIdentifiers(str: string): string { + return str.replaceAll(MAGIC_IDENTIFIER_REGEX, (ident) => { + try { + return magenta(`{${decodeMagicIdentifier(ident)}}`) + } catch (e) { + return magenta(`{${ident} (decoding failed: ${e})}`) + } + }) + } + switch (string.type) { case 'text': - return string.value + return decodeMagicIdentifiers(string.value) case 'strong': - return bold(red(string.value)) + return bold(red(decodeMagicIdentifiers(string.value))) case 'code': - return green(string.value) + return green(decodeMagicIdentifiers(string.value)) case 'line': return string.value.map(renderStyledStringToErrorAnsi).join('') case 'stack': diff --git a/packages/next/src/shared/lib/magic-identifier.ts b/packages/next/src/shared/lib/magic-identifier.ts new file mode 100644 index 0000000000000..5d52b5b763345 --- /dev/null +++ b/packages/next/src/shared/lib/magic-identifier.ts @@ -0,0 +1,95 @@ +function decodeHex(hexStr: string): string { + if (hexStr.trim() === '') { + throw new Error("can't decode empty hex") + } + + const num = parseInt(hexStr, 16) + if (isNaN(num)) { + throw new Error(`invalid hex: \`${hexStr}\``) + } + + return String.fromCodePoint(num) +} + +const enum Mode { + Text, + Underscore, + Hex, + LongHex, +} + +const DECODE_REGEX = /^__TURBOPACK__([a-zA-Z0-9_$]+)__$/ + +export function decodeMagicIdentifier(identifier: string): string { + const matches = identifier.match(DECODE_REGEX) + if (!matches) { + return identifier + } + + const inner = matches[1] + + let output = '' + + let mode: Mode = Mode.Text + let buffer = '' + for (let i = 0; i < inner.length; i++) { + const char = inner[i] + + if (mode === Mode.Text) { + if (char === '_') { + mode = Mode.Underscore + } else if (char === '$') { + mode = Mode.Hex + } else { + output += char + } + } else if (mode === Mode.Underscore) { + if (char === '_') { + output += ' ' + mode = Mode.Text + } else if (char === '$') { + output += '_' + mode = Mode.Hex + } else { + output += char + mode = Mode.Text + } + } else if (mode === Mode.Hex) { + if (buffer.length === 2) { + output += decodeHex(buffer) + buffer = '' + } + + if (char === '_') { + if (buffer !== '') { + throw new Error(`invalid hex: \`${buffer}\``) + } + + mode = Mode.LongHex + } else if (char === '$') { + if (buffer !== '') { + throw new Error(`invalid hex: \`${buffer}\``) + } + + mode = Mode.Text + } else { + buffer += char + } + } else if (mode === Mode.LongHex) { + if (char === '_') { + throw new Error(`invalid hex: \`${buffer + char}\``) + } else if (char === '$') { + output += decodeHex(buffer) + buffer = '' + + mode = Mode.Text + } else { + buffer += char + } + } + } + + return output +} + +export const MAGIC_IDENTIFIER_REGEX = /__TURBOPACK__[a-zA-Z0-9_$]+__/g From 49af7a3473488283467d3db5dc10517247575195 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Tue, 6 Feb 2024 08:47:42 +0000 Subject: [PATCH 2/2] decode magic identifers in error overlay --- .../components/CodeFrame/CodeFrame.tsx | 4 +- .../get-words-and-whitespaces.test.ts | 17 ------- .../get-words-and-whitespaces.ts | 38 -------------- .../components/hot-linked-text/index.tsx | 50 +++++++++++++------ .../container/RuntimeError/CallStackFrame.tsx | 3 +- .../RuntimeError/ComponentStackFrameRow.tsx | 5 +- .../ReactRefreshLogBox.test.ts.snap | 2 +- 7 files changed, 46 insertions(+), 73 deletions(-) delete mode 100644 packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/get-words-and-whitespaces.test.ts delete mode 100644 packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/get-words-and-whitespaces.ts diff --git a/packages/next/src/client/components/react-dev-overlay/internal/components/CodeFrame/CodeFrame.tsx b/packages/next/src/client/components/react-dev-overlay/internal/components/CodeFrame/CodeFrame.tsx index 3c656cca0f90e..ae511a1cb73cd 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/components/CodeFrame/CodeFrame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/components/CodeFrame/CodeFrame.tsx @@ -4,6 +4,7 @@ import type { StackFrame } from 'next/dist/compiled/stacktrace-parser' import stripAnsi from 'next/dist/compiled/strip-ansi' import { getFrameSource } from '../../helpers/stack-frame' import { useOpenInEditor } from '../../helpers/use-open-in-editor' +import { HotlinkedText } from '../hot-linked-text' export type CodeFrameProps = { stackFrame: StackFrame; codeFrame: string } @@ -66,7 +67,8 @@ export const CodeFrame: React.FC = function CodeFrame({ title="Click to open in your editor" > - {getFrameSource(stackFrame)} @ {stackFrame.methodName} + {getFrameSource(stackFrame)} @{' '} + { - it('should return sequences of words and whitespaces', () => { - const text = ' \n\nhello world https://nextjs.org/\nhttps://nextjs.org/' - expect(getWordsAndWhitespaces(text)).toEqual([ - ' \n\n', - 'hello', - ' ', - 'world', - ' ', - 'https://nextjs.org/', - '\n', - 'https://nextjs.org/', - ]) - }) -}) diff --git a/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/get-words-and-whitespaces.ts b/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/get-words-and-whitespaces.ts deleted file mode 100644 index 22a03fa6eaae0..0000000000000 --- a/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/get-words-and-whitespaces.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Returns true if the given character is a whitespace character, false otherwise. -function isWhitespace(char: string): boolean { - return char === ' ' || char === '\n' || char === '\t' || char === '\r' -} - -/** - * Get sequences of words and whitespaces from a string. - * - * e.g. "Hello world \n\n" -> ["Hello", " ", "world", " \n\n"] - */ -export function getWordsAndWhitespaces(text: string) { - const wordsAndWhitespaces: string[] = [] - - let current = '' - let currentIsWhitespace = false - for (const char of text) { - if (current.length === 0) { - current += char - currentIsWhitespace = isWhitespace(char) - continue - } - - const nextIsWhitespace = isWhitespace(char) - if (currentIsWhitespace === nextIsWhitespace) { - current += char - } else { - wordsAndWhitespaces.push(current) - current = char - currentIsWhitespace = nextIsWhitespace - } - } - - if (current.length > 0) { - wordsAndWhitespaces.push(current) - } - - return wordsAndWhitespaces -} diff --git a/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/index.tsx b/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/index.tsx index 2dde5aba96952..7e5f338c21233 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/index.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/components/hot-linked-text/index.tsx @@ -1,29 +1,51 @@ import React from 'react' -import { getWordsAndWhitespaces } from './get-words-and-whitespaces' +import { + decodeMagicIdentifier, + MAGIC_IDENTIFIER_REGEX, +} from '../../../../../../shared/lib/magic-identifier' const linkRegex = /https?:\/\/[^\s/$.?#].[^\s"]*/i +const splitRegexp = new RegExp(`(${MAGIC_IDENTIFIER_REGEX.source}|\\s+)`) + export const HotlinkedText: React.FC<{ text: string }> = function HotlinkedText(props) { const { text } = props - const wordsAndWhitespaces = getWordsAndWhitespaces(text) + const wordsAndWhitespaces = text.split(splitRegexp) return ( <> - {linkRegex.test(text) - ? wordsAndWhitespaces.map((word, index) => { - if (linkRegex.test(word)) { - return ( - - {word} - - ) - } - return {word} - }) - : text} + {wordsAndWhitespaces.map((word, index) => { + if (linkRegex.test(word)) { + return ( + + {word} + + ) + } + try { + const decodedWord = decodeMagicIdentifier(word) + if (decodedWord !== word) { + return ( + + {'{'} + {decodedWord} + {'}'} + + ) + } + } catch (e) { + return ( + + {'{'} + {word} (decoding failed: {'' + e}){'}'} + + ) + } + return {word} + })} ) } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx index 3d4e042edbf1d..8b5613229f79b 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx @@ -5,6 +5,7 @@ import { type OriginalStackFrame, } from '../../helpers/stack-frame' import { useOpenInEditor } from '../../helpers/use-open-in-editor' +import { HotlinkedText } from '../../components/hot-linked-text' export const CallStackFrame: React.FC<{ frame: OriginalStackFrame @@ -27,7 +28,7 @@ export const CallStackFrame: React.FC<{ return (

- {f.methodName} +

-

{component}

+

+ +

) diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap index f141a77eb9e80..42a0e0d1f1a84 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -118,7 +118,7 @@ https://nextjs.org/docs/messages/module-not-found" `; exports[`ReactRefreshLogBox app turbo Should not show __webpack_exports__ when exporting anonymous arrow function 1`] = ` -"index.js (3:2) @ __TURBOPACK__default__export__ +"index.js (3:2) @ {default export} 1 | export default () => { 2 | if (typeof window !== 'undefined') {