Skip to content

turbopack support#1639

Open
RJWadley wants to merge 70 commits intovanilla-extract-css:masterfrom
RJWadley:turbo-loader
Open

turbopack support#1639
RJWadley wants to merge 70 commits intovanilla-extract-css:masterfrom
RJWadley:turbo-loader

Conversation

@RJWadley
Copy link

@RJWadley RJWadley commented Oct 8, 2025

adds turbopack support to @vanilla-extract/next-plugin

turbopack doesn't have any sort of external API yet, so in order to evaluate the contents of our *.css.ts files we need to compile it separately using @vanilla-extract/compiler. To avoid import resolution mismatches, we defer module resolution to turbopack via its webpack-compatible api.

next/font doesn't work out of the box because turbopack has custom logic for it, so we transform any modules that use it in a css context to contain static references to the generated font family, style, and weight. next/image gets a similar treatment. most other next modules won't work in css.ts files for now.

fixes #1367

@changeset-bot
Copy link

changeset-bot bot commented Oct 8, 2025

🦋 Changeset detected

Latest commit: 4ff3498

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@vanilla-extract/turbopack-plugin Minor
@vanilla-extract/next-plugin Minor
@vanilla-extract/compiler Minor
@vanilla-extract/css Patch
@vanilla-extract/vite-plugin Patch
@vanilla-extract/integration Patch
@vanilla-extract/rollup-plugin Patch
@vanilla-extract/esbuild-plugin Patch
@vanilla-extract/jest-transform Patch
@vanilla-extract/parcel-transformer Patch
@vanilla-extract/webpack-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@csorfab
Copy link

csorfab commented Oct 19, 2025

@mattcompiles @askoufis Can anyone take a look at this? This is a pretty important feature for next.js users. Or should we consider the project dead? Last PR was merged in early August, basically no activity from maintainers since then. Not meaning to bash anyone, I know it's thankless work maintaining open source software, but it would be nice to know what we can expect from this project. Thanks in advance!

@askoufis
Copy link
Contributor

Or should we consider the project dead? Last PR was merged in early August, basically no activity from maintainers since then.

A PR was merged in September. Reviews take time and ongoing work is often slow. All current maintainers have professional commitments and/or families.

I try to look at PRs when I have spare time/motivation, which is often quite sporadic. I'm currently on holiday, but I intend to make some headway on open PRs and repo maintenance in when I get back in early November.

@csorfab
Copy link

csorfab commented Oct 19, 2025

Thanks for your reply! I was a bit too dramatic, sorry about that. Enjoy your holiday, and let me know if I can help with anything to get this PR going.

@RJWadley RJWadley marked this pull request as draft October 23, 2025 04:49
@callum-gander
Copy link

I see there have been some recent commits pushed up to this PR. Are there any bits that you need help with that could be provided to speed up getting this merged in?

Appreciate that it's been a holiday period, but a lot of apps have performance issues with Next which has various potential solutions but the main recommendation seems to boil down to "just use Turbopack" vercel/next.js#48748. Obviously, the lack of support for Turbopack with Vanilla is effectively blocking us from getting those gains, which would be greatly appreciated ahead of Black Friday.

It would be great if there were a list of tasks that I or people within my company could help out with to speed this up, whether that's commits or just QAing. Cheers and thanks for the work on this so far!

@RJWadley
Copy link
Author

RJWadley commented Nov 17, 2025

If you're interested in trying this out, I've been using a preinstall script to download and install this branch:

"preinstall": "pnpm dlx gitpick https://github.com/RJWadley/vanilla-extract/tree/turbo-loader/ /tmp/vanilla-temp -f && cd /tmp/vanilla-temp && pnpm i && pnpm build"

You can install that temporary build with a link:

"@vanilla-extract/next-plugin": "link:/tmp/vanilla-temp/packages/next-plugin",

Although, you probably want to pin to a specific commit instead of the branch for the sake of stability:

"preinstall": "pnpm dlx gitpick RJWadley/vanilla-extract -b f5163f1 /tmp/vanilla-temp -f && cd /tmp/vanilla-temp && pnpm i && pnpm build"

Feedback or issues appreciated! I haven't seen many stats yet on speed or size of builds (compared to webpack) so if you have those I'd love to see some details.


