vercel / next.js

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

Error using Emotion component selectors with SWC #46973

Open trappar opened 1 year ago

trappar commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: x64
      Version: Darwin Kernel Version 22.2.0: Fri Nov 11 02:08:47 PST 2022; root:xnu-8792.61.2~4/RELEASE_X86_64
    Binaries:
      Node: 16.18.1
      npm: 9.1.1
      Yarn: 1.22.19
      pnpm: 7.27.1
    Relevant packages:
      next: 13.1.6
      eslint-config-next: 13.1.6
      react: 18.2.0
      react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

SWC transpilation

Link to the code that reproduces this issue

https://codesandbox.io/p/sandbox/polished-browser-went6z

To Reproduce

The following is a minimal reproduction (used in the reproduction codesandbox):

import styled from "@emotion/styled";

const A = styled.div`
  color: red;
`;

const B = styled.div`
  color: blue;
  ${A} {
    color: green;
  }
`;

export default function Home() {
  return <B><A>A inside B</A></B>;
}

No extra steps are required to reproduce. Simply load the codesandbox and observe the resulting error.

Describe the Bug

I'm getting the error:

Error: Component selectors can only be used in conjunction with @emotion/babel-plugin, the swc Emotion plugin, or another Emotion-aware compiler transform.

When using component selectors as described in the "Targeting another emotion component" section of the emotion docs, I'm getting this error.

Expected Behavior

Since this reproduction repo uses the swc Emotion plugin, I would not expect this syntax to result in an error.

Given the example above, I would expect green text "Test" when rendering A inside B.

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

JesseKoldewijn commented 1 year ago

@trappar Hey man, just had a look for ya and it seems like you both had none of the emotion plugins installed and/or configured + a not working syntax in your home page which together prevented the text to become green as you explained

see example here: https://github.com/JesseKoldewijn/nextjs-emotion-example

The incorrect syntax was the [ ] around your inner A tag on the B styled component.

JesseKoldewijn commented 1 year ago

Funny enough you do have these brackets around your A in the B element on your codesandbox but not in the snippet shown in your issue.

trappar commented 1 year ago

There is still something fishy going on.

The brackets were just an artifact of me playing around trying to get it to work, which is why they weren't present in the example I gave. I removed them and it still failed.

As far as I can tell from the documentation, NextJS has built in support for emotion without installing that swc plugin. Also, I didn't enable emotion in the compiler options because the documentation claims that it's enabled by default, at least when running in dev (which is very confusing btw):

