Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v4] Nested unknown at-rule (for PostCSS plugins) is ejected from rule during build #15185

Open
vnphanquang opened this issue Nov 26, 2024 · 2 comments

Comments

@vnphanquang
Copy link

vnphanquang commented Nov 26, 2024

Version

[email protected] and @tailwindcss/[email protected]

Environment

Output of npx envinfo --system --binaries --browsers --npmPackages "{vite,tailwindcss,@tailwindcss/*,postcss-mixins}":

  System:
    OS: Linux 6.6 Arch Linux
    CPU: (4) x64 Intel(R) Core(TM) i5-4590 CPU @ 3.30GHz
    Memory: 8.86 GB / 15.57 GB
    Container: Yes
    Shell: 3.7.1 - /usr/bin/fish
  Binaries:
    Node: 22.11.0 - ~/.volta/tools/image/node/22.11.0/bin/node
    Yarn: 4.5.1 - ~/.volta/tools/image/yarn/4.5.1/bin/yarn
    npm: 10.9.0 - ~/.volta/tools/image/node/22.11.0/bin/npm
    pnpm: 9.12.3 - ~/.volta/bin/pnpm
  Browsers:
    Chromium: 131.0.6778.85
  npmPackages:
    @tailwindcss/vite: 4.0.0-beta.1 => 4.0.0-beta.1
    postcss-mixins: ^11.0.3 => 11.0.3
    tailwindcss: 4.0.0-beta.1 => 4.0.0-beta.1
    vite: ^5.4.10 => 5.4.11

Reproduction

https://github.com/vnphanquang/tailwind-4-at-rule-build-reproduction. Please see README for steps and expected output.

Or better, use this suggested test case:

Expand test case
// @file: integrations/vite/postcss-plugins.test.ts

import { expect } from 'vitest'
import { css, html, json, test, ts, retryAssertion, fetchStyles } from '../utils'

const config: Parameters<typeof test>[1] = {
  fs: {
    'package.json': json`
      {
        "type": "module",
        "dependencies": {
          "@tailwindcss/vite": "workspace:^",
          "postcss-mixins": "^11.0.3",
          "tailwindcss": "workspace:^"
        },
        "devDependencies": {
          "vite": "^5.3.5"
        }
      }
    `,
    'vite.config.ts': ts`
      import tailwindcss from '@tailwindcss/vite'
      import postcssMixins from 'postcss-mixins'
      import { defineConfig } from 'vite'

      export default defineConfig({
        css: {
          transformer: 'postcss',
          postcss: {
            plugins: [postcssMixins()],
          },
        },
        plugins: [tailwindcss()],
      })
    `,
    'index.html': html`
      <head>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <div class="test"></div>
      </body>
      `,
    'styles.css': css`
      @import 'tailwindcss/utilities';

      @define-mixin box {
        background-color: green;
      }

      .box {
        width: 8rem;
        height: 8rem;
        background-color: red;

        @mixin box;
      }
    `,
  },
}

// currently passing
test(
  `dev mode`,
  config,
  async ({ spawn, getFreePort }) => {
    let port = await getFreePort()
    await spawn(`pnpm vite dev --port ${port}`)

    await retryAssertion(async () => {
      let styles = await fetchStyles(port, '/index.html')

      expect(styles).not.contain('@define-mixin box')
      expect(styles).not.contain('@mixin box')
      expect(styles).contain(css`
        .box {
          width: 8rem;
          height: 8rem;
          background-color: red;
          background-color: green;
        }
      `)
    })
  },
  {
    debug: true,
  },
)

// currently failing
test(
  `production build`,
  config,
  async ({ fs, exec }) => {
    await exec('pnpm vite build')

    let files = await fs.glob('dist/**/*.css')
    expect(files).toHaveLength(1)

    const file = files[0][0]
    await fs.expectFileNotToContain(file, ['@define-mixin box', '@mixin box'])
    await fs.expectFileToContain(
      file,
      '.box{background-color:red;width:8rem;height:8rem;background-color: green}',
    )
  },
  {
    debug: true,
  },
)

Description

Originally reported in #14973 (comment) but after digging I've narrowed it down to this.

There are a few things I want to say about this issue so please bear with me. Given this CSS:

@import 'tailwindcss/utilities';

@define-mixin test {
  background-color: green;
}

.test {
  width: 8rem;
  height: 8rem;
  background-color: red;

  @mixin test;
}

Assuming postcss-mixins is registered appropriately in config. Everything is fine in dev mode. But for production build, this is the output:

/*! tailwindcss v4.0.0-beta.1 | MIT License | https://tailwindcss.com */
.test {
  background-color:red;
  width:8rem;
  height:8rem
}
background-color: green; {}

After tracing through Tailwind codebase, I believe it is likely because of this code...

async renderStart() {
for (let [id, root] of roots.entries()) {
// Do not do a second render pass on Svelte `<style>` tags.
if (isSvelteStyle(id)) continue
let generated = await regenerateOptimizedCss(
root,
// During the renderStart phase, we can not add watch files since
// those would not be causing a refresh of the right CSS file. This
// should not be an issue since we did already process the CSS file
// before and the dependencies should not be changed (only the
// candidate list might have)
() => {},
)
if (!generated) {
roots.delete(id)
continue
}
// These plugins have side effects which, during build, results in CSS
// being written to the output dir. We need to run them here to ensure
// the CSS is written before the bundle is generated.
await transformWithPlugins(this, id, generated)
}
},

...and the fact that LightningCSS is run against the source code before postcss is. The renderStart hook above is only applied for build phase, hence the misaligned behavior compared to dev mode.

As of this writing LightningCSS will extract out any unknown at rule nested inside a rule. Try the code above in LightningCSS playground and notice position of @mixin vite.

@RobinMalfait mentioned similar issue in LightningCSS repo at parcel-bundler/lightningcss#353. It is true these at-rules are not standard CSS but given their usefulness I suspect we will see more and more people observing similar issues once they try to migrate to V4, especially because V3 was a postcss plugin itself.

To me, it'd be ideal if we could run LightningCSS after Postcss (renderStart after vite:css?) in @tailwindcss/vite, which will better match with behavior during dev mode and do not require changes from LightningCSS side.

I did also try setting up custom transform for LightningCSS but so far no straightforward (or even possible) one to work around this issue.

Thanks all.

@vnphanquang vnphanquang changed the title [v4] Nested unknown at-Rule (for PostCSS plugins) are ejected from rule during build [v4] Nested unknown at-rule (for PostCSS plugins) are ejected from rule during build Nov 26, 2024
@vnphanquang vnphanquang changed the title [v4] Nested unknown at-rule (for PostCSS plugins) are ejected from rule during build [v4] Nested unknown at-rule (for PostCSS plugins) is ejected from rule during build Nov 26, 2024
@Robert344Humphries
Copy link

Robert344Humphries commented Nov 26, 2024

Hello!
I faced similar kind of issue last time, I am still searching for some proper solution.

Best Regards,
Club Pilates Certified Instructor

@vnphanquang
Copy link
Author

vnphanquang commented Nov 26, 2024

@Robert344Humphries you can try using tailwind v4 as a postcss plugin instead of vite plugin. If your setup is not too complex, that may be a viable workaround at the moment. Also if possible can you include some more info about the at-rule / postcss plugin you are having problem with here? That might be helpful for further debugging. Thanks

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

No branches or pull requests

2 participants