vanilla-extract: errors when using external ui library implemented with vanilla-extract in next.js app

Describe the bug

In a Next.js app using the @vanilla-extract/next-plugin to support VE locally, everything works as expected. However, when trying to use view code from an external library that has styles implemented with vanilla-extract, there are render errors.

The minimal example for can be found here:

The following behavior is observed while the Next.js app is running in dev mode:

A. The external code is not used on the page.

  1. Refresh the page.
  2. Observe that the page loads successfully.

B. The external code is added to the page.

  1. Observe that the page injects the external code to the page successfully.
  2. Refresh the page.
  3. Observe that the page displays the error described below.
Server Error
Error: Styles were unable to be assigned to a file. This is generally caused by one of the following:

- You may have created styles outside of a '.css.ts' context
- You may have incorrect configuration. See https://vanilla-extract.style/documentation/getting-started

Reproduction

https://github.com/jneander/example-app-using-third-party-vanilla-extract-ui-library

System Info

System:
  OS: macOS 13.0
  CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  Memory: 359.04 MB / 16.00 GB
  Shell: 3.2.57 - /bin/bash
Binaries:
  Node: 18.12.1 - ~/.nvm/versions/node/v18.12.1/bin/node
  Yarn: 1.22.17 - /usr/local/bin/yarn
  npm: 8.19.2 - ~/.nvm/versions/node/v18.12.1/bin/npm
Browsers:
  Chrome: 108.0.5359.94
  Firefox: 97.0.2
  Safari: 16.1
npmPackages:
  @vanilla-extract/css: ^1.9.2 => 1.9.2 
  @vanilla-extract/next-plugin: ^2.1.1 => 2.1.1

Used Package Manager

npm

Logs

Server Error
Error: Styles were unable to be assigned to a file. This is generally caused by one of the following:

- You may have created styles outside of a '.css.ts' context
- You may have incorrect configuration. See https://vanilla-extract.style/documentation/getting-started

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
Object.getFileScope
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@vanilla-extract/css/fileScope/dist/vanilla-extract-css-fileScope.cjs.dev.js (33:11)
generateIdentifier
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js (183:49)
style
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js (404:19)
Object.<anonymous>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@jneander/x-throwaway-vanilla-extract-ui/dist/cjs/example.css.js (5:32)
Module._compile
node:internal/modules/cjs/loader (1159:14)
Module._extensions..js
node:internal/modules/cjs/loader (1213:10)
Module.load
node:internal/modules/cjs/loader (1037:32)
Module._load
node:internal/modules/cjs/loader (878:12)
Module.require
node:internal/modules/cjs/loader (1061:19)
require
node:internal/modules/cjs/helpers (103:18)
Object.<anonymous>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@jneander/x-throwaway-vanilla-extract-ui/dist/cjs/example.js (5:23)
Module._compile
node:internal/modules/cjs/loader (1159:14)
Module._extensions..js
node:internal/modules/cjs/loader (1213:10)
Module.load
node:internal/modules/cjs/loader (1037:32)
Module._load
node:internal/modules/cjs/loader (878:12)
Module.require
node:internal/modules/cjs/loader (1061:19)
require
node:internal/modules/cjs/helpers (103:18)
Object.<anonymous>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/@jneander/x-throwaway-vanilla-extract-ui/dist/cjs/index.js (17:14)
Module._compile
node:internal/modules/cjs/loader (1159:14)
Module._extensions..js
node:internal/modules/cjs/loader (1213:10)
Module.load
node:internal/modules/cjs/loader (1037:32)
Module._load
node:internal/modules/cjs/loader (878:12)
Module.require
node:internal/modules/cjs/loader (1061:19)
require
node:internal/modules/cjs/helpers (103:18)
@jneander/x-throwaway-vanilla-extract-ui
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/pages/index.js (54:18)
__webpack_require__
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/webpack-runtime.js (33:42)
eval
webpack-internal:///./pages/index.tsx (8:98)
./pages/index.tsx
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/pages/index.js (43:1)
__webpack_require__
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/webpack-runtime.js (33:42)
__webpack_exec__
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/pages/index.js (75:39)
<unknown>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/pages/index.js (76:28)
Object.<anonymous>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/.next/server/pages/index.js (79:3)
Module._compile
node:internal/modules/cjs/loader (1159:14)
Module._extensions..js
node:internal/modules/cjs/loader (1213:10)
Module.load
node:internal/modules/cjs/loader (1037:32)
Module._load
node:internal/modules/cjs/loader (878:12)
Module.require
node:internal/modules/cjs/loader (1061:19)
require
node:internal/modules/cjs/helpers (103:18)
Object.requirePage
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/require.js (88:12)
<unknown>
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/load-components.js (37:73)
async Object.loadComponents
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/load-components.js (37:26)
async DevServer.findPageComponents
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/next-server.js (548:36)
async DevServer.renderPageComponent
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/base-server.js (926:24)
async DevServer.renderToResponse
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/base-server.js (955:32)
async DevServer.pipe
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/base-server.js (406:25)
async Object.fn
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/next-server.js (744:21)
async Router.execute
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/router.js (252:36)
async DevServer.run
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/base-server.js (383:29)
async DevServer.run
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/dev/next-dev-server.js (734:20)
async DevServer.handleRequest
file:///Users/jeremy/Projects/temp/example-app-with-ve-bug/node_modules/next/dist/server/base-server.js (321:20)

