diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/getModuleTrace.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/getModuleTrace.ts index e473aa181f..caf26636e6 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/getModuleTrace.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/getModuleTrace.ts @@ -1,18 +1,59 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' +import loaderUtils from 'next/dist/compiled/loader-utils3' import { relative } from 'path' -export function formatModule(compiler: webpack.Compiler, module: any) { - return relative(compiler.context, module.resource).replace(/\?.+$/, '') +function formatModule(compiler: webpack.Compiler, module: any) { + const relativePath = relative(compiler.context, module.resource).replace( + /\?.+$/, + '' + ) + return loaderUtils.isUrlRequest(relativePath) + ? loaderUtils.urlToRequest(relativePath) + : relativePath } -export function getImportTraceForOverlay( +export function formatModuleTrace( compiler: webpack.Compiler, moduleTrace: any[] ) { - return moduleTrace - .map((m) => (m.resource ? ' ' + formatModule(compiler, m) : '')) - .filter(Boolean) - .join('\n') + let importTrace: string[] = [] + let firstExternalModule: any + for (let i = moduleTrace.length - 1; i >= 0; i--) { + const mod = moduleTrace[i] + if (!mod.resource) continue + + if (!mod.resource.includes('node_modules/')) { + importTrace.unshift(formatModule(compiler, mod)) + } else { + firstExternalModule = mod + break + } + } + + let invalidImportMessage = '' + if (firstExternalModule) { + const firstExternalPackageName = + firstExternalModule.resourceResolveData?.descriptionFileData?.name + + if (firstExternalPackageName === 'styled-jsx') { + invalidImportMessage += `\n\nThe error was caused by using 'styled-jsx' in '${importTrace[0]}'. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.` + } else { + let formattedExternalFile = + firstExternalModule.resource.split('node_modules') + formattedExternalFile = + formattedExternalFile[formattedExternalFile.length - 1] + + invalidImportMessage += `\n\nThe error was caused by importing '${formattedExternalFile.slice( + 1 + )}' in '${importTrace[0]}'.` + } + } + + return { + lastInternalFileName: importTrace[0], + invalidImportMessage, + formattedModuleTrace: `${importTrace.map((mod) => ' ' + mod).join('\n')}`, + } } export function getModuleTrace( diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNextInvalidImportError.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNextInvalidImportError.ts index b8d55d6a7e..68f2f91d3c 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNextInvalidImportError.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNextInvalidImportError.ts @@ -1,5 +1,5 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' -import { formatModule, getModuleTrace } from './getModuleTrace' +import { formatModuleTrace, getModuleTrace } from './getModuleTrace' import { SimpleWebpackError } from './simpleWebpackError' export function getNextInvalidImportError( @@ -18,49 +18,15 @@ export function getNextInvalidImportError( } const { moduleTrace } = getModuleTrace(module, compilation, compiler) - - let importTrace: string[] = [] - let firstExternalModule: any - for (let i = moduleTrace.length - 1; i >= 0; i--) { - const mod = moduleTrace[i] - if (!mod.resource) continue - - if (!mod.resource.includes('node_modules/')) { - importTrace.unshift(formatModule(compiler, mod)) - } else { - firstExternalModule = mod - break - } - } - - let invalidImportMessage = '' - if (firstExternalModule) { - const firstExternalPackageName = - firstExternalModule.resourceResolveData?.descriptionFileData?.name - - if (firstExternalPackageName === 'styled-jsx') { - invalidImportMessage += `\n\nThe error was caused by using 'styled-jsx' in '${importTrace[0]}'. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.` - } else { - let formattedExternalFile = - firstExternalModule.resource.split('node_modules') - formattedExternalFile = - formattedExternalFile[formattedExternalFile.length - 1] - - invalidImportMessage += `\n\nThe error was caused by importing '${formattedExternalFile.slice( - 1 - )}' in '${importTrace[0]}'.` - } - } + const { formattedModuleTrace, lastInternalFileName, invalidImportMessage } = + formatModuleTrace(compiler, moduleTrace) return new SimpleWebpackError( - importTrace[0], + lastInternalFileName, err.message + invalidImportMessage + - (importTrace.length > 0 - ? `\n\nImport trace for requested module:\n${importTrace - .map((mod) => ' ' + mod) - .join('\n')}` - : '') + '\n\nImport trace for requested module:\n' + + formattedModuleTrace ) } catch { return false diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts index c322023d4a..41bc875444 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseRSC.ts @@ -1,6 +1,6 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' -import { getModuleTrace, getImportTraceForOverlay } from './getModuleTrace' +import { getModuleTrace, formatModuleTrace } from './getModuleTrace' import { SimpleWebpackError } from './simpleWebpackError' function formatRSCErrorMessage( @@ -141,12 +141,16 @@ export function getRscError( fileName ) + const { formattedModuleTrace, lastInternalFileName, invalidImportMessage } = + formatModuleTrace(compiler, moduleTrace) + const error = new SimpleWebpackError( - fileName, + lastInternalFileName, 'ReactServerComponentsError:\n' + formattedError[0] + + invalidImportMessage + formattedError[1] + - getImportTraceForOverlay(compiler, moduleTrace) + formattedModuleTrace ) // Delete the stack because it's created here. diff --git a/test/development/acceptance-app/invalid-imports.test.ts b/test/development/acceptance-app/invalid-imports.test.ts index 685e759e26..ca6cf8f19b 100644 --- a/test/development/acceptance-app/invalid-imports.test.ts +++ b/test/development/acceptance-app/invalid-imports.test.ts @@ -67,15 +67,15 @@ createNextDescribe( expect(await session.hasRedbox(true)).toBe(true) expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "app/comp2.js + "./app/comp2.js 'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component. - The error was caused by using 'styled-jsx' in 'app/comp2.js'. It only works in a Client Component but none of its parents are marked with \\"use client\\", so they're Server Components by default. + The error was caused by using 'styled-jsx' in './app/comp2.js'. It only works in a Client Component but none of its parents are marked with \\"use client\\", so they're Server Components by default. Import trace for requested module: - app/comp2.js - app/comp1.js - app/page.js" + ./app/comp2.js + ./app/comp1.js + ./app/page.js" `) await cleanup() @@ -142,15 +142,15 @@ createNextDescribe( expect(await session.hasRedbox(true)).toBe(true) expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "app/comp2.js + "./app/comp2.js 'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component. - The error was caused by importing 'client-only-package/index.js' in 'app/comp2.js'. + The error was caused by importing 'client-only-package/index.js' in './app/comp2.js'. Import trace for requested module: - app/comp2.js - app/comp1.js - app/page.js" + ./app/comp2.js + ./app/comp1.js + ./app/page.js" `) await cleanup() @@ -215,15 +215,15 @@ createNextDescribe( expect(await session.hasRedbox(true)).toBe(true) expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "app/comp2.js + "./app/comp2.js 'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component. - The error was caused by importing 'server-only-package/index.js' in 'app/comp2.js'. + The error was caused by importing 'server-only-package/index.js' in './app/comp2.js'. Import trace for requested module: - app/comp2.js - app/comp1.js - app/page.js" + ./app/comp2.js + ./app/comp1.js + ./app/page.js" `) 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 ba6e2e8867..efc8cdbe24 100644 --- a/test/development/acceptance-app/rsc-build-errors.test.ts +++ b/test/development/acceptance-app/rsc-build-errors.test.ts @@ -284,7 +284,7 @@ createNextDescribe( \`---- Import path: - app/server-with-errors/error-file/error.js" + ./app/server-with-errors/error-file/error.js" `) await cleanup() @@ -314,7 +314,7 @@ createNextDescribe( \`---- Import path: - app/server-with-errors/error-file/error.js" + ./app/server-with-errors/error-file/error.js" `) await cleanup() @@ -363,8 +363,8 @@ createNextDescribe( \`---- Maybe one of these should be marked as a client entry with \\"use client\\": - app/editor-links/component.js - app/editor-links/page.js" + ./app/editor-links/component.js + ./app/editor-links/page.js" `) await browser.waitForElementByCss('[data-with-open-in-editor-link]') @@ -413,5 +413,58 @@ createNextDescribe( await cleanup() }) + + it('should show which import caused an error in node_modules', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/client-package/module2.js', + "import { useState } from 'react'", + ], + ['node_modules/client-package/module1.js', "import './module2.js'"], + ['node_modules/client-package/index.js', "import './module1.js'"], + [ + 'node_modules/client-package/package.json', + ` + { + "name": "client-package", + "version": "0.0.1" + } + `, + ], + ['app/Component.js', "import 'client-package'"], + [ + 'app/page.js', + ` + import './Component.js' + export default function Page() { + return

Hello world

+ }`, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + "./app/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 | import { useState } from 'react' + : ^^^^^^^^ + \`---- + + The error was caused by importing 'client-package/index.js' in './app/Component.js'. + + Maybe one of these should be marked as a client entry with \\"use client\\": + ./app/Component.js + ./app/page.js" + `) + + await cleanup() + }) } ) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index 52391a3499..a158fc2a96 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -64,8 +64,8 @@ createNextDescribe( \`---- Import trace for requested module: - components/Comp.js - pages/index.js" + ./components/Comp.js + ./pages/index.js" `) await cleanup() @@ -106,8 +106,8 @@ createNextDescribe( \`---- Import trace for requested module: - components/Comp.js - pages/index.js" + ./components/Comp.js + ./pages/index.js" `) await cleanup()