module.exports = {
  compiler: {
    emotion: boolean | {
      // default is true. It will be disabled when build type is production.

I can confirm that emotion is working without setting emotion: true. See this codesandbox for an example of this: https://codesandbox.io/p/sandbox/throbbing-cdn-oip9jv

Now what's really strange is that if I do set emotion: true in my next.config.js, then this particular feature suddenly starts working. This works even without installing @swc/plugin-emotion.

I've updated my original codesandbox to this state (emotion enabled in next config and no extra plugin installed). See here: https://codesandbox.io/p/sandbox/polished-browser-went6z

So maybe the issue is that the documentation claims that emotion is enabled by default, which it sort of is... but in some half-functional state where not all features work?

Definitely still seems like there are some improvements needed.

JohnDaly commented 1 year ago

Also, I didn't enable emotion in the compiler options because the documentation claims that it's enabled by default, at least when running in dev (which is very confusing btw):

I believe the "default is true" comment is referring to the sourceMap setting:

// default is true. It will be disabled when build type is production.
sourceMap?: boolean,

If compiler.emotion is not set in the next.config.js, then the Emotion plugin will not be enabled: https://github.com/vercel/next.js/blob/287ad83399fa6e90deae4ab75a5f9f762adc45e7/packages/next/src/build/swc/options.ts#L153-L155

Now what's really strange is that if I do set emotion: true in my next.config.js, then this particular feature suddenly starts working. This works even without installing @swc/plugin-emotion.

A few things on this:

  1. The Emotion plugin is only needed if you want to use features like: minification, dead code elimination, source maps, and "components as selectors"
  2. You don't need to explicitly install @swc/plugin-emotion, as it comes with Next by default. The plugin will be enabled if emotion is configured in next.config.js
JesseKoldewijn commented 1 year ago

Yeah I was unable to replicate the issue on the latest canary release even without the swc emotion plugin installed. So I'm honestly not all to sure what the issue could be.

trappar commented 1 year ago

Doesn't sound like there is an issue aside from the documentation being lacking/confusing. At this point four different devs from my team interacted with trying to get this setup and none of us got it right, and Jesse actually got it wrong in the comments above as well.

Things that I think could be improved:

At very least the documentation could be changed to something like:

Emotion

We're working to port @emotion/babel-plugin to the Next.js Compiler. Since Next.js version (insert version here), most emotion core features work out of the box. To enable full support for advanced features such as minification, dead code elimination, source maps, and components as selectors, follow these instructions:

First, update to the latest version of Next.js: npm install next@latest. Then, update your next.config.js file:

// next.config.js

module.exports = {
  compiler: {
    // set to true/object to enable advanced emotion features.
    emotion: boolean | {
      // default is true. It will be disabled when build type is production.
      sourceMap?: boolean,
      // default is 'dev-only'.
      autoLabel?: 'never' | 'dev-only' | 'always',
      // default is '[local]'.
      // Allowed values: `[local]` `[filename]` and `[dirname]`
      // This option only works when autoLabel is set to 'dev-only' or 'always'.
      // It allows you to define the format of the resulting label.
      // The format is defined via string where variable parts are enclosed in square brackets [].
      // For example labelFormat: "my-classname--[local]", where [local] will be replaced with the name of the variable the result is assigned to.
      labelFormat?: string,
      // default is undefined.
      // This option allows you to tell the compiler what imports it should
      // look at to determine what it should transform so if you re-export
      // Emotion's exports, you can still use transforms.
      importMap?: {
        [packageName: string]: {
          [exportName: string]: {
            canonicalImport?: [string, string],
            styledBaseImport?: [string, string],
          }
        }
      },
    },
  },
}

Note: You do not need to install the SWC emotion plugin manually, as it is included with Next.js by default.

JesseKoldewijn commented 1 year ago

Doesn't sound like there is an issue aside from the documentation being lacking/confusing. At this point four different devs from my team interacted with trying to get this setup and none of us got it right, and Jesse actually got it wrong in the comments above as well.

Things that I think could be improved:

  • It's confusing/unexpected that emotion mostly works when disabled, even including features like the CSS prop which seemingly shouldn't work without having emotion support correctly enabled. This is what convinced me that I didn't need to enable compiler.emotion, and is not mentioned in the documentation in any way as far as I can tell. There is a precedent of frameworks enabling support for specific features when a related library is installed, so since it was working 99% of the time, I figured that simply installing emotion was all I had to do.
  • Strange to me that none of this appears to be documented anywhere:

    The Emotion plugin is only needed if you want to use features like: minification, dead code elimination, source maps, and "components as selectors"

  • Confusing that the error messages you get refer to the "the swc Emotion plugin" despite official support being present without having to install that plugin. Not sure who owns this error but if it's beyond the scope of NextJS then at least an official documentation could be added to explain what's necessary to fix the issue if you're using Next.
  • No documentation seems to exist which clarifies the requirement/role of the SWC emotion plugin. I'm clearly not the only one confused by this considering that Jesse also got this wrong while trying to explain how to correctly set this up (they answered my incorrect usage by giving an example repo where they installed the SWC emotion plugin, despite that not being necessary)

At very least the documentation could be changed to something like:

Emotion

We're working to port @emotion/babel-plugin to the Next.js Compiler. Since Next.js version (insert version here), most emotion core features work out of the box. To enable full support for advanced features such as minification, dead code elimination, source maps, and components as selectors, follow these instructions:

First, update to the latest version of Next.js: npm install next@latest. Then, update your next.config.js file:

// next.config.js

module.exports = {
  compiler: {
    // set to true/object to enable advanced emotion features.
    emotion: boolean | {
      // default is true. It will be disabled when build type is production.
      sourceMap?: boolean,
      // default is 'dev-only'.
      autoLabel?: 'never' | 'dev-only' | 'always',
      // default is '[local]'.
      // Allowed values: `[local]` `[filename]` and `[dirname]`
      // This option only works when autoLabel is set to 'dev-only' or 'always'.
      // It allows you to define the format of the resulting label.
      // The format is defined via string where variable parts are enclosed in square brackets [].
      // For example labelFormat: "my-classname--[local]", where [local] will be replaced with the name of the variable the result is assigned to.
      labelFormat?: string,
      // default is undefined.
      // This option allows you to tell the compiler what imports it should
      // look at to determine what it should transform so if you re-export
      // Emotion's exports, you can still use transforms.
      importMap?: {
        [packageName: string]: {
          [exportName: string]: {
            canonicalImport?: [string, string],
            styledBaseImport?: [string, string],
          }
        }
      },
    },
  },
}

Note: You do not need to install the SWC emotion plugin manually, as it is included with Next.js by default.

Hey man, sorry to hear the issue at hand is still present. The miscommunication around the actual need for the plugin is my bad, I didn't read through the docs well enough to notice this matter is already included inside NextJS.

Not to make excuses but I got a lot on my head with some stuff in my personal life and should have read through the docs better for a more accurate replication of the issue at hand.

trappar commented 1 year ago

It's totally okay! Not blaming you for anything :)

The issue is resolved for me. Just might be a good opportunity to improve the docs a bit.

JesseKoldewijn commented 1 year ago

It's totally okay! Not blaming you for anything :)

The issue is resolved for me. Just might be a good opportunity to improve the docs a bit.

Glad to hear man! đŸ˜„

brendanmorrell commented 10 months ago

So, this seems to still be an issue when feeding in a variable as the second option to styled( as in styled('div', options). when I put an inline options object in as the second argument, everything works fine (as in styled('div', {})), but if I define the options argument in a variable and then feed it in (const options ={}; styled('div', options)), I get the error Error: Component selectors can only be used in conjunction with @emotion/babel-plugin, the swc Emotion plugin, or another Emotion-aware compiler transform.

this is in a completely fresh build with nothing but next and emotion, the swc compiler set to emotion, and the emotion-root-layout-registry set up exactly as it is in the next example repo/ @Andarist's example is in the emotion github repo.

here is a codesandbox

https://codesandbox.io/p/devbox/emotion-swc-component-selector-options-q9r6dy

"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"eslint": "8.54.0",
"eslint-config-next": "^14.0.3",
"next": "^14.0.5-canary.41",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.3.2"
kdy1 commented 10 months ago

I think the transform for component selectors is not implemented in the swc plugin.

https://github.com/swc-project/plugins/tree/main/packages/emotion/transform

luchillo17 commented 9 months ago

I'm sorry to say it feels like the compiler.emotion config does more damage than good, at present I feel like I have to mark most of my components that do not need for 'use client'; with /** @jsxImportSource react */, I'm not sure if it's the compiler option fault, or the TSConfig compilerOptions.jsxImportSource: "@emotion/react", I ended up disabling both and everything worked ok, so how are these 2 options supposed to be used then?

pfmartins-zartis commented 4 months ago

I'm trying to make SWC work with MUI/Emotionand it none of the available solutions seem to work Heres a base code https://codesandbox.io/p/devbox/frosty-lake-v6ymx4

jove4015 commented 1 week ago

Just ran into this moving from styled components to emotion for MUI. We were using an options object to replace the "transient prop" behavior for "$" props. Passing the object directly causes this error (only when compiling in dev mode). You can avoid the error by supplying a literal instead - but you don't actually have to rewrite all of the code for shouldForwardProp each time - instead I've discovered you can simply spread the object out into an empty object literal, so:

Instead of this:

export const transientOptions: Parameters<CreateStyled>[1] = {
  shouldForwardProp: (propName: string) => !propName.startsWith("$"),
};
....
// This will cause an error with emotion/SWC
export const TimelineEvent = styled(Box, transientOptions)<{ $prop: boolean; }>` ... code which contains component selectors...`

You can just do this, which works fine:

export const TimelineEvent = styled(Box, {...transientOptions})<{ $prop: boolean; }>`... code which contains component selectors...`