Loose RSC import restrictions for 3rd party packages (#56501)

When we landed #51179 it broke library like `apollo-client` as it's bundling client hooks into RSC bundle, so our RSC linter caught them and reported fatal errors. But those client hook APIs won't get executed in RSC. The original purpose of erroring on invalid hooks for server & client components was to catch bugs easier, but it might be too strict for the 3rd party libraries like `apollo-client` due to few reasons. 

We changed the rules only applying on user land source code. For 3rd party packages if they're not being imported correctly into proper server or client components, we're still showing runtime errors instead of fatal build errors.

x-ref: https://github.com/apollographql/apollo-client/issues/10974
Closes NEXT-1673
This commit is contained in:
Jiachi Liu 2023-10-06 01:59:22 +02:00 committed by GitHub
parent 35e4539304
commit 9d150b116d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 139 additions and 60 deletions

View file

@ -344,6 +344,10 @@ impl<C: Comments> ReactServerComponents<C> {
}
fn assert_server_graph(&self, imports: &[ModuleImports], module: &Module) {
// If the
if self.is_from_node_modules(&self.filepath) {
return;
}
for import in imports {
let source = import.source.0.clone();
if self.invalid_server_imports.contains(&source) {
@ -391,6 +395,9 @@ impl<C: Comments> ReactServerComponents<C> {
}
fn assert_server_filename(&self, module: &Module) {
if self.is_from_node_modules(&self.filepath) {
return;
}
let is_error_file = Regex::new(r"[\\/]error\.(ts|js)x?$")
.unwrap()
.is_match(&self.filepath);
@ -416,6 +423,9 @@ impl<C: Comments> ReactServerComponents<C> {
}
fn assert_client_graph(&self, imports: &[ModuleImports]) {
if self.is_from_node_modules(&self.filepath) {
return;
}
for import in imports {
let source = import.source.0.clone();
if self.invalid_client_imports.contains(&source) {
@ -432,6 +442,9 @@ impl<C: Comments> ReactServerComponents<C> {
}
fn assert_invalid_api(&self, module: &Module, is_client_entry: bool) {
if self.is_from_node_modules(&self.filepath) {
return;
}
let is_layout_or_page = Regex::new(r"[\\/](page|layout)\.(ts|js)x?$")
.unwrap()
.is_match(&self.filepath);
@ -562,6 +575,12 @@ impl<C: Comments> ReactServerComponents<C> {
},
);
}
fn is_from_node_modules(&self, filepath: &str) -> bool {
Regex::new(r"[\\/]node_modules[\\/]")
.unwrap()
.is_match(filepath)
}
}
pub fn server_components<C: Comments>(

View file

@ -1 +0,0 @@
module.exports = {}

View file

@ -0,0 +1,4 @@
'use client'
export default function page() {
return 'page'
}

View file

@ -0,0 +1,12 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,3 @@
export default function page() {
return 'page'
}

View file

@ -0,0 +1,6 @@
import { useState } from 'react'
export function callClientApi() {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useState(0)
}

View file

@ -0,0 +1,4 @@
{
"name": "client-package",
"exports": "./index.js"
}

View file

@ -0,0 +1,7 @@
'use client'
import { cookies } from 'next/headers'
export function callServerApi() {
return cookies()
}

View file

@ -0,0 +1,4 @@
{
"name": "server-package",
"exports": "./index.js"
}

View file

@ -379,63 +379,4 @@ describe('Error overlay - RSC build errors', () => {
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',
outdent`
{
"name": "client-package",
"version": "0.0.1"
}
`,
],
['app/Component.js', "import 'client-package'"],
[
'app/page.js',
outdent`
import './Component.js'
export default function Page() {
return <p>Hello world</p>
}
`,
],
])
)
expect(await session.hasRedbox(true)).toBe(true)
expect(
next.normalizeTestDirContent(await session.getRedboxSource())
).toMatchInlineSnapshot(
next.normalizeSnapshot(`
"./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.
Learn more: https://nextjs.org/docs/getting-started/react-essentials
,-[TEST_DIR/node_modules/client-package/module2.js:1:1]
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()
})
})

View file

@ -0,0 +1,80 @@
import path from 'path'
import { outdent } from 'outdent'
import { FileRef, createNextDescribe } from 'e2e-utils'
import {
check,
getRedboxDescription,
hasRedbox,
shouldRunTurboDevTest,
} from 'next-test-utils'
createNextDescribe(
'Error overlay - RSC runtime errors',
{
files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-runtime-errors')),
packageJson: {
scripts: {
setup: 'cp -r ./node_modules_bak/* ./node_modules',
build: 'yarn setup && next build',
dev: `yarn setup && next ${
shouldRunTurboDevTest() ? 'dev --turbo' : 'dev'
}`,
start: 'next start',
},
},
installCommand: 'yarn',
startCommand: (global as any).isNextDev ? 'yarn dev' : 'yarn start',
},
({ next }) => {
it('should show runtime errors if invalid client API from node_modules is executed', async () => {
await next.patchFile(
'app/server/page.js',
outdent`
import { callClientApi } from 'client-package'
export default function Page() {
callClientApi()
return 'page'
}
`
)
const browser = await next.browser('/server')
await check(
async () => ((await hasRedbox(browser, true)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
expect(errorDescription).toContain(
`Error: useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component`
)
})
it('should show runtime errors if invalid server API from node_modules is executed', async () => {
await next.patchFile(
'app/client/page.js',
outdent`
'use client'
import { callServerApi } from 'server-package'
export default function Page() {
callServerApi()
return 'page'
}
`
)
const browser = await next.browser('/client')
await check(
async () => ((await hasRedbox(browser, true)) ? 'success' : 'fail'),
/success/
)
const errorDescription = await getRedboxDescription(browser)
expect(errorDescription).toContain(
`Error: Invariant: cookies() expects to have requestAsyncStorage, none available.`
)
})
}
)