vercel / next.js

The React Framework
https://nextjs.org
MIT License
124.41k stars 26.54k forks source link

`NextRscErrClientMetadataExport` thrown when mixing metadata and virtual css modules #67591

Open jantimon opened 1 month ago

jantimon commented 1 month ago

Link to the code that reproduces this issue

https://github.com/jantimon/reproduction-next-server-component-css-error

To Reproduce

  1. clone https://github.com/jantimon/reproduction-next-server-component-css-error
  2. npm install
  3. npm run dev

Current vs. Expected behavior

I am using a virtual css module (./page.yak.module.css!=!./page?./page.yak.module.css).

It looks like that this virtual css module is compiled by SWC and fails because it was marked as client component.

yarn dev
yarn run v1.22.19
$ next dev
  ▲ Next.js 14.2.4
  - Local:        http://localhost:3000

 ✓ Starting...
 ✓ Ready in 1491ms
 ⨯ ./src/app/page.tsx?./page.yak.module.css
Error: 
  × You are attempting to export "metadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/
  │ docs/getting-started/react-essentials#the-use-client-directive
  │ 
  │ 
   ╭─[/Users/jannicklas/Desktop/repo-yak-meta-data/src/app/page.tsx:2:1]
 2 │ import { Button } from "./button";
 3 │ import { styled } from "next-yak";
 4 │ import __styleYak from "./page.yak.module.css!=!./page?./page.yak.module.css";
 5 │ export const metadata: Metadata = {
   ·              ────────
 6 │   title: '...'
 7 │ };
 8 │ const Title =
   ╰────

Import trace for requested module:
./src/app/page.tsx?./page.yak.module.css

I would expect that SWC does not transpile this file at all

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.1.0: Mon Oct  9 21:27:24 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T6000
  Available memory (MB): 65536
  Available CPU cores: 10
Binaries:
  Node: 20.9.0
  npm: 10.1.0
  Yarn: 1.22.19
  pnpm: 8.15.7
Relevant Packages:
  next: 15.0.0-canary.59 // Latest available version is detected (15.0.0-canary.59).
  eslint-config-next: N/A
  react: 19.0.0-rc.0
  react-dom: 19.0.0-rc.0
  typescript: 5.5.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

SWC

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

In the reproduction repository are two React Server components:

page.tsx:

import { Metadata } from "next";
import { Button } from "./button";
import { styled } from "next-yak";

export const metadata: Metadata = {
  title: '...',
}

const Title = styled.h1`
  color: blue;
`;

export default function Home() {
  return (<>
    <Title>Hello</Title>
    <Button>Click me</Button>
  </>);
}

and button.tsx:

import { styled } from "next-yak";

export const Button = styled.button`
  color: red;
`

The bug happens only if export const metadata and a yak styled component are in the same file.

Removing export const metadata or Title will not trigger the error.

This is surprising as the button is also a yak styled component and can be imported into a server component which uses export const metadata.

kdy1 commented 1 month ago

Does next-yak generate "use client"?

Error reporting code is at https://github.com/vercel/next.js/blob/d8884615c9928a243abf9505b04b3ad34f2edf37/packages/next-swc/crates/next-custom-transforms/src/transforms/react_server_components.rs#L738-L749.

It requires is_client_entry to be true, and is_client_entry becomes true only by the code at https://github.com/vercel/next.js/blob/d8884615c9928a243abf9505b04b3ad34f2edf37/packages/next-swc/crates/next-custom-transforms/src/transforms/react_server_components.rs#L321-L351

jantimon commented 1 month ago

no next-yak will never inject use-client