Validations

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 3
  • Comments: 27 (11 by maintainers)

Most upvoted comments

I appreciate all of the ideas and collaboration here. ❤️ I’ll make my way back to this eventually. Work has been eating me alive and I don’t have anything left in the tank right now for tackling this issue.

I think I found a definitive fix. Make this change to your next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  webpack(config, options) {
    config.module.rules.unshift({
      test: /@your-theme-scope/, // Can also be your individual package name
      resolve: {
        mainFields: ['module'],
      },
    });

    return config;
  },
};

This will make it so that the imports are consistent between client and server. You also need to write an exports field in your package as well as your main and module fields (which I thought was pretty weird but it consistently fixed the issue):

{
  "main": "./dist/cjs/index.css.js",
  "module": "./dist/esm/index.css.js",
  "types": "./dist/types/index.css.d.ts",
  "exports": {
    ".": {
      "import": "./dist/esm/index.css.js",
      "require": "./dist/cjs/index.css.js",
      "types": "./dist/types/index.css.d.ts"
    }
  },
}

Finding this was a result of a very frustrating trial an error process, but I’m 100% sure that this is a problem on Next.js’ fault. If the original poster could try this I think we could also close the issue 😃

EDIT:

After we did some internal investigation, this solution was weirdly only working inside of our monorepo, not in packages consuming our toolkit. The definite absolutely final solution was actually to add our VE-dependant packages to the transpilePackages key on the next.js configuration. Next.js Docs

@viclafouch we had some challenges with VE UI Library to integrate it with Vite. With some tweaking in the Vite config, we succeed to make it work. The hack was to exclude the UI library packages from the “optimizeDeps”.

Dep Optimization Options

This is how our vite.config.ts looks now:

import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), vanillaExtractPlugin()],
  optimizeDeps: {
    exclude: [
      "@filamix-ui",
      "@filamix-ui/generics",
    ],
  },
});

Worth mentioning is that from “@filamix-ui” we are exporting subpaths thru package.json. It works fine. Just make sure in your tsconfig.json to set “compilerOptions” > “moduleResolution” to “Node16”.

Hey @askoufis. No worries about the delay.

This warrants further debugging, as there seems to be something allowing the mismatch to happen. If the easy fix of dropping cjs holds up, then I might have a path forward for now. But for folks needing both esm and cjs for legacy purposes, this little gremlin might be a showstopper. I’ll dig deeper and see what I come up with. It’s as good a time as any to learn more about the VE internals.

Any new findings on this @jneander ? I have the same case as you, UI library that exports VE and I need to support both esm and cjs.

It gets complex in the context of Next.js because it doesn’t transpile node_modules out of the box.