The biggest issue with this at the moment is next specific modules not playing nicely. Since we must process styles outside of next.js there's not much that can be done. Think next/font, next/image, etc. Next has decent webpack loader support, but there are a few things missing that make it much harder (and in some cases impossible) for us to preprocess styles.

I'm gonna do a quick cleanup pass this week to prep this for merge, it's basically ready at this point but is still a bit messy while I'm experimenting with some stuff.

TLDR:

  • if your styles don't use next modules at all everything will work fine
  • next/font partially works
  • next/image imports don't work at all
  • other next modules may or may not work

@callum-gander
Copy link

callum-gander commented Nov 18, 2025

Hmmm, strange, I followed your steps and got this error

@frontend/app:dev:    ▲ Next.js 15.5.0
@frontend/app:dev:    - Local:        http://localhost:3000
@frontend/app:dev:    - Network:      http://10.100.22.2:3000
@frontend/app:dev:    - Environments: .env
@frontend/app:dev:    - Experiments (use with caution):
@frontend/app:dev:      ⨯ reactCompiler
@frontend/app:dev:      · optimizePackageImports
@frontend/app:dev: 
@frontend/app:dev:  ✓ Starting...
@frontend/app:dev: Warning: total number of custom routes exceeds 1000, this can reduce performance. Route counts:
@frontend/app:dev: headers: 5
@frontend/app:dev: rewrites: 2
@frontend/app:dev: redirects: 1246
@frontend/app:dev: See more info: https://nextjs.org/docs/messages/max-custom-routes-reached
@frontend/app:dev: Error: > Couldn't find a `pages` directory. Please create one under the project root
@frontend/app:dev:     at Object.webpack (../../../../../../../../private/tmp/vanilla-temp/packages/next-plugin/dist/vanilla-extract-next-plugin.cjs.dev.js:130:47)

That seemed to point at this specific line in the dist

 const findPagesDirResult = findPagesDir.findPagesDir(dir, ((_resolvedNextConfig$e = resolvedNextConfig.experimental) === null || _resolvedNextConfig$e === void 0 ? void 0 : _resolvedNextConfig$e.appDir) ?? false);

It's probably worth saying that we don't have a pages dir at all, we're fully on app dir. To be fair as well, I'm not sure that this would work for us anyway as we make heavy use of next/image, next/font, as well as other next modules.

When you say it's basically ready, do you mean basically ready minus support for next modules due to dependency on things turbopack isn't yet providing to help you preprocess styles?

If you have a solution to the above error, more than happy to give it a whirl

@RJWadley
Copy link
Author

RJWadley commented Nov 18, 2025

The error looks like resolution error in the webpack plugin setup. Should've seen that coming.
Switch back to the release version of next-plugin, then try configuring the loader manually:

"@vanilla-extract/turbopack-plugin": "link:/tmp/vanilla-temp/packages/turbopack-plugin",
  turbopack: {
    rules: {
      '**/*.css.{ts,tsx,js,jsx}': {
        loaders: [
          {
            loader: '@vanilla-extract/turbopack-plugin',
            options: { nextEnv: {} }, // include env vars set by next.config if any
          },
        ],
      },
    },
  },

As far as next/image support, you're free to style next images as this doesn't route through the plugin:

// page.ts
<Image className={myVanillaClass} />

The specific scenario that doesn't work is importing an image into a css.ts file and using it to generate styles:

// myStyles.css.ts
import coolThing from "./my-cool-image.png"

export const background = style({
  backgroundImage: `url(${coolThing.src})` // fails! use public dir instead
})

This is unlikely to change until we get better tools from turbopack.

For next/font, support is better. Anything under the style property is supported:

// myStyles.css.ts
import myFont from "./typography"

export const background = style({
  fontFamily: myFont.style.fontFamily
})

The specific scenario that doesn't work is extending font classNames in your styles:

// myStyles.css.ts
import myFont from "./typography"

export const background = style(myFont.className) // fails! set font family directly instead

This is also unlikely to change until we get better tools from turbopack.

TLDR using next modules is fine, but using next modules in your styles themselves is likely to fail unless I've specifically worked around it.

@Pnlvfx
Copy link
Contributor

Pnlvfx commented Nov 21, 2025