the entire solution works well together with server components (as long as you don't add export const metadata)

here is the transformed page.tsx code from above (after next-yak):

import { Metadata } from "next";
import { Button } from "./button";
import { styled } from "next-yak";
import __styleYak from "./file.yak.module.css!=!./file?./file.yak.module.css";
export const metadata: Metadata = {
  title: '...'
};
const Title =
/*YAK Extracted CSS:
.Title {
  color: blue;
}*/
/*#__PURE__*/
styled.h1(__styleYak.Title);
export default function Home() {
  return <>
    <Title>Hello</Title>
    <Button>Click me</Button>
  </>;
}

and the magic virtual css module "./file.yak.module.css!=!./file?./file.yak.module.css" becomes:

.Title {
  color: blue;
}

maybe the https://yak.js.org/playground helps

jantimon commented 1 month ago

I patched dist/build/webpack/loaders/next-swc-loader.js locally by adding a console.log to the error case:

function swcLoader(inputSource, inputSourceMap) {

    const loaderSpan = this.currentTraceSpan.traceChild("next-swc-loader");
    const callback = this.async();
    loaderSpan.traceAsyncFn(()=>loaderTransform.call(this, loaderSpan, inputSource, inputSourceMap)).then(([transformedSource, outputSourceMap])=>{
        if (this.resourcePath.includes("keyframes/page.tsx")) {
            console.log("swcLoader succeeded 🙂");
            console.dir({
                loaders: this.loaders.map((loader) => {
                    return {
                        path: loader.path.replace(/.*node_modules\/next/, "next"),
                    }
                }),
                userRequest: this._module.userRequest,
                resourcePath: this.resourcePath,
                inputSource: String(inputSource),
            }, { depth: 10 });
        }
        callback(null, transformedSource, outputSourceMap || inputSourceMap);
    }, (err)=>{   
        console.log("swcLoader failed");
        console.dir({
            loaders: this.loaders.map((loader) => {
                return {
                    path: loader.path.replace(/.*node_modules\/next/, "next"),
                }
            }),
            userRequest: this._module.userRequest,
            resourcePath: this.resourcePath,
            inputSource: String(inputSource),
            err,
        }, { depth: 10 });
        callback(err);
    });
}

Now this gives me the following log:

swcLoader succeeded 🙂
{
  loaders: [
    {
      path: 'next/dist/build/webpack/loaders/next-flight-loader/index.js'
    },
    { path: 'next/dist/build/webpack/loaders/next-swc-loader.js' },
    {
      path: 'next-yak/packages/next-yak/dist/loaders/ts-loader.cjs'
    }
  ],
  userRequest: 'next-yak/packages/example/app/keyframes/page.tsx',
  resourcePath: 'next-yak/packages/example/app/keyframes/page.tsx',
  inputSource: 'import { Metadata } from "next";\n' +
    'import { styled } from "next-yak/internal";\n' +
    'import __styleYak from "./page.yak.module.css!=!./page?./page.yak.module.css";\n' +
    'export const metadata: Metadata = {\n' +
    "  title: '..'\n" +
    '};\n' +
    'const Title =\n' +
    '/*YAK Extracted CSS:\n' +
    '.Title {\n' +
    '  color: blue;\n' +
    '}*/\n' +
    '/*#__PURE__*/\n' +
    'styled.h1(__styleYak.Title);\n' +
    'export default function Home() {\n' +
    '  return <>\n' +
    '    <Title>Hello</Title>\n' +
    '  </>;\n' +
    '}'
}

and directly after that:

swcLoader failed
{
  loaders: [
    {
      path: 'next/dist/compiled/@next/react-refresh-utils/dist/loader.js'
    },
    {
      path: 'next/dist/build/webpack/loaders/next-flight-client-module-loader.js'
    },
    { path: 'next/dist/build/webpack/loaders/next-swc-loader.js' },
    {
      path: 'next-yak/packages/next-yak/dist/loaders/ts-loader.cjs'
    }
  ],
  userRequest: 'next-yak/packages/example/app/keyframes/page.tsx?./page.yak.module.css',
  resourcePath: 'next-yak/packages/example/app/keyframes/page.tsx',
  inputSource: 'import { Metadata } from "next";\n' +
    'import { styled } from "next-yak/internal";\n' +
    'import __styleYak from "./page.yak.module.css!=!./page?./page.yak.module.css";\n' +
    'export const metadata: Metadata = {\n' +
    "  title: '..'\n" +
    '};\n' +
    'const Title =\n' +
    '/*YAK Extracted CSS:\n' +
    '.Title {\n' +
    '  color: blue;\n' +
    '}*/\n' +
    '/*#__PURE__*/\n' +
    'styled.h1(__styleYak.Title);\n' +
    'export default function Home() {\n' +
    '  return <>\n' +
    '    <Title>Hello</Title>\n' +
    '  </>;\n' +
    '}',
  err: [Error: 
    × You are attempting to export "metadata" from a component marked with "use client", which is disallowed. Either remove the export, or the "use client" directive. Read more: https://nextjs.org/
    │ docs/getting-started/react-essentials#the-use-client-directive
    │ 
    │ 
     ╭─[next-yak/packages/example/app/keyframes/page.tsx:1:1]
   1 │ import { Metadata } from "next";
   2 │ import { styled } from "next-yak/internal";
   3 │ import __styleYak from "./page.yak.module.css!=!./page?./page.yak.module.css";
   4 │ export const metadata: Metadata = {
     ·              ────────
   5 │   title: '..'
   6 │ };
   7 │ const Title =
     ╰────
  ] {
    code: 'GenericFailure'
  }
}
 ⨯ ./app/keyframes/page.tsx?./page.yak.module.css

@kdy1 as you can see there is no "use client" in the input source for the swc loader

the problem seems to be that it processes ./page.yak.module.css!=!./page?./page.yak.module.css as typescript file as soon as export const metadata: Metadata is added

jantimon commented 1 month ago

It happens only in the "app-pages-browser" issuerLayer - and gets injected as entry:

import: 'next-flight-client-entry-loader?modules={"request":"/next-yak/packages/example/app/keyframes/page.tsx?./page.yak.module.css","ids":[]}&modules={"request":"/next-yak/packages/next-yak/dist/context/index.js","ids":["YakThemeProvider"]}&server=false!'

https://github.com/vercel/next.js/blob/73f0868a8088b27aefd58671c147ff164967d645/packages/next/src/build/entries.ts#L861-L875