From 8e3cdc353152cb842509d6aa6ccc6434cb8077bd Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Mon, 2 Dec 2024 15:58:48 -0500 Subject: [PATCH 0001/1023] Fix `style` config resolution during SSR --- packages/@tailwindcss-vite/src/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 9c92699fa..26acfdb6a 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -42,15 +42,31 @@ export default function tailwindcss(): Plugin[] { conditions: ['style', 'development|production'], tryIndex: false, preferRelative: true, + + // If the `ssr.resolve.conditions` config option is set it overrides the + // `conditions` option above when building for SSR. As a result, we may + // resolve to JS files instead of CSS files. To work around this we use + // `ssrConfig` to ensure correct behavior during SSR. + // + // This workaround is for Vite 5.x. Vite 6.x works correctly without + // this setting but keeping it around does not break anything. + ssrConfig: { + resolve: { + conditions: ['style', 'development|production'], + }, + }, }) + function customCssResolver(id: string, base: string) { return cssResolver(id, base, false, isSSR) } let jsResolver = config!.createResolver(config!.resolve) + function customJsResolver(id: string, base: string) { return jsResolver(id, base, true, isSSR) } + return new Root( id, () => moduleGraphCandidates, From 93b922dcb6e40ef78d893a002ee35b93521caa34 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Mon, 2 Dec 2024 23:08:13 +0100 Subject: [PATCH 0002/1023] Fix resolution of imported CSS files in Vite SSR builds (#15279) Fixes #15237 --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 1 + integrations/vite/ssr.test.ts | 139 ++++++++++++++++++++++++ packages/@tailwindcss-vite/src/index.ts | 2 +- 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 integrations/vite/ssr.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1c70ddf..1d460d1ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` - Fix issues with dev servers using Svelte 5 with the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274)) +- Fix resolution of imported CSS files in Vite SSR builds ([#15279](https://github.com/tailwindlabs/tailwindcss/issues/15279)) ### Added diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts new file mode 100644 index 000000000..e66970f88 --- /dev/null +++ b/integrations/vite/ssr.test.ts @@ -0,0 +1,139 @@ +import { expect } from 'vitest' +import { candidate, css, html, json, test, ts } from '../utils' + +test( + `Vite 5`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "_comment": "This test uses Vite 5.3 on purpose. Do not upgrade it to Vite 6.", + "devDependencies": { + "vite": "^5.3" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + ssr: { resolve: { conditions: [] } }, + }) + `, + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, + }, + }, + async ({ fs, exec }) => { + await exec('pnpm vite build --ssr server.ts') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + ]) + }, +) + +test( + `Vite 6`, + { + fs: { + 'package.json': json` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + "vite": "^6.0" + } + } + `, + 'vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + build: { + cssMinify: false, + ssrEmitAssets: true, + }, + plugins: [tailwindcss()], + ssr: { resolve: { conditions: [] } }, + }) + `, + 'index.html': html` + +
+ + + `, + 'src/index.css': css`@import 'tailwindcss';`, + 'src/index.ts': ts` + import './index.css' + + document.querySelector('#app').innerHTML = \` +
Hello, world!
+ \` + `, + 'server.ts': ts` + import css from './src/index.css?url' + + document.querySelector('#app').innerHTML = \` + +
Hello, world!
+ \` + `, + }, + }, + async ({ fs, exec }) => { + await exec('pnpm vite build --ssr server.ts') + + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + ]) + }, +) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 9c92699fa..a6ba5db3f 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -44,7 +44,7 @@ export default function tailwindcss(): Plugin[] { preferRelative: true, }) function customCssResolver(id: string, base: string) { - return cssResolver(id, base, false, isSSR) + return cssResolver(id, base, true, isSSR) } let jsResolver = config!.createResolver(config!.resolve) From 545401469dc342893e42dae66747e3008082457a Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 3 Dec 2024 10:28:51 +0100 Subject: [PATCH 0003/1023] Postcss: Run plugin in `Once` hook (#15273) Closes #15138 This PR changes the postcss client to run in the `Once` hook instead of `OnceExit`. This makes sure the postcss order in v4 matches that of v3. Conceptually this also makes more sense, since we expect tailwindcss to be run as one of the first plugins in the pipeline (where `OnceExit` would run it almost at the end). To make sure it's still possible to use `postcss-import` before and have it resolve to the right paths, we also needed to change the `postcss-fix-relative-paths` plugin to run in the `Once` order (`postcss-import` also uses `Once` order so the order). ## Test Plan This issue had many ways in which it can manifest. I added a unit test to ensure the plugin order works but here's a concrete example when using the postcss plugin in Vite. ### Before Image `url()`s were not properly handled since the postcss plugin to transform these was run before Tailwind CSS could generate the class for it: Screenshot 2024-12-02 at 14 55 42 ### After Screenshot 2024-12-02 at 14 53 52 --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 2 + .../@tailwindcss-postcss/src/index.test.ts | 60 ++++++++++++++++++- packages/@tailwindcss-postcss/src/index.ts | 2 +- .../src/postcss-fix-relative-paths/index.ts | 6 +- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d460d1ac..aace62b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` - Fix issues with dev servers using Svelte 5 with the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274)) - Fix resolution of imported CSS files in Vite SSR builds ([#15279](https://github.com/tailwindlabs/tailwindcss/issues/15279)) +- Ensure other plugins can run after `@tailwindcss/postcss` ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273)) +- Rebase `url()` inside imported CSS files when using Vite with the `@tailwindcss/postcss` extension ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273)) ### Added diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 02ab23bea..50f72a13d 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -2,7 +2,6 @@ import dedent from 'dedent' import { unlink, writeFile } from 'node:fs/promises' import postcss from 'postcss' import { afterEach, beforeEach, describe, expect, test } from 'vitest' -// @ts-ignore import tailwindcss from './index' // We give this file path to PostCSS for processing. @@ -248,3 +247,62 @@ test('bail early when Tailwind is not used', async () => { }" `) }) + +test('runs `Once` plugins in the right order', async () => { + let before = '' + let after = '' + let processor = postcss([ + { + postcssPlugin: 'before', + Once(root) { + before = root.toString() + }, + }, + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + { + postcssPlugin: 'after', + Once(root) { + after = root.toString() + }, + }, + ]) + + let result = await processor.process( + css` + @theme { + --color-red-500: red; + } + .custom-css { + color: theme(--color-red-500); + } + `, + { from: inputCssFilePath() }, + ) + + expect(result.css.trim()).toMatchInlineSnapshot(` + ":root { + --color-red-500: red; + } + + .custom-css { + color: red; + }" + `) + expect(before).toMatchInlineSnapshot(` + "@theme { + --color-red-500: red; + } + .custom-css { + color: theme(--color-red-500); + }" + `) + expect(after).toMatchInlineSnapshot(` + ":root { + --color-red-500: red; + } + + .custom-css { + color: red; + }" + `) +}) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 6720dfea0..a1759f9f4 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -55,7 +55,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { { postcssPlugin: 'tailwindcss', - async OnceExit(root, { result }) { + async Once(root, { result }) { env.DEBUG && console.time('[@tailwindcss/postcss] Total time in @tailwindcss/postcss') let inputFile = result.opts.from ?? '' let context = getContextFromCache(inputFile, opts) diff --git a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts index 2b88014a3..68dcb5551 100644 --- a/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts +++ b/packages/@tailwindcss-postcss/src/postcss-fix-relative-paths/index.ts @@ -67,10 +67,8 @@ export default function fixRelativePathsPlugin(): Plugin { return { postcssPlugin: 'tailwindcss-postcss-fix-relative-paths', - AtRule: { - source: fixRelativePath, - plugin: fixRelativePath, - config: fixRelativePath, + Once(root) { + root.walkAtRules(/source|plugin|config/, fixRelativePath) }, } } From 3e5745fbb28bb4cf8cc53c8618461bf136b67381 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 3 Dec 2024 13:48:52 +0100 Subject: [PATCH 0004/1023] Update changelog and add test for absolute url rebasing (#15282) I noticed uncommitted changes of #15275 on my local setup so here's the updated changelog and the added unit test. --- CHANGELOG.md | 2 +- packages/@tailwindcss-node/src/urls.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aace62b6e..a8087a60e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` +- Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` ([#15275](https://github.com/tailwindlabs/tailwindcss/pull/15275)) - Fix issues with dev servers using Svelte 5 with the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274)) - Fix resolution of imported CSS files in Vite SSR builds ([#15279](https://github.com/tailwindlabs/tailwindcss/issues/15279)) - Ensure other plugins can run after `@tailwindcss/postcss` ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273)) diff --git a/packages/@tailwindcss-node/src/urls.test.ts b/packages/@tailwindcss-node/src/urls.test.ts index d6d9fcb3a..3378e45ed 100644 --- a/packages/@tailwindcss-node/src/urls.test.ts +++ b/packages/@tailwindcss-node/src/urls.test.ts @@ -18,6 +18,12 @@ test('URLs can be rewritten', async () => { background: url('./image.jpg'); background: url("./image.jpg"); + /* Absolute URLs: ignored */ + background: url(/image.jpg); + background: url(/foo/image.jpg); + background: url('/image.jpg'); + background: url("/image.jpg"); + /* External URL: ignored */ background: url(http://example.com/image.jpg); background: url('http://example.com/image.jpg'); @@ -99,6 +105,10 @@ test('URLs can be rewritten', async () => { background: url(./foo/image.jpg); background: url('./foo/bar/image.jpg'); background: url("./foo/bar/image.jpg"); + background: url(/image.jpg); + background: url(/foo/image.jpg); + background: url('/image.jpg'); + background: url("/image.jpg"); background: url(http://example.com/image.jpg); background: url('http://example.com/image.jpg'); background: url("http://example.com/image.jpg"); From 78f5b087d4f7168f8a6b41ed56600ba7f60a612c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 3 Dec 2024 14:15:24 +0100 Subject: [PATCH 0005/1023] =?UTF-8?q?Add=20`@import=20"=E2=80=A6"=20refere?= =?UTF-8?q?nce`=20(#15228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #15219 This PR adds a new feature, `@import "…" reference` that can be used to load Tailwind CSS configuration files without adding any style rules to the CSS. The idea is that you can use this in combination with your Tailwind CSS root file when you need to have access to your full CSS config outside of the main stylesheet. A common example is for Vue, Svelte, or CSS modules: ```css @import "./tailwind.css" reference; .link { @apply underline; } ``` Importing a file as a reference will convert all `@theme` block to be `reference`, so no CSS variables will be emitted. Furthermore it will strip out all custom styles from the stylesheet. Furthermore plugins registered via `@plugin` or `@config` inside reference-mode files will not add any content to the CSS file via `addBase()`. ## Test Plan Added unit test for when we handle the import resolution and when `postcss-import` does it outside of Tailwind CSS. I also changed the Svelte and Vue integration tests to use this new syntax to ensure it works end to end. --------- Co-authored-by: Jordan Pittman --- CHANGELOG.md | 11 +-- integrations/vite/svelte.test.ts | 5 +- integrations/vite/vue.test.ts | 6 +- packages/tailwindcss/src/ast.ts | 12 +-- .../src/compat/apply-compat-hooks.ts | 41 ++++++--- .../src/compat/config/resolve-config.ts | 16 ++-- .../tailwindcss/src/compat/plugin-api.test.ts | 46 ++++++++++ packages/tailwindcss/src/compat/plugin-api.ts | 8 +- packages/tailwindcss/src/index.test.ts | 86 +++++++++++++++++++ packages/tailwindcss/src/index.ts | 35 +++++++- 10 files changed, 226 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8087a60e..af3d866f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270)) +- Support Vite 6 in the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274)) +- Add a new `@import "…" reference` syntax for only importing the Tailwind CSS configurations of a stylesheets ([#15228](https://github.com/tailwindlabs/tailwindcss/pull/15228)) + ### Fixed - Ensure absolute `url()`s inside imported CSS files are not rebased when using `@tailwindcss/vite` ([#15275](https://github.com/tailwindlabs/tailwindcss/pull/15275)) @@ -15,11 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure other plugins can run after `@tailwindcss/postcss` ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273)) - Rebase `url()` inside imported CSS files when using Vite with the `@tailwindcss/postcss` extension ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273)) -### Added - -- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270)) -- Support Vite 6 in the Vite plugin ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274)) - ## [4.0.0-beta.4] - 2024-11-29 ### Fixed diff --git a/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts index b0950f9f9..0f7953c63 100644 --- a/integrations/vite/svelte.test.ts +++ b/integrations/vite/svelte.test.ts @@ -48,10 +48,7 @@ test( target: document.body, }) `, - 'src/index.css': css` - @import 'tailwindcss/theme' theme(reference); - @import 'tailwindcss/utilities'; - `, + 'src/index.css': css`@import 'tailwindcss' reference;`, 'src/App.svelte': html` - - -
- - `, - 'src/component.ts': ts` - import { foo } from './component.module.css' - let root = document.getElementById('root') - root.className = foo - root.innerText = 'Hello, world!' - `, - 'src/component.module.css': css` - @import 'tailwindcss/utilities'; + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'index.html': html` + + + + +
+ + `, + 'src/component.ts': ts` + import { foo } from './component.module.css' + let root = document.getElementById('root') + root.className = foo + root.innerText = 'Hello, world!' + `, + 'src/component.module.css': css` + @import 'tailwindcss/utilities'; - .foo { - @apply underline; - } - `, - }, + .foo { + @apply underline; + } + `, }, - async ({ exec, fs }) => { - await exec(`pnpm vite build`) + }, + async ({ exec, fs, expect }) => { + await exec(`pnpm vite build`) - let files = await fs.glob('dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] + let files = await fs.glob('dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] - await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi]) - }, - ) - }) -} + await fs.expectFileToContain(filename, [/text-decoration-line: underline;/gi]) + }, + ) +}) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index 80a2a50c4..eebe11127 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import { describe, expect } from 'vitest' +import { describe } from 'vitest' import { candidate, css, @@ -14,42 +14,189 @@ import { yaml, } from '../utils' -for (let transformer of ['postcss', 'lightningcss']) { - describe(transformer, () => { - test( - `production build`, - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } +describe.each(['postcss', 'lightningcss'])('%s', (transformer) => { + test( + `production build`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`m-2`, + candidate`flex`, + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + 'dev mode', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/about.html': html` + + + + +
Tailwind Labs
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev', { + cwd: path.join(root, 'project-a'), + }) + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + // Candidates are resolved lazily, so the first visit of index.html + // will only have candidates from this file. + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/index.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).not.toContain(candidate`font-bold`) + }) + + // Going to about.html will extend the candidate list to include + // candidates from about.html. + await retryAssertion(async () => { + let styles = await fetchStyles(url, '/about.html') + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + }) + + await retryAssertion(async () => { + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` @@ -57,411 +204,175 @@ for (let transformer of ['postcss', 'lightningcss']) {
Hello, world!
`, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss/theme' theme(reference); - @import 'tailwindcss/utilities'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" + ) + + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`m-2`) + }) + + await retryAssertion(async () => { + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" module.exports = { className } `, - }, - }, - async ({ root, fs, exec }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + ) - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [filename] = files[0] + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`font-bold`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) - await fs.expectFileToContain(filename, [ - candidate`underline`, - candidate`m-2`, - candidate`flex`, - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - `dev mode`, - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - 'project-a/about.html': html` - - - - -
Tailwind Labs
- - `, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss/theme' theme(reference); - @import 'tailwindcss/utilities'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, spawn, getFreePort, fs }) => { - let port = await getFreePort() - await spawn(`pnpm vite dev --port ${port}`, { - cwd: path.join(root, 'project-a'), - }) - - // Candidates are resolved lazily, so the first visit of index.html - // will only have candidates from this file. - await retryAssertion(async () => { - let styles = await fetchStyles(port, '/index.html') - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).not.toContain(candidate`font-bold`) - }) - - // Going to about.html will extend the candidate list to include - // candidates from about.html. - await retryAssertion(async () => { - let styles = await fetchStyles(port, '/about.html') - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`font-bold`) - }) - - await retryAssertion(async () => { - // Updates are additive and cause new candidates to be added. - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- - `, - ) - - let styles = await fetchStyles(port) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`m-2`) - }) - - await retryAssertion(async () => { - // Manually added `@source`s are watched and trigger a rebuild - await fs.write( - 'project-b/src/index.js', - js` - const className = "[.changed_&]:content-['project-b/src/index.js']" - module.exports = { className } - `, - ) - - let styles = await fetchStyles(port) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`font-bold`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - }) - - await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in - // the generated CSS - await fs.write( - 'project-a/src/index.css', - css` - ${await fs.read('project-a/src/index.css')} - - .red { - color: red; - } - `, - ) - - let styles = await fetchStyles(port) - expect(styles).toContain(candidate`red`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - expect(styles).toContain(candidate`font-bold`) - }) - }, - ) - - test( - 'watch mode', - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - `, - 'project-a/tailwind.config.js': js` - export default { - content: ['../project-b/src/**/*.js'], - } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss/theme' theme(reference); - @import 'tailwindcss/utilities'; - @import './custom-theme.css'; - @config '../tailwind.config.js'; - @source '../../project-b/src/**/*.html'; - `, - 'project-a/src/custom-theme.css': css` - /* Will be overwritten later */ - @theme { - --color-primary: black; - } - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, spawn, fs }) => { - await spawn(`pnpm vite build --watch`, { - cwd: path.join(root, 'project-a'), - }) - - let filename = '' - await retryAssertion(async () => { - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - filename = files[0][0] - }) - - await fs.expectFileToContain(filename, [ - candidate`underline`, - candidate`flex`, + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', css` - .text-primary { - color: var(--color-primary); + ${await fs.read('project-a/src/index.css')} + + .red { + color: red; } `, - ]) + ) - await retryAssertion(async () => { - await fs.write( - 'project-a/src/custom-theme.css', - css` - /* Overriding the primary color */ - @theme { - --color-primary: red; - } - `, - ) + let styles = await fetchStyles(url) + expect(styles).toContain(candidate`red`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`font-bold`) + }) + }, + ) - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - - expect(styles).toContain(css` - .text-primary { - color: var(--color-primary); + test( + 'watch mode', + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" } - `) - }) + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' - await retryAssertion(async () => { - // Updates are additive and cause new candidates to be added. - await fs.write( - 'project-a/index.html', - html` - - - - -
Hello, world!
- - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - }) - - await retryAssertion(async () => { - // Manually added `@source`s are watched and trigger a rebuild - await fs.write( - 'project-b/src/index.js', - js` - const className = "[.changed_&]:content-['project-b/src/index.js']" - module.exports = { className } - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - }) - - await retryAssertion(async () => { - // After updates to the CSS file, all previous candidates should still be in - // the generated CSS - await fs.write( - 'project-a/src/index.css', - css` - ${await fs.read('project-a/src/index.css')} - - .red { - color: red; - } - `, - ) - - let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(1) - let [, styles] = files[0] - expect(styles).toContain(candidate`underline`) - expect(styles).toContain(candidate`flex`) - expect(styles).toContain(candidate`m-2`) - expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) - expect(styles).toContain(candidate`red`) - }) + export default defineConfig({ + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/tailwind.config.js': js` + export default { + content: ['../project-b/src/**/*.js'], + } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss/theme' theme(reference); + @import 'tailwindcss/utilities'; + @import './custom-theme.css'; + @config '../tailwind.config.js'; + @source '../../project-b/src/**/*.html'; + `, + 'project-a/src/custom-theme.css': css` + /* Will be overwritten later */ + @theme { + --color-primary: black; + } + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, }, - ) + }, + async ({ root, spawn, fs, expect }) => { + let process = await spawn('pnpm vite build --watch', { + cwd: path.join(root, 'project-a'), + }) + await process.onStdout((m) => m.includes('built in')) - test( - `source(none) disables looking at the module graph`, - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } + let filename = '' + await retryAssertion(async () => { + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + filename = files[0][0] + }) + + await fs.expectFileToContain(filename, [ + candidate`underline`, + candidate`flex`, + css` + .text-primary { + color: var(--color-primary); + } + `, + ]) + + await retryAssertion(async () => { + await fs.write( + 'project-a/src/custom-theme.css', + css` + /* Overriding the primary color */ + @theme { + --color-primary: red; } `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' + ) - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [, styles] = files[0] + + expect(styles).toContain(css` + .text-primary { + color: var(--color-primary); + } + `) + }) + + await retryAssertion(async () => { + // Updates are additive and cause new candidates to be added. + await fs.write( + 'project-a/index.html', + html` @@ -469,225 +380,320 @@ for (let transformer of ['postcss', 'lightningcss']) {
Hello, world!
`, - 'project-a/src/index.css': css` - @import 'tailwindcss' source(none); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, fs, exec }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + ) let files = await fs.glob('project-a/dist/**/*.css') expect(files).toHaveLength(1) - let [filename] = files[0] + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + }) - // `underline` and `m-2` are only present from files in the module graph - // which we've explicitly disabled with source(none) so they should not - // be present - await fs.expectFileNotToContain(filename, [ - // - candidate`underline`, - candidate`m-2`, - ]) - - // The files from `project-b` should be included because there is an - // explicit `@source` directive for it - await fs.expectFileToContain(filename, [ - // - candidate`flex`, - ]) - - // The explicit source directive only covers HTML files, so the JS file - // should not be included - await fs.expectFileNotToContain(filename, [ - // - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - `source("…") filters the module graph`, - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } - } - `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - - `, - 'project-a/app/index.js': js` - const className = "content-['project-a/app/index.js']" - export default { className } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss' source('../app'); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" + await retryAssertion(async () => { + // Manually added `@source`s are watched and trigger a rebuild + await fs.write( + 'project-b/src/index.js', + js` + const className = "[.changed_&]:content-['project-b/src/index.js']" module.exports = { className } `, - }, - }, - async ({ root, fs, exec }) => { - await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + ) let files = await fs.glob('project-a/dist/**/*.css') expect(files).toHaveLength(1) - let [filename] = files[0] + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + }) - // `underline` and `m-2` are present in files in the module graph but - // we've filtered the module graph such that we only look in - // `./app/**/*` so they should not be present - await fs.expectFileNotToContain(filename, [ - // - candidate`underline`, - candidate`m-2`, - candidate`content-['project-a/index.html']`, - ]) + await retryAssertion(async () => { + // After updates to the CSS file, all previous candidates should still be in + // the generated CSS + await fs.write( + 'project-a/src/index.css', + css` + ${await fs.read('project-a/src/index.css')} - // We've filtered the module graph to only look in ./app/**/* so the - // candidates from that project should be present - await fs.expectFileToContain(filename, [ - // - candidate`content-['project-a/app/index.js']`, - ]) - - // Even through we're filtering the module graph explicit sources are - // additive and as such files from `project-b` should be included - // because there is an explicit `@source` directive for it - await fs.expectFileToContain(filename, [ - // - candidate`content-['project-b/src/index.html']`, - ]) - - // The explicit source directive only covers HTML files, so the JS file - // should not be included - await fs.expectFileNotToContain(filename, [ - // - candidate`content-['project-b/src/index.js']`, - ]) - }, - ) - - test( - `source("…") must be a directory`, - { - fs: { - 'package.json': json`{}`, - 'pnpm-workspace.yaml': yaml` - # - packages: - - project-a - `, - 'project-a/package.json': txt` - { - "type": "module", - "dependencies": { - "@tailwindcss/vite": "workspace:^", - "tailwindcss": "workspace:^" - }, - "devDependencies": { - ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} - "vite": "^6" - } + .red { + color: red; } `, - 'project-a/vite.config.ts': ts` - import tailwindcss from '@tailwindcss/vite' - import { defineConfig } from 'vite' - - export default defineConfig({ - css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, - build: { cssMinify: false }, - plugins: [tailwindcss()], - }) - `, - 'project-a/index.html': html` - - - - -
Hello, world!
- - - `, - 'project-a/app/index.js': js` - const className = "content-['project-a/app/index.js']" - export default { className } - `, - 'project-a/src/index.css': css` - @import 'tailwindcss' source('../i-do-not-exist'); - @source '../../project-b/src/**/*.html'; - `, - 'project-b/src/index.html': html` -
- `, - 'project-b/src/index.js': js` - const className = "content-['project-b/src/index.js']" - module.exports = { className } - `, - }, - }, - async ({ root, fs, exec }) => { - await expect(() => - exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }), - ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist') + ) let files = await fs.glob('project-a/dist/**/*.css') - expect(files).toHaveLength(0) + expect(files).toHaveLength(1) + let [, styles] = files[0] + expect(styles).toContain(candidate`underline`) + expect(styles).toContain(candidate`flex`) + expect(styles).toContain(candidate`m-2`) + expect(styles).toContain(candidate`[.changed_&]:content-['project-b/src/index.js']`) + expect(styles).toContain(candidate`red`) + }) + }, + ) + + test( + `source(none) disables looking at the module graph`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source(none); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, }, - ) - }) -} + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // `underline` and `m-2` are only present from files in the module graph + // which we've explicitly disabled with source(none) so they should not + // be present + await fs.expectFileNotToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + ]) + + // The files from `project-b` should be included because there is an + // explicit `@source` directive for it + await fs.expectFileToContain(filename, [ + // + candidate`flex`, + ]) + + // The explicit source directive only covers HTML files, so the JS file + // should not be included + await fs.expectFileNotToContain(filename, [ + // + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + `source("…") filters the module graph`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + + `, + 'project-a/app/index.js': js` + const className = "content-['project-a/app/index.js']" + export default { className } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source('../app'); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await exec('pnpm vite build', { cwd: path.join(root, 'project-a') }) + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + // `underline` and `m-2` are present in files in the module graph but + // we've filtered the module graph such that we only look in + // `./app/**/*` so they should not be present + await fs.expectFileNotToContain(filename, [ + // + candidate`underline`, + candidate`m-2`, + candidate`content-['project-a/index.html']`, + ]) + + // We've filtered the module graph to only look in ./app/**/* so the + // candidates from that project should be present + await fs.expectFileToContain(filename, [ + // + candidate`content-['project-a/app/index.js']`, + ]) + + // Even through we're filtering the module graph explicit sources are + // additive and as such files from `project-b` should be included + // because there is an explicit `@source` directive for it + await fs.expectFileToContain(filename, [ + // + candidate`content-['project-b/src/index.html']`, + ]) + + // The explicit source directive only covers HTML files, so the JS file + // should not be included + await fs.expectFileNotToContain(filename, [ + // + candidate`content-['project-b/src/index.js']`, + ]) + }, + ) + + test( + `source("…") must be a directory`, + { + fs: { + 'package.json': json`{}`, + 'pnpm-workspace.yaml': yaml` + # + packages: + - project-a + `, + 'project-a/package.json': txt` + { + "type": "module", + "dependencies": { + "@tailwindcss/vite": "workspace:^", + "tailwindcss": "workspace:^" + }, + "devDependencies": { + ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''} + "vite": "^6" + } + } + `, + 'project-a/vite.config.ts': ts` + import tailwindcss from '@tailwindcss/vite' + import { defineConfig } from 'vite' + + export default defineConfig({ + css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"}, + build: { cssMinify: false }, + plugins: [tailwindcss()], + }) + `, + 'project-a/index.html': html` + + + + +
Hello, world!
+ + + `, + 'project-a/app/index.js': js` + const className = "content-['project-a/app/index.js']" + export default { className } + `, + 'project-a/src/index.css': css` + @import 'tailwindcss' source('../i-do-not-exist'); + @source '../../project-b/src/**/*.html'; + `, + 'project-b/src/index.html': html` +
+ `, + 'project-b/src/index.js': js` + const className = "content-['project-b/src/index.js']" + module.exports = { className } + `, + }, + }, + async ({ root, fs, exec, expect }) => { + await expect(() => + exec('pnpm vite build', { cwd: path.join(root, 'project-a') }, { ignoreStdErr: true }), + ).rejects.toThrowError('The `source(../i-do-not-exist)` does not exist') + + let files = await fs.glob('project-a/dist/**/*.css') + expect(files).toHaveLength(0) + }, + ) +}) test( `demote Tailwind roots to regular CSS files and back to Tailwind roots while restoring all candidates`, @@ -733,14 +739,21 @@ test( 'src/index.css': css`@import 'tailwindcss';`, }, }, - async ({ spawn, getFreePort, fs }) => { - let port = await getFreePort() - await spawn(`pnpm vite dev --port ${port}`) + async ({ spawn, fs, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) // Candidates are resolved lazily, so the first visit of index.html // will only have candidates from this file. await retryAssertion(async () => { - let styles = await fetchStyles(port, '/index.html') + let styles = await fetchStyles(url, '/index.html') expect(styles).toContain(candidate`underline`) expect(styles).not.toContain(candidate`font-bold`) }) @@ -748,7 +761,7 @@ test( // Going to about.html will extend the candidate list to include // candidates from about.html. await retryAssertion(async () => { - let styles = await fetchStyles(port, '/about.html') + let styles = await fetchStyles(url, '/about.html') expect(styles).toContain(candidate`underline`) expect(styles).toContain(candidate`font-bold`) }) @@ -757,7 +770,7 @@ test( // We change the CSS file so it is no longer a valid Tailwind root. await fs.write('src/index.css', css`@import 'tailwindcss';`) - let styles = await fetchStyles(port) + let styles = await fetchStyles(url) expect(styles).toContain(candidate`underline`) expect(styles).toContain(candidate`font-bold`) }) @@ -801,18 +814,25 @@ test( 'src/index.css': css`@import 'tailwindcss';`, }, }, - async ({ spawn, getFreePort }) => { - let port = await getFreePort() - await spawn(`pnpm vite dev --port ${port}`) + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let baseUrl = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) baseUrl = match[1] + return Boolean(baseUrl) + }) await retryAssertion(async () => { // We have to load the .js file first so that the static assets are // resolved - await fetch(`http://localhost:${port}/src/index.js`).then((r) => r.text()) + await fetch(`${baseUrl}/src/index.js`).then((r) => r.text()) let [raw, url] = await Promise.all([ - fetch(`http://localhost:${port}/src/index.css?raw`).then((r) => r.text()), - fetch(`http://localhost:${port}/src/index.css?url`).then((r) => r.text()), + fetch(`${baseUrl}/src/index.css?raw`).then((r) => r.text()), + fetch(`${baseUrl}/src/index.css?url`).then((r) => r.text()), ]) expect(firstLine(raw)).toBe(`export default "@import 'tailwindcss';"`) diff --git a/integrations/vite/multi-root.test.ts b/integrations/vite/multi-root.test.ts index d2d59f50e..39da43690 100644 --- a/integrations/vite/multi-root.test.ts +++ b/integrations/vite/multi-root.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'vitest' import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' test( @@ -65,7 +64,7 @@ test( `, }, }, - async ({ fs, exec }) => { + async ({ fs, exec, expect }) => { await exec('pnpm vite build') let files = await fs.glob('dist/**/*.css') @@ -86,7 +85,7 @@ test( ) test( - `dev mode`, + 'dev mode', { fs: { 'package.json': json` @@ -141,14 +140,21 @@ test( `, }, }, - async ({ root, spawn, getFreePort, fs }) => { - let port = await getFreePort() - await spawn(`pnpm vite dev --port ${port}`) + async ({ spawn, expect }) => { + let process = await spawn('pnpm vite dev') + await process.onStdout((m) => m.includes('ready in')) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) // Candidates are resolved lazily, so the first visit of index.html // will only have candidates from this file. await retryAssertion(async () => { - let styles = await fetchStyles(port, '/root1.html') + let styles = await fetchStyles(url, '/root1.html') expect(styles).toContain(candidate`one:underline`) expect(styles).not.toContain(candidate`two:underline`) }) @@ -156,7 +162,7 @@ test( // Going to about.html will extend the candidate list to include // candidates from about.html. await retryAssertion(async () => { - let styles = await fetchStyles(port, '/root2.html') + let styles = await fetchStyles(url, '/root2.html') expect(styles).not.toContain(candidate`one:underline`) expect(styles).toContain(candidate`two:underline`) }) diff --git a/integrations/vite/nuxt.test.ts b/integrations/vite/nuxt.test.ts index cfd521e48..1b834525c 100644 --- a/integrations/vite/nuxt.test.ts +++ b/integrations/vite/nuxt.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'vitest' import { candidate, css, fetchStyles, html, json, retryAssertion, test, ts } from '../utils' const SETUP = { @@ -37,12 +36,25 @@ const SETUP = { }, } -test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => { - let port = await getFreePort() - await spawn(`pnpm nuxt dev --port ${port}`) +test('dev mode', SETUP, async ({ fs, spawn, expect }) => { + let process = await spawn('pnpm nuxt dev', { + env: { + TEST: 'false', // VERY IMPORTANT OTHERWISE YOU WON'T GET OUTPUT + NODE_ENV: 'development', + }, + }) + + let url = '' + await process.onStdout((m) => { + let match = /Local:\s*(http.*)\//.exec(m) + if (match) url = match[1] + return Boolean(url) + }) + + await process.onStdout((m) => m.includes('server warmed up in')) await retryAssertion(async () => { - let css = await fetchStyles(port) + let css = await fetchStyles(url) expect(css).toContain(candidate`underline`) }) @@ -50,29 +62,36 @@ test('dev mode', SETUP, async ({ fs, spawn, getFreePort }) => { await fs.write( 'app.vue', html` -