The error looks like resolution error in the webpack plugin setup. Should've seen that coming. Switch back to the release version of next-plugin, then try configuring the loader manually:

"@vanilla-extract/turbopack-plugin": "link:/tmp/vanilla-temp/packages/turbopack-plugin",
  turbopack: {
    rules: {
      '**/*.css.{ts,tsx,js,jsx}': {
        loaders: [
          {
            loader: '@vanilla-extract/turbopack-plugin',
            options: { nextEnv: {} }, // include env vars set by next.config if any
          },
        ],
      },
    },
  },

As far as next/image support, you're free to style next images as this doesn't route through the plugin:

// page.ts
<Image className={myVanillaClass} />

The specific scenario that doesn't work is importing an image into a css.ts file and using it to generate styles:

// myStyles.css.ts
import coolThing from "./my-cool-image.png"

export const background = style({
  backgroundImage: `url(${coolThing.src})` // fails! use public dir instead
})

This is unlikely to change until we get better tools from turbopack.

For next/font, support is better. Anything under the style property is supported:

// myStyles.css.ts
import myFont from "./typography"

export const background = style({
  fontFamily: myFont.style.fontFamily
})

The specific scenario that doesn't work is extending font classNames in your styles:

// myStyles.css.ts
import myFont from "./typography"

export const background = style(myFont.className) // fails! set font family directly instead

This is also unlikely to change until we get better tools from turbopack.

TLDR using next modules is fine, but using next modules in your styles themselves is likely to fail unless I've specifically worked around it.

For me is fine, thanks, I also had next font issues with webpack so I don't mind this approach,

thank you very much for your work,

I'll try this ASAP

@nandahibatullah
Copy link

Hello, when is this planned on being approved and release?

This would be so good to get in soon, tested it out with this branch and it seemed to be working great.

@dy0gu
Copy link

dy0gu commented Feb 19, 2026

I've also tested these changes in a relatively complex Turbopack Next project and everything worked flawlessly.

@dasutyuzh
Copy link

I've also tested these changes in a relatively complex Turbopack Next project and everything worked flawlessly.

I have not but am invested in this thread :-) how can I persuade a merge?

@jksaunders
Copy link

Also very invested in this thread!

@gardsa
Copy link

gardsa commented Feb 19, 2026

Along with many others, this PR is a blocker for us moving fully onto Next with Turbopack (we’re otherwise on Webpack due to lack of support here).

Could a maintainer share a rough status or timeline for review/merge, or point to anything still needed from contributors?

We’re happy to help with rebases, fixes, or testing to get this ready. Thanks

@trekinbami
Copy link

It's been almost six months. I think it's safe to say this is not going to get merged. It might be a good idea to see if we can turn this PR into a plugin, or "just" fork?

@malechite
Copy link

malechite commented Feb 23, 2026

Maybe @mattcompiles and @askoufis should promote @RJWadley or some other contributors to be Maintainers of Vanilla Extract if they don't have the time

@askoufis
Copy link
Contributor

I started reviewing this over the weekend. It's a large PR so it will take some time.

@zecka
Copy link

zecka commented Feb 23, 2026

This might be an edge case, but I am hitting a failure when outputFileTracingRoot is set outside of the Next.js project root.

For example:

//next.config.ts
export const config: NextConfig = {
  outputFileTracingRoot: path.join(__dirname, '../../'),
};

With this configuration, Turbopack log the following error:

FATAL: An unexpected Turbopack error occurred. A panic log has been written to /var/folders/0t/x_bh86jn0cv7mvtwwdxp_plr0000gp/T/next-panic-6b92f89cf755c87a1be69f2c01527830.log.

Here is the complete log

---------------------------
Resource path "nextjs/app/page.css.ts" need to be on project filesystem "apps/nextjs"

Debug info:
- Execution of hmr_update_with_issues_operation failed
- Execution of PlainIssue::from_issue failed
- Execution of PlainSource::from_source failed
- Execution of <WebpackLoadersProcessedAsset as Asset>::content failed
- Execution of WebpackLoadersProcessedAsset::process failed
- Resource path "nextjs/app/page.css.ts" need to be on project filesystem "apps/nextjs"
---------------------------
Resource path "nextjs/app/page.css.ts" need to be on project filesystem "apps/nextjs"

