Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable opening source file in build error overlay #48194

Merged
merged 4 commits into from Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,22 +1,37 @@
import React from 'react'
import { useOpenInEditor } from '../../helpers/use-open-in-editor'

export function EditorLink({ file }: { file: string }) {
type EditorLinkProps = {
file: string
isSourceFile: boolean
location?: {
line: number
column: number
}
}
export function EditorLink({ file, isSourceFile, location }: EditorLinkProps) {
const open = useOpenInEditor({
file,
column: 1,
lineNumber: 1,
lineNumber: location?.line ?? 1,
column: location?.column ?? 0,
})

return (
<div
data-with-open-in-editor-link
data-with-open-in-editor-link-source-file={
isSourceFile ? true : undefined
}
data-with-open-in-editor-link-import-trace={
isSourceFile ? undefined : true
}
tabIndex={10}
role={'link'}
onClick={open}
title={'Click to open in your editor'}
>
{file}
{location ? ` (${location.line}:${location.column})` : null}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
Expand Down
Expand Up @@ -5,14 +5,31 @@ import { EditorLink } from './EditorLink'

export type TerminalProps = { content: string }

function getImportTraceFiles(content: string): [string, string[]] {
function getFile(lines: string[]) {
const contentFileName = lines.shift()
if (!contentFileName) return null
const [fileName, line, column] = contentFileName.split(':')

const parsedLine = Number(line)
const parsedColumn = Number(column)
const hasLocation = !Number.isNaN(parsedLine) && !Number.isNaN(parsedColumn)

return {
fileName: hasLocation ? fileName : contentFileName,
location: hasLocation
? {
line: parsedLine,
column: parsedColumn,
}
: undefined,
}
}

function getImportTraceFiles(lines: string[]) {
if (
/ReactServerComponentsError:/.test(content) ||
/Import trace for requested module:/.test(content)
lines.some((line) => /ReactServerComponentsError:/.test(line)) ||
lines.some((line) => /Import trace for requested module:/.test(line))
) {
// It's an RSC Build Error
const lines = content.split('\n')

// Grab the lines at the end containing the files
const files = []
while (
Expand All @@ -23,17 +40,25 @@ function getImportTraceFiles(content: string): [string, string[]] {
files.unshift(file)
}

return [lines.join('\n'), files]
return files
}

return [content, []]
return []
}

function getEditorLinks(content: string) {
const lines = content.split('\n')
const file = getFile(lines)
const importTraceFiles = getImportTraceFiles(lines)

return { file, source: lines.join('\n'), importTraceFiles }
}

export const Terminal: React.FC<TerminalProps> = function Terminal({
content,
}) {
const [source, editorLinks] = React.useMemo(
() => getImportTraceFiles(content),
const { file, source, importTraceFiles } = React.useMemo(
() => getEditorLinks(content),
[content]
)

Expand All @@ -47,6 +72,14 @@ export const Terminal: React.FC<TerminalProps> = function Terminal({

return (
<div data-nextjs-terminal>
{file && (
<EditorLink
isSourceFile
key={file.fileName}
file={file.fileName}
location={file.location}
/>
)}
<pre>
{decoded.map((entry, index) => (
<span
Expand All @@ -63,8 +96,12 @@ export const Terminal: React.FC<TerminalProps> = function Terminal({
<HotlinkedText text={entry.content} />
</span>
))}
{editorLinks.map((file) => (
<EditorLink key={file} file={file} />
{importTraceFiles.map((importTraceFile) => (
<EditorLink
isSourceFile={false}
key={importTraceFile}
file={importTraceFile}
/>
))}
</pre>
</div>
Expand Down
Expand Up @@ -37,7 +37,13 @@ const styles = css`
[data-with-open-in-editor-link]:hover {
text-decoration: underline dotted;
}
[data-with-open-in-editor-link] {
[data-with-open-in-editor-link-source-file] {
border-bottom: 1px solid var(--color-ansi-bright-black);
display: flex;
align-items: center;
justify-content: space-between;
}
[data-with-open-in-editor-link-import-trace] {
margin-left: var(--size-gap-double);
}
[data-nextjs-terminal] a {
Expand Down
Expand Up @@ -323,7 +323,7 @@ for (const variant of ['default', 'turbo']) {
await session.patch('index.module.css', `.button {`)
expect(await session.hasRedbox(true)).toBe(true)
const source = await session.getRedboxSource()
expect(source).toMatch('./index.module.css:1:1')
expect(source).toMatch('./index.module.css (1:1)')
expect(source).toMatch('Syntax error: ')
expect(source).toMatch('Unclosed block')
expect(source).toMatch('> 1 | .button {')
Expand Down
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ReactRefreshLogBox app default Module not found 1`] = `
"./index.js:1:0
"./index.js (1:0)
Module not found: Can't resolve 'b'
> 1 | import Comp from 'b'
2 | export default function Oops() {
Expand All @@ -15,7 +15,7 @@ Import trace for requested module:
`;

exports[`ReactRefreshLogBox app default Module not found empty import trace 1`] = `
"./app/page.js:2:6
"./app/page.js (2:6)
Module not found: Can't resolve 'b'
1 | 'use client'
> 2 | import Comp from 'b'
Expand All @@ -41,7 +41,7 @@ https://nextjs.org/docs/messages/module-not-found"
`;

exports[`ReactRefreshLogBox app default Node.js builtins 1`] = `
"./node_modules/my-package/index.js:2:0
"./node_modules/my-package/index.js (2:0)
Module not found: Can't resolve 'dns'

https://nextjs.org/docs/messages/module-not-found
Expand Down
Expand Up @@ -16,7 +16,7 @@ Import trace for requested module:
`;

exports[`ReactRefreshLogBox app scss syntax errors 2`] = `
"./index.module.scss:1:1
"./index.module.scss (1:1)
Syntax error: Selector \\"button\\" is not pure (pure selectors must contain at least one local class or id)

> 1 | button { font-size: 5px; }
Expand Down
Expand Up @@ -11,7 +11,7 @@ Import trace for requested module:
`;

exports[`ReactRefreshLogBox app default Import trace when module not found in layout 1`] = `
"./app/module.js:1:0
"./app/module.js (1:0)
Module not found: Can't resolve 'non-existing-module'
> 1 | import \\"non-existing-module\\"

Expand Down Expand Up @@ -53,7 +53,7 @@ exports[`ReactRefreshLogBox app default conversion to class component (1) 1`] =
`;

exports[`ReactRefreshLogBox app default css syntax errors 1`] = `
"./index.module.css:1:1
"./index.module.css (1:1)
Syntax error: Selector \\"button\\" is not pure (pure selectors must contain at least one local class or id)

> 1 | button {}
Expand Down
31 changes: 21 additions & 10 deletions test/development/acceptance-app/editor-links.test.ts
Expand Up @@ -3,10 +3,21 @@ import { createNextDescribe, FileRef } from 'e2e-utils'
import path from 'path'
import { sandbox } from './helpers'

async function clickEditorLinks(browser: any) {
await browser.waitForElementByCss('[data-with-open-in-editor-link]')
async function clickSourceFile(browser: any) {
await browser.waitForElementByCss(
'[data-with-open-in-editor-link-source-file]'
)
await browser
.elementByCss('[data-with-open-in-editor-link-source-file]')
.click()
}

async function clickImportTraceFiles(browser: any) {
await browser.waitForElementByCss(
'[data-with-open-in-editor-link-import-trace]'
)
const collapsedFrameworkGroups = await browser.elementsByCss(
'[data-with-open-in-editor-link]'
'[data-with-open-in-editor-link-import-trace]'
)
for (const collapsedFrameworkButton of collapsedFrameworkGroups) {
await collapsedFrameworkButton.click()
Expand All @@ -24,7 +35,7 @@ createNextDescribe(
skipStart: true,
},
({ next }) => {
it('should be possible to open files on RSC build error', async () => {
it('should be possible to open source file on build error', async () => {
let editorRequestsCount = 0
const { session, browser, cleanup } = await sandbox(
next,
Expand Down Expand Up @@ -57,13 +68,13 @@ createNextDescribe(
)

expect(await session.hasRedbox(true)).toBe(true)
await clickEditorLinks(browser)
await check(() => editorRequestsCount, /2/)
await clickSourceFile(browser)
await check(() => editorRequestsCount, /1/)

await cleanup()
})

it('should be possible to open files on RSC parse error', async () => {
it('should be possible to open import trace files on RSC parse error', async () => {
let editorRequestsCount = 0
const { session, browser, cleanup } = await sandbox(
next,
Expand Down Expand Up @@ -98,13 +109,13 @@ createNextDescribe(
)

expect(await session.hasRedbox(true)).toBe(true)
await clickEditorLinks(browser)
await clickImportTraceFiles(browser)
await check(() => editorRequestsCount, /4/)

await cleanup()
})

it('should be possible to open files on module not found error', async () => {
it('should be possible to open import trace files on module not found error', async () => {
let editorRequestsCount = 0
const { session, browser, cleanup } = await sandbox(
next,
Expand Down Expand Up @@ -139,7 +150,7 @@ createNextDescribe(
)

expect(await session.hasRedbox(true)).toBe(true)
await clickEditorLinks(browser)
await clickImportTraceFiles(browser)
await check(() => editorRequestsCount, /3/)

await cleanup()
Expand Down
60 changes: 0 additions & 60 deletions test/development/acceptance-app/rsc-build-errors.test.ts
Expand Up @@ -320,66 +320,6 @@ createNextDescribe(
await cleanup()
})

it('should be possible to open the import trace files in your editor', async () => {
timneutkens marked this conversation as resolved.
Show resolved Hide resolved
let editorRequestsCount = 0
const { session, browser, cleanup } = await sandbox(
next,
undefined,
'/editor-links',
{
beforePageLoad(page) {
page.route('**/__nextjs_launch-editor**', (route) => {
editorRequestsCount += 1
route.fulfill()
})
},
}
)

const componentFile = 'app/editor-links/component.js'
const fileContent = await next.readFile(componentFile)

await session.patch(
componentFile,
fileContent.replace(
"// import { useState } from 'react'",
"import { useState } from 'react'"
)
)

expect(await session.hasRedbox(true)).toBe(true)
expect(await session.getRedboxSource()).toMatchInlineSnapshot(`
"./app/editor-links/component.js
ReactServerComponentsError:

You're importing a component that needs useState. It only works in a Client Component but none of its parents are marked with \\"use client\\", so they're Server Components by default.

,-[1:1]
1 | import { useState } from 'react'
: ^^^^^^^^
2 | export default function Component() {
3 | return <div>Component</div>
4 | }
\`----

Maybe one of these should be marked as a client entry with \\"use client\\":
./app/editor-links/component.js
./app/editor-links/page.js"
`)

await browser.waitForElementByCss('[data-with-open-in-editor-link]')
const collapsedFrameworkGroups = await browser.elementsByCss(
'[data-with-open-in-editor-link]'
)
for (const collapsedFrameworkButton of collapsedFrameworkGroups) {
await collapsedFrameworkButton.click()
}

await check(() => editorRequestsCount, /2/)

await cleanup()
})

it('should freeze parent resolved metadata to avoid mutating in generateMetadata', async () => {
const pagePath = 'app/metadata/mutate/page.js'
const content = `export default function page(props) {
Expand Down