As I stated in a previous edit, I think this is a problem on Next.js’ part. Just by looking at the classNames generated, I can see that the server is serving from the cjs module while the client is importing from the esm module. There is a stale issue about this on their github (https://github.com/vercel/next.js/issues/35581). The workaround is deleting the “module” key from our package.json.

Is that not possible with VE currently? Each component gets its own CSS file after transpilation so we can update styles of one component without affecting others. Variable scoping shouldn’t be a problem as all bundlers aim for deterministic builds. So if you run a build twice on the same code, it shouldn’t mean that you get different class names each time. I’d assume VE’s hashing algorithm is deterministic too, it’s a bug if it isn’t.

Can you please elaborate a bit more? I’m really not seeing the benefit in moving transpilation to consumer projects. All it’ll achieve is make installation of your UI toolkit harder for users.

Sure! It indeed is possible, however as I said, you may end up running into scoping issues. We extensively use contracts for our values, and there is one main theme which gets consistently referenced by all our components. If at some point, someone decides to make a small change to the main theme, it means that only the theme package needs to be updated. This would not be the case with pre-built CSS, since building the theme package would generate a new hash for the variable scope and thus all the CSS that references that would have to be rebuilt, even if they weren’t changed at all. Since all of our contracts also come from the same place, it would mean that adding a new component would have to trigger a new version for all the components as well, which pretty much just invalidates the reasoning for having separate component versions.

It looks like this is more of a feature request than a bug. Shipping styles without compilation isn’t really how Vanilla Extract is meant to be used. As a UI library author, I don’t see the benefit in forcing every user to compile the VE styles themselves from node_modules.

My component library is working without issues in Next.js v13.1.2 app directory. For reference, here’s the library source code and usage in a Next.js app.

For anyone getting blocked by this, the solution might just be to add the respective bundler plugin to your library so you’re shipping static styles, not TypeScript. And if there are any significant benefits to shipping TypeScript and making users compile VE styles, I’d love to see a docs page on this.

This warrants further debugging, as there seems to be something allowing the mismatch to happen. If the easy fix of dropping cjs holds up, then I might have a path forward for now. But for folks needing both esm and cjs for legacy purposes, this little gremlin might be a showstopper. I’ll dig deeper and see what I come up with. It’s as good a time as any to learn more about the VE internals.

I think I’ve been running into the same thing (with just the Webpack plugin). For me, it boiled down to the fact that my component library is published in CJS, and I don’t believe that the Vanilla Extract tooling supports CommonJS. I think I have a fix in #970.

In the meantime, I found that the workaround is to include the @vanilla-extract/babel-plugin (now deprecated) in the library build step. It appears to inject the addFileScope stuff prior to the file being converted to ESM. So now my published Button.css.js file looks something like this:

Object.defineProperty(exports, "__esModule", {
  value: true
});

var __vanilla_filescope__ = _interopRequireWildcard(require("@vanilla-extract/css/fileScope"));

var _css = require("@vanilla-extract/css");

__vanilla_filescope__.setFileScope("src/Button/Button.css.ts", "my-library");

var button = _css.style({});

exports.button = button;

__vanilla_filescope__.endFileScope();

Then when the app consumes, this file, @vanilla-extract/integration doesn’t need to care if the file is CJS or ESM. See here.

I don’t think this is the ideal solution by any means, but it’s a decent work around, and I think this can be handled upstream (maybe my PR is the right solution, maybe not).

Hi @jneander. Sorry for the delayed response.

In my original comment I thought you were trying to achieve something different, but given the context you’ve provided I think I now understand the problem better.

As far as I know, there’s no reason your approach shouldn’t work. Vanilla extract integrations should find files ending in .css.ts OR .css.js. The example repo linked in the issue you referenced transpiles its files to javascript, and those files are consumed just fine in the app adjacent to the components library. https://github.com/mihkeleidast/vanilla-extract-component-library-example

In fact, my team at SEEK is working on a packaging tool that works by this principle, as our design system has gotten large enough that it has warranted investment in speeding up build times. Currently we just ship raw typescript and put the onus on the consumer to transpile it in their app, which as you’ve mentioned, doesn’t scale very well.

The error you mentioned in the top-level issue comment is coming from here, which is called slightly further down the call stack from the style function in your css.js file. The webpack plugin should be inserting a setFileScope call before style is called, which would prevent that error from being thrown.

Debugging your app, I noticed that the VE webpack plugin was transforming the esm version of example.css.js, and then the app was loading the untransformed cjs version of example.css.js, causing the error. Why this is happening I’m not so sure about, but that seems to be the crux of the issue. This could be something to do with the component library’s package.json, maybe an exports key could do the trick. I’m not super knowledgeable about ESM resolution, but I do know it has some weird quirks that can lead to unexpected results.

Hope this helps.

Hi, thanks for the reproduction.

Your app looks fine to me. I think the issue might be how your UI library is bundled. You’ll need to use one of the bundler integrations to create a package that can be consumed correctly in a project that uses vanilla-extract.

Take a look at the webpack or the rollup integrations for some ideas of how to get that working. Alternatively, a discord user recently shared an example repo that uses rollup to bundle a UI package that uses vanilla-extract for styling. https://github.com/graup/vanilla-extract-rollup-example/tree/main/packages/ui.

Feel free to join the discord for some quicker feedback from community members if you need some help getting things working.