Here is a minimal reproduction on the try/outputFileTracingRoot branch:
https://github.com/zeckaissue/vanilla-extract-turbopack-test-yarn/tree/try/outputFileTracingRoot

Notes:

  • the issue doesn't appear with css module
  • (My goal with outputFileTracingRoot is to work locally with a UI library linked via yarn link. It is possible that this workflow is not supported and that I need to adjust how I work locally, but I wanted to report the behavior in case this is unintended.)

@kelseyway
Copy link

+1 on this, would be a massive win to have this in. currently blocked by this, and my company would love to use vanilla extract out the box with our turbopack next app

@RJWadley
Copy link
Author

Here is a minimal reproduction on the try/outputFileTracingRoot branch:
zeckaissue/vanilla-extract-turbopack-test-yarn@try/outputFileTracingRoot

@zecka I wasn't able to reproduce the issue on this repository when I cloned it. I would presume the issue is caused by the way things are linked on your filesystem.

Just guessing here, but one potential issue that I have seen before is that you may need to configure turbopack.root as turbopack is a bit more picky with how it resolves modules than webpack is. Try setting it to match your outputFileTracingRoot configuration.

Since you're using yarn link, you may also need to set the root higher (potentially as high as the filesystem root) to get turbopack to resolve the modules.

here's the relevant bit of documentation for turbopack.root

omd0 pushed a commit to Ruya-WEB/vanilla-extract that referenced this pull request Feb 24, 2026
Implements Turbopack compatibility based on PR vanilla-extract-css#1639 by @RJWadley:

- packages/compiler: add `splitCssPerRule` option, `invalidateAllModules()`
  method, and update `cssImportSpecifier` to accept CSS content + return
  Promise<string>
- packages/css: add `vanilla.virtual.css` noop file for virtual CSS imports,
  update package.json exports and files list
- packages/next-plugin: auto-detect Next >= 16 and configure Turbopack rules;
  add `turbopackMode` ('auto'|'on'|'off') and `turbopackGlob` options;
  add semver + @vanilla-extract/turbopack-plugin dependencies
- packages/turbopack-plugin: new package — Turbopack webpack loader that
  compiles *.css.ts files via @vanilla-extract/compiler, encodes CSS into
  virtual query-param imports on vanilla.virtual.css, and handles next/font
  stubs via SWC transform

https://claude.ai/code/session_0145VazPBZC9Q6nKK8gfjrXf
@zecka
Copy link

zecka commented Feb 24, 2026

Here is a minimal reproduction on the try/outputFileTracingRoot branch:
zeckaissue/vanilla-extract-turbopack-test-yarn@try/outputFileTracingRoot

@zecka I wasn't able to reproduce the issue on this repository when I cloned it. I would presume the issue is caused by the way things are linked on your filesystem.

You’re right, it’s probably related to my local system configuration.
I’m not able to reproduce the issue on Codespaces, for example.
However, I can still reproduce it on my Mac, even with a fresh clone of the repository.
I’ll drop it for now, this is likely too much of an edge case to consider.

Copy link
Contributor

@askoufis askoufis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a few questions and suggestions but overall this looks good. I'll do another review once the feedback has been addressed and I get more time to run the fixtures locally.

"test:playwright": "pnpm test:build-next && playwright test",
"test:build-next": "node scripts/copy-next-plugin.ts && pnpm --filter=@fixtures/next-* clean-build",
"test:playwright": "pnpm test:clean-next && pnpm test:build-next && playwright test",
"test:clean-next": "pnpm --filter=@fixtures/next-* run '/^clean.*/'",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused about test:clean-next because it looks like you've removed the clean scripts from the next fixtures. Should they have been removed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the cleaning is all still in there, I just removed the clean-build shortcut that calls clean + build.

this is just because we're testing webpack + turbopack and I wanted them to build in parallel, and it seemed like the easiest way at the time.

I would rather remove the cleaning entirely, as it shouldn't be needed, but thought that would be best for a future PR if at all.

@@ -0,0 +1,13 @@
import { NextConfig } from 'next';
import { createVanillaExtractPlugin } from './next-plugin/dist/vanilla-extract-next-plugin.cjs.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this import be using the package specifier rather than relative path?

