From e3ed2f27e12f9197cb28b89f7b177b5ab33e842e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Born=C3=B6?= Date: Sat, 4 Feb 2023 01:41:01 +0100 Subject: [PATCH] Add editor links to module import traces (#45257) Makes it possible to open the files in module import traces in your editor by clicking them. Module not found: ![image](https://user-images.githubusercontent.com/25056922/214539916-a776fa6f-7645-49c8-9106-a2abd62a8447.png) Parse error: ![image](https://user-images.githubusercontent.com/25056922/214539987-0e3fd312-7ade-44fc-a334-b95d73320775.png) Fixes NEXT-422 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- .../parseNotFoundError.ts | 8 +- .../internal/components/Terminal/Terminal.tsx | 10 +- .../ReactRefreshLogBox-builtins.test.ts.snap | 12 +- .../ReactRefreshLogBox.test.ts.snap | 12 +- .../acceptance-app/editor-links.test.ts | 148 ++++++++++++++++++ .../acceptance-app/rsc-build-errors.test.ts | 42 ----- .../ReactRefreshLogBox-builtins.test.ts.snap | 12 +- 7 files changed, 177 insertions(+), 67 deletions(-) create mode 100644 test/development/acceptance-app/editor-links.test.ts diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts index 82403320ca86..beb6f35ff5f0 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts @@ -116,9 +116,7 @@ export async function getNotFoundError( ) if (moduleTrace.length === 0) return '' - return `\nImport trace for requested module:\n${moduleTrace.join( - '\n' - )}\n\n` + return `\nImport trace for requested module:\n${moduleTrace.join('\n')}` } let message = @@ -127,8 +125,8 @@ export async function getNotFoundError( '\n' + frame + (frame !== '' ? '\n' : '') + - importTrace() + - '\nhttps://nextjs.org/docs/messages/module-not-found' + '\nhttps://nextjs.org/docs/messages/module-not-found\n' + + importTrace() let formattedFileName: string = chalk.cyan(fileName) if (lineNumber && column) { diff --git a/packages/next/src/client/components/react-dev-overlay/internal/components/Terminal/Terminal.tsx b/packages/next/src/client/components/react-dev-overlay/internal/components/Terminal/Terminal.tsx index 5bcfe72a1af6..ed61d5fd13e3 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/components/Terminal/Terminal.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/components/Terminal/Terminal.tsx @@ -5,13 +5,19 @@ import { EditorLink } from './EditorLink' export type TerminalProps = { content: string } function getImportTraceFiles(content: string): [string, string[]] { - if (/ReactServerComponentsError:/.test(content)) { + if ( + /ReactServerComponentsError:/.test(content) || + /Import trace for requested module:/.test(content) + ) { // It's an RSC Build Error const lines = content.split('\n') // Grab the lines at the end containing the files const files = [] - while (/app\/.+\./.test(lines[lines.length - 1])) { + while ( + /.+\..+/.test(lines[lines.length - 1]) && + !lines[lines.length - 1].includes(':') + ) { const file = lines.pop()!.trim() files.unshift(file) } diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap index 6b6d37ce8191..9ba9510722cd 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap @@ -8,10 +8,10 @@ Module not found: Can't resolve 'b' 3 | return ( 4 |
-Import trace for requested module: -./app/page.js +https://nextjs.org/docs/messages/module-not-found -https://nextjs.org/docs/messages/module-not-found" +Import trace for requested module: +./app/page.js" `; exports[`ReactRefreshLogBox app default Module not found empty import trace 1`] = ` @@ -44,9 +44,9 @@ exports[`ReactRefreshLogBox app default Node.js builtins 1`] = ` "./node_modules/my-package/index.js:2:0 Module not found: Can't resolve 'dns' +https://nextjs.org/docs/messages/module-not-found + Import trace for requested module: ./index.js -./app/page.js - -https://nextjs.org/docs/messages/module-not-found" +./app/page.js" `; diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap index f85bf0bf7411..da2c995d1e24 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -4,10 +4,10 @@ exports[`ReactRefreshLogBox app default Can't resolve @import in CSS file 1`] = "./app/styles2.css Module not found: Can't resolve './boom.css' -Import trace for requested module: -./app/styles1.css +https://nextjs.org/docs/messages/module-not-found -https://nextjs.org/docs/messages/module-not-found" +Import trace for requested module: +./app/styles1.css" `; exports[`ReactRefreshLogBox app default Import trace when module not found in layout 1`] = ` @@ -15,10 +15,10 @@ exports[`ReactRefreshLogBox app default Import trace when module not found in la Module not found: Can't resolve 'non-existing-module' > 1 | import \\"non-existing-module\\" -Import trace for requested module: -./app/layout.js +https://nextjs.org/docs/messages/module-not-found -https://nextjs.org/docs/messages/module-not-found" +Import trace for requested module: +./app/layout.js" `; exports[`ReactRefreshLogBox app default Should not show __webpack_exports__ when exporting anonymous arrow function 1`] = ` diff --git a/test/development/acceptance-app/editor-links.test.ts b/test/development/acceptance-app/editor-links.test.ts new file mode 100644 index 000000000000..d9b3c6e30e1b --- /dev/null +++ b/test/development/acceptance-app/editor-links.test.ts @@ -0,0 +1,148 @@ +import { check } from 'next-test-utils' +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]') + const collapsedFrameworkGroups = await browser.elementsByCss( + '[data-with-open-in-editor-link]' + ) + for (const collapsedFrameworkButton of collapsedFrameworkGroups) { + await collapsedFrameworkButton.click() + } +} + +createNextDescribe( + 'Error overlay - editor links', + { + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }, + ({ next }) => { + it('should be possible to open files on RSC build error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + import Component from '../index' + export default function Page() { + return + } + `, + ], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) + + await session.patch( + 'index.js', + `import { useState } from 'react' + export default () => 'hello world'` + ) + + expect(await session.hasRedbox(true)).toBe(true) + await clickEditorLinks(browser) + await check(() => editorRequestsCount, /2/) + + await cleanup() + }) + + it('should be possible to open files on RSC parse error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + import Component from '../index' + export default function Page() { + return + } + `, + ], + ['mod1.js', "import './mod2.js'"], + ['mod2.js', '{{{{{'], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) + + await session.patch( + 'index.js', + `import './mod1' + export default () => 'hello world'` + ) + + expect(await session.hasRedbox(true)).toBe(true) + await clickEditorLinks(browser) + await check(() => editorRequestsCount, /4/) + + await cleanup() + }) + + it('should be possible to open files on module not found error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + ` + import Component from '../index' + export default function Page() { + return + } + `, + ], + ['mod1.js', "import './mod2.js'"], + ['mod2.js', 'import "boom"'], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) + + await session.patch( + 'index.js', + `import './mod1' + export default () => 'hello world'` + ) + + expect(await session.hasRedbox(true)).toBe(true) + await clickEditorLinks(browser) + await check(() => editorRequestsCount, /3/) + + await cleanup() + }) + } +) diff --git a/test/development/acceptance-app/rsc-build-errors.test.ts b/test/development/acceptance-app/rsc-build-errors.test.ts index c6efdbd41697..2992540334ca 100644 --- a/test/development/acceptance-app/rsc-build-errors.test.ts +++ b/test/development/acceptance-app/rsc-build-errors.test.ts @@ -1,4 +1,3 @@ -import { check } from 'next-test-utils' import { createNextDescribe, FileRef } from 'e2e-utils' import path from 'path' import { sandbox } from './helpers' @@ -267,46 +266,5 @@ createNextDescribe( await cleanup() }) - - it('should be possible to open the import trace files in your editor', async () => { - 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) - 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() - }) } ) diff --git a/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap b/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap index 5b11d2ab4fb7..61b51fea3087 100644 --- a/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap +++ b/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap @@ -32,19 +32,19 @@ Module not found: Can't resolve 'b' 3 | return ( 4 |
-Import trace for requested module: -./pages/index.js +https://nextjs.org/docs/messages/module-not-found -https://nextjs.org/docs/messages/module-not-found" +Import trace for requested module: +./pages/index.js" `; exports[`ReactRefreshLogBox default Node.js builtins 1`] = ` "./node_modules/my-package/index.js:2:0 Module not found: Can't resolve 'dns' +https://nextjs.org/docs/messages/module-not-found + Import trace for requested module: ./index.js -./pages/index.js - -https://nextjs.org/docs/messages/module-not-found" +./pages/index.js" `;