diff --git a/packages/uniwind/src/bundler/artifacts/css/themes.ts b/packages/uniwind/src/bundler/artifacts/css/themes.ts index da0ef3e2..5003b50d 100644 --- a/packages/uniwind/src/bundler/artifacts/css/themes.ts +++ b/packages/uniwind/src/bundler/artifacts/css/themes.ts @@ -20,6 +20,14 @@ const isExcludedDependency = (url: string) => url.includes('node_modules/uniwind'), ].some(Boolean) +// Strips Tailwind's `source()`, `prefix()` and `theme()` @import options, which Lightning CSS can't parse +const QUOTED_OR_COMMENT = `/\\*[\\s\\S]*?\\*/|"[^"]*"|'[^']*'` +const importRuleRegex = new RegExp(`@import(?:${QUOTED_OR_COMMENT}|[^;"'])+;`, 'g') +const importOptionRegex = new RegExp(`(${QUOTED_OR_COMMENT})|\\s+(?:source|prefix|theme)\\((?:${QUOTED_OR_COMMENT}|[^)"'])*\\)`, 'g') + +const stripTailwindImportOptions = (css: string) => + css.replace(importRuleRegex, rule => rule.replace(importOptionRegex, (_, kept?: string) => kept ?? '')) + export const generateCSSForThemes = async (themes: Array, input: string) => { // css generation const themesVariables = Object.fromEntries(themes.map(theme => [theme, new Set()])) @@ -32,7 +40,7 @@ export const generateCSSForThemes = async (themes: Array, input: string) } const { dependencies } = transform({ - code: Buffer.from(css), + code: Buffer.from(stripTailwindImportOptions(css)), filename: 'uniwind.css', analyzeDependencies: true, visitor: { diff --git a/packages/uniwind/tests/web/bundler/themes-import-options.test.ts b/packages/uniwind/tests/web/bundler/themes-import-options.test.ts new file mode 100644 index 00000000..d9019b9f --- /dev/null +++ b/packages/uniwind/tests/web/bundler/themes-import-options.test.ts @@ -0,0 +1,63 @@ +import fs from 'fs' +import path from 'path' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import { generateCSSForThemes } from '../../../src/bundler/artifacts/css/themes' + +describe('Tailwind import options', () => { + let dir: string + + beforeEach(() => { + dir = fs.mkdtempSync(path.join(__dirname, '.tmp-themes-')) + }) + + afterEach(() => { + fs.rmSync(dir, { recursive: true, force: true }) + }) + + const generate = async (globalCSS: Array, themeFile = 'theme.css') => { + fs.writeFileSync( + path.join(dir, themeFile), + [ + '@variant light { --color-background: white; }', + '@variant dark { --color-background: black; }', + ].join('\n'), + ) + fs.writeFileSync(path.join(dir, 'global.css'), globalCSS.join('\n')) + + return generateCSSForThemes(['light', 'dark'], path.join(dir, 'global.css')) + } + + test.each([ + `source('./')`, + `source("./src/(app)")`, + `source('./src/(app);')`, + 'source(none)', + 'prefix(tw)', + 'theme(static)', + ])('@import with %s still collects theme variables', async option => { + const css = await generate([ + `@import 'tailwindcss' ${option};`, + `@import './theme.css' ${option};`, + ]) + + expect(css).toContain('--color-background: unset;') + }) + + test('quotes inside comments do not break option stripping', async () => { + const css = await generate([ + `@import 'tailwindcss' /* don't */ source('./');`, + `@import './theme.css';`, + ]) + + expect(css).toContain('--color-background: unset;') + }) + + test('option-like text inside the import path is kept', async () => { + const css = await generate([ + `@import 'tailwindcss' source('./');`, + `@import './theme source(none).css';`, + ], 'theme source(none).css') + + expect(css).toContain('--color-background: unset;') + }) +})