Suggested change
import { createVanillaExtractPlugin } from './next-plugin/dist/vanilla-extract-next-plugin.cjs.js';
import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin';

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would be better IMO, but neither of the existing next fixtures do, and we'll experience type errors if we try. see scripts/copy-next-plugin.ts for a bit more context.

this is another thing I'd like to clean up soon, but opted to just match the status quo for the first revision. happy to take another look if you want it in this PR.

"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependencies": {
"next": ">=12.1.7"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the docs say only next>=16 supports the turbopack plugin, should this version range reflect that too, or is this purely so package managers don't complain when a user that doesn't want to use turbopack installs @vanilla-extract/next-plugin?

Copy link
Author

@RJWadley RJWadley Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is purely so that package managers don't complain. users don't install turbopack-plugin directly, it's installed by the next plugin. so any users on old next version would see peer errors.

Turbopack will only configure itself if next is >= 16. The user could still attempt to manually configure turbopack on next 15, but it would always crash if they did. It would be trivial to warn in this case, which I think is a good idea.

edit: added

Comment on lines +266 to +272
/**
* When true, generates one CSS import per rule instead of one per file.
* This can help bundlers like Turbopack deduplicate shared CSS more effectively.
*
* @default false
*/
splitCssPerRule?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused about this. If you're already sharing the same VE style in multiple places, isn't there no need to deduplicate anything?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't confirm how webpack or any other integration works, but before adding this we were seeing massive amounts of duplication across reused files. For example, every file that used our color library would re-bundle the entire color library (and it would be included hundreds of times)

I added a test that catches some common duplication issues, and splitting each rule out per import was the easiest way to deduplicate them all.

turbopack preserves the import order when bundling the CSS

Comment on lines +21 to +30
type PluginOptions = ConstructorParameters<typeof VanillaExtractPlugin>[0] & {
turbopackGlob?: string[];
/**
* controls whether we attempt to configure turbopack
* auto: enable only when Next >= 16.0.0
* on: force enable regardless of detected Next version
* off: never configure turbopack, webpack only
*/
turbopackMode?: 'auto' | 'on' | 'off';
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer these options to be nested under an unstable_turbopack property for 2 reasons:

  • I'm not confident that this integration won't require breaking changes in the near future
  • Grouping turbopack-related options together seems a bit neater than prefixing everything with turbopack

Copy link
Author

@RJWadley RJWadley Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually great idea, I might also do this to the compiler option I added to reset the cache (since it's only there to work around a bug in the compiler)

edit: done. also renamed the two experimental compiler apis.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you also want turbopack support to disable to "off" instead of "auto"? auto mode configures turbopack if we're on next >=16

},
"exports": {
"./package.json": "./package.json",
"./vanilla.virtual.css?*": "./vanilla.virtual.css?*",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the ?* necessary here? AFAIK query parameters should be preserved during module resolution, and they don't need to be specified in export conditions.

Suggested change
"./vanilla.virtual.css?*": "./vanilla.virtual.css?*",
"./vanilla.virtual.css": "./vanilla.virtual.css",

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not originally include it, but iirc I added it because turbopack does require it. will double check.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed. turbopack does not strip query strings before matching against the exports field. without the wildcard, every CSS import will fail.

Comment on lines +72 to +74
return `@vanilla-extract/css/vanilla.virtual.css?ve-css=${encodeURIComponent(
await serializeCss(css),
)}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the vanilla.virtual.css file live inside the turbopack plugin package instead of the css package? I'd rather it be associated with the plugin it's relevant to, rather than being included in the css package.

Copy link
Author

@RJWadley RJWadley Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turbopack must be able to resolve the import from the source file itself - so if you're compiling styles across packages in a monorepo (like how, for example, our test fixtures do) turbopack would not be able to resolve @vanilla-extract/turbopack-plugin. Turbopack searches for that module from the importing file, so the only package that is guaranteed to exist is @vanilla-extract/css. Turbopack refused to resolve relative imports here, and it doesn't support importing the absolute path.

Originally I used data imports here but those are not deduplicated by turbopack.

Resolution and compilation across package boundaries is something I'd like to revisit at some point, as every integration seems to handle this slightly differently.

@RJWadley
Copy link
Author

@askoufis thanks for the review! addressed feedback and replied to comments, I will leave resolving them up to you at your satisfaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot build a next app with Turbopack (Styles were unable to be assigned to a file)