diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d28a1e1e0660..54512c071671 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,19 +14,32 @@ jobs:
fail-fast: false
matrix:
node-version: [20]
- runner: [namespace-profile-default, windows-latest, macos-14]
+ runner:
+ - name: Windows
+ os: windows-latest
+
+ - name: Linux
+ os: namespace-profile-default
+
+ - name: macOS
+ os: macos-14
+
# Exclude windows and macos from being built on feature branches
on-next-branch:
- ${{ github.ref == 'refs/heads/next' }}
exclude:
- on-next-branch: false
- runner: windows-latest
+ runner:
+ name: Windows
- on-next-branch: false
- runner: macos-14
+ runner:
+ name: macOS
- runs-on: ${{ matrix.runner }}
+ runs-on: ${{ matrix.runner.os }}
timeout-minutes: 30
+ name: ${{ matrix.runner.name }}
+
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@@ -72,16 +85,11 @@ jobs:
- name: Lint
run: pnpm run lint
# Only lint on linux to avoid \r\n line ending errors
- if: matrix.runner == 'ubuntu-latest'
+ if: matrix.runner.os == 'ubuntu-latest'
- name: Test
run: pnpm run test
- - name: Integration Tests
- run: pnpm run test:integrations
- env:
- GITHUB_WORKSPACE: ${{ github.workspace }}
-
- name: Install Playwright Browsers
run: npx playwright install --with-deps
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
new file mode 100644
index 000000000000..d61223ae72b6
--- /dev/null
+++ b/.github/workflows/integration-tests.yml
@@ -0,0 +1,102 @@
+name: Integration Tests
+
+on:
+ push:
+ branches: [next]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ tests:
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: [20]
+
+ runner:
+ - name: Windows
+ os: windows-latest
+
+ - name: Linux
+ os: namespace-profile-default
+
+ - name: macOS
+ os: macos-14
+
+ integration:
+ - upgrade
+ - vite
+ - cli
+ - postcss
+
+ # Exclude windows and macos from being built on feature branches
+ on-next-branch:
+ - ${{ github.ref == 'refs/heads/next' }}
+ exclude:
+ - on-next-branch: false
+ runner:
+ name: Windows
+ - on-next-branch: false
+ runner:
+ name: macOS
+
+ runs-on: ${{ matrix.runner.os }}
+ timeout-minutes: 30
+
+ name: ${{ matrix.runner.name }} / ${{ matrix.integration }}
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'pnpm'
+
+ # Cargo already skips downloading dependencies if they already exist
+ - name: Cache cargo
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ # Cache the `oxide` Rust build
+ - name: Cache oxide build
+ uses: actions/cache@v4
+ with:
+ path: |
+ ./target/
+ ./crates/node/*.node
+ ./crates/node/index.js
+ ./crates/node/index.d.ts
+ key: ${{ runner.os }}-oxide-${{ hashFiles('./crates/**/*') }}
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Build
+ run: pnpm run build
+ env:
+ CARGO_PROFILE_RELEASE_LTO: 'off'
+ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: 'lld-link'
+
+ - name: Test ${{ matrix.integration }}
+ run: pnpm run test:integrations ./integrations/${{ matrix.integration }}
+ env:
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+
+ - name: Notify Discord
+ if: failure() && github.ref == 'refs/heads/next'
+ uses: discord-actions/message@v2
+ with:
+ webhookUrl: ${{ secrets.DISCORD_WEBHOOK_URL }}
+ message: 'The [most recent build](<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>) on the `next` branch has failed.'
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f58660ae16bf..ed22d9b9e2f9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -248,6 +248,13 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ - name: Alias packages to `latest`
+ if: ${{ inputs.release_channel == 'next' }}
+ run: |
+ npm dist-tag add @tailwindcss/upgrade@${{ env.TAG_NAME }} latest
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
- name: Release
uses: softprops/action-gh-release@v2
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 14cc77dbfb59..235f3bd61452 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-- Nothing yet!
+### Fixed
+
+- Use the correct property value for `place-content-between`, `place-content-around`, and `place-content-evenly` utilities ([#15440](https://github.com/tailwindlabs/tailwindcss/pull/15440))
+- Don’t detect arbitrary properties when preceded by an escape ([#15456](https://github.com/tailwindlabs/tailwindcss/pull/15456))
+- Fix incorrectly named `bg-round` and `bg-space` utilities to `bg-repeat-round` to `bg-repeat-space` ([#15462](https://github.com/tailwindlabs/tailwindcss/pull/15462))
+
+### Changed
+
+- Removed `--container-prose` in favor of a deprecated `--max-width-prose` theme variable so that `*-prose` is only available for max-width utilities and only for backward compatibility ([#15439](https://github.com/tailwindlabs/tailwindcss/pull/15439))
+
+## [4.0.0-beta.8] - 2024-12-17
+
+### Fixed
+
+- Ensure `Symbol.dispose` and `Symbol.asyncDispose` are polyfilled ([#15404](https://github.com/tailwindlabs/tailwindcss/pull/15404))
+
+## [4.0.0-beta.7] - 2024-12-13
+
+### Added
+
+- Export `tailwindcss/lib/util/flattenColorPalette` for backward compatibility ([#15318](https://github.com/tailwindlabs/tailwindcss/pull/15318))
+- Improve debug logs to get better insights ([#15303](https://github.com/tailwindlabs/tailwindcss/pull/15303))
+
+### Fixed
+
+- Fix dependency related warnings when using `@tailwindcss/postcss` on Windows ([#15321](https://github.com/tailwindlabs/tailwindcss/pull/15321))
+- Skip creating a compiler for CSS files that should not be processed ([#15340](https://github.com/tailwindlabs/tailwindcss/pull/15340))
+- Fix missing `shadow-none` suggestion in IntelliSense ([#15342](https://github.com/tailwindlabs/tailwindcss/pull/15342))
+- Optimize AST before printing for IntelliSense ([#15347](https://github.com/tailwindlabs/tailwindcss/pull/15347))
+- Generate vendor prefixes for Chrome 111+ (e.g. `-webkit-background-clip: text`) ([#15389](https://github.com/tailwindlabs/tailwindcss/pull/15389))
+
+### Changed
+
+- Rename `--aspect-ratio-*` theme key to `--aspect-*` ([#15365](https://github.com/tailwindlabs/tailwindcss/pull/15365))
+- Derive `aspect-video` utility from theme ([#15365](https://github.com/tailwindlabs/tailwindcss/pull/15365))
+
+## [4.0.0-beta.6] - 2024-12-06
+
+### Fixed
+
+- Ensure `@import "…" reference` never generates utilities ([#15307](https://github.com/tailwindlabs/tailwindcss/pull/15307))
+
+## [4.0.0-beta.5] - 2024-12-04
+
+### Added
+
+- Parallelize parsing of individual source files ([#15270](https://github.com/tailwindlabs/tailwindcss/pull/15270))
+- Add new `@import "…" reference` option for importing Tailwind CSS configuration details into another CSS entry point without duplicating CSS ([#15228](https://github.com/tailwindlabs/tailwindcss/pull/15228))
+- Improve performance of `@tailwindcss/postcss` by translating between internal data structures and PostCSS nodes directly without additional parsing or stringification ([#15297](https://github.com/tailwindlabs/tailwindcss/pull/15297))
+
+### Fixed
+
+- Ensure absolute URLs 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 `@tailwindcss/vite` ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274))
+- Support installing `@tailwindcss/vite` in Vite 6 projects ([#15274](https://github.com/tailwindlabs/tailwindcss/issues/15274))
+- Fix resolution of imported CSS files in SSR builds with `@tailwindcss/vite` ([#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 URLs inside imported CSS files when using Vite with the `@tailwindcss/postcss` extension ([#15273](https://github.com/tailwindlabs/tailwindcss/pull/15273))
+- Fix missing font family suggestions in IntelliSense ([#15288](https://github.com/tailwindlabs/tailwindcss/pull/15288))
+- Fix missing `@container` suggestion in IntelliSense ([#15288](https://github.com/tailwindlabs/tailwindcss/pull/15288))
## [4.0.0-beta.4] - 2024-11-29
@@ -718,3 +777,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Move the CLI into a separate `@tailwindcss/cli` package ([#13095](https://github.com/tailwindlabs/tailwindcss/pull/13095))
## [4.0.0-alpha.1] - 2024-03-06
+
+- First 4.0.0-alpha.1 release
diff --git a/crates/node/npm/android-arm-eabi/package.json b/crates/node/npm/android-arm-eabi/package.json
index 7f6928ce7c89..e577726c5cca 100644
--- a/crates/node/npm/android-arm-eabi/package.json
+++ b/crates/node/npm/android-arm-eabi/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm-eabi",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/android-arm64/package.json b/crates/node/npm/android-arm64/package.json
index 619ec5796f0c..402b48c17c32 100644
--- a/crates/node/npm/android-arm64/package.json
+++ b/crates/node/npm/android-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-android-arm64",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/darwin-arm64/package.json b/crates/node/npm/darwin-arm64/package.json
index c1b4e77bfda6..a397cbc786dc 100644
--- a/crates/node/npm/darwin-arm64/package.json
+++ b/crates/node/npm/darwin-arm64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-arm64",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/darwin-x64/package.json b/crates/node/npm/darwin-x64/package.json
index 3547b028bca9..938a842ecb3d 100644
--- a/crates/node/npm/darwin-x64/package.json
+++ b/crates/node/npm/darwin-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-darwin-x64",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/freebsd-x64/package.json b/crates/node/npm/freebsd-x64/package.json
index 72b62b6ecd44..e831118c6aff 100644
--- a/crates/node/npm/freebsd-x64/package.json
+++ b/crates/node/npm/freebsd-x64/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-freebsd-x64",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm-gnueabihf/package.json b/crates/node/npm/linux-arm-gnueabihf/package.json
index 5a0b6927880e..c7c01238930b 100644
--- a/crates/node/npm/linux-arm-gnueabihf/package.json
+++ b/crates/node/npm/linux-arm-gnueabihf/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm-gnueabihf",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm64-gnu/package.json b/crates/node/npm/linux-arm64-gnu/package.json
index d56ca83d9db8..4f1d021717c4 100644
--- a/crates/node/npm/linux-arm64-gnu/package.json
+++ b/crates/node/npm/linux-arm64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-gnu",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-arm64-musl/package.json b/crates/node/npm/linux-arm64-musl/package.json
index f49d190b2050..920202246a4d 100644
--- a/crates/node/npm/linux-arm64-musl/package.json
+++ b/crates/node/npm/linux-arm64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-arm64-musl",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-x64-gnu/package.json b/crates/node/npm/linux-x64-gnu/package.json
index 4c90e4ea55e0..b5358d57a695 100644
--- a/crates/node/npm/linux-x64-gnu/package.json
+++ b/crates/node/npm/linux-x64-gnu/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-gnu",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/linux-x64-musl/package.json b/crates/node/npm/linux-x64-musl/package.json
index 05959c777bbf..6198fcd716bd 100644
--- a/crates/node/npm/linux-x64-musl/package.json
+++ b/crates/node/npm/linux-x64-musl/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-linux-x64-musl",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/win32-arm64-msvc/package.json b/crates/node/npm/win32-arm64-msvc/package.json
index 60942baac3fe..f751383aab4c 100644
--- a/crates/node/npm/win32-arm64-msvc/package.json
+++ b/crates/node/npm/win32-arm64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-arm64-msvc",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/npm/win32-x64-msvc/package.json b/crates/node/npm/win32-x64-msvc/package.json
index 9ff9c2f86053..dddeab19b276 100644
--- a/crates/node/npm/win32-x64-msvc/package.json
+++ b/crates/node/npm/win32-x64-msvc/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide-win32-x64-msvc",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/node/package.json b/crates/node/package.json
index 8266d94188b6..513a9a7a82e2 100644
--- a/crates/node/package.json
+++ b/crates/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@tailwindcss/oxide",
- "version": "4.0.0-beta.4",
+ "version": "4.0.0-beta.8",
"repository": {
"type": "git",
"url": "git+https://github.com/tailwindlabs/tailwindcss.git",
diff --git a/crates/oxide/src/lib.rs b/crates/oxide/src/lib.rs
index a9567c5c4019..4631cabc843c 100644
--- a/crates/oxide/src/lib.rs
+++ b/crates/oxide/src/lib.rs
@@ -24,7 +24,7 @@ pub mod paths;
pub mod scanner;
static SHOULD_TRACE: sync::LazyLock = sync::LazyLock::new(
- || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || value.eq("1") || value.eq("true") || value.contains("tailwind")),
+ || matches!(std::env::var("DEBUG"), Ok(value) if value.eq("*") || (value.contains("tailwindcss:oxide") && !value.contains("-tailwindcss:oxide"))),
);
fn init_tracing() {
@@ -102,13 +102,11 @@ impl Scanner {
pub fn scan(&mut self) -> Vec {
init_tracing();
self.prepare();
- self.check_for_new_files();
self.compute_candidates();
- let mut candidates: Vec = self.candidates.clone().into_iter().collect();
-
- candidates.sort();
+ let mut candidates: Vec = self.candidates.clone().into_par_iter().collect();
+ candidates.par_sort();
candidates
}
@@ -140,7 +138,7 @@ impl Scanner {
let extractor = Extractor::with_positions(&content[..], Default::default());
let candidates: Vec<(String, usize)> = extractor
- .into_iter()
+ .into_par_iter()
.map(|(s, i)| {
// SAFETY: When we parsed the candidates, we already guaranteed that the byte slices
// are valid, therefore we don't have to re-check here when we want to convert it back
@@ -156,7 +154,7 @@ impl Scanner {
self.prepare();
self.files
- .iter()
+ .par_iter()
.filter_map(|x| Path::from(x.clone()).canonicalize().ok())
.map(|x| x.to_string())
.collect()
@@ -173,11 +171,18 @@ impl Scanner {
fn compute_candidates(&mut self) {
let mut changed_content = vec![];
- for path in &self.files {
- let current_time = fs::metadata(path)
- .and_then(|m| m.modified())
- .unwrap_or(SystemTime::now());
+ let current_mtimes = self
+ .files
+ .par_iter()
+ .map(|path| {
+ fs::metadata(path)
+ .and_then(|m| m.modified())
+ .unwrap_or(SystemTime::now())
+ })
+ .collect::>();
+ for (idx, path) in self.files.iter().enumerate() {
+ let current_time = current_mtimes[idx];
let previous_time = self.mtimes.insert(path.clone(), current_time);
let should_scan_file = match previous_time {
@@ -201,7 +206,7 @@ impl Scanner {
if !changed_content.is_empty() {
let candidates = parse_all_blobs(read_all_files(changed_content));
- self.candidates.extend(candidates);
+ self.candidates.par_extend(candidates);
}
}
@@ -209,6 +214,7 @@ impl Scanner {
// content for candidates.
fn prepare(&mut self) {
if self.ready {
+ self.check_for_new_files();
return;
}
@@ -219,14 +225,21 @@ impl Scanner {
#[tracing::instrument(skip_all)]
fn check_for_new_files(&mut self) {
+ let current_mtimes = self
+ .dirs
+ .par_iter()
+ .map(|path| {
+ fs::metadata(path)
+ .and_then(|m| m.modified())
+ .unwrap_or(SystemTime::now())
+ })
+ .collect::>();
+
let mut modified_dirs: Vec = vec![];
// Check all directories to see if they were modified
- for path in &self.dirs {
- let current_time = fs::metadata(path)
- .and_then(|m| m.modified())
- .unwrap_or(SystemTime::now());
-
+ for (idx, path) in self.dirs.iter().enumerate() {
+ let current_time = current_mtimes[idx];
let previous_time = self.mtimes.insert(path.clone(), current_time);
let should_scan = match previous_time {
@@ -455,12 +468,10 @@ fn read_all_files(changed_content: Vec) -> Vec> {
#[tracing::instrument(skip_all)]
fn parse_all_blobs(blobs: Vec>) -> Vec {
- let input: Vec<_> = blobs.iter().map(|blob| &blob[..]).collect();
- let input = &input[..];
-
- let mut result: Vec = input
+ let mut result: Vec<_> = blobs
.par_iter()
- .map(|input| Extractor::unique(input, Default::default()))
+ .flat_map(|blob| blob.par_split(|x| matches!(x, b'\n')))
+ .map(|blob| Extractor::unique(blob, Default::default()))
.reduce(Default::default, |mut a, b| {
a.extend(b);
a
@@ -473,6 +484,7 @@ fn parse_all_blobs(blobs: Vec>) -> Vec {
unsafe { String::from_utf8_unchecked(s.to_vec()) }
})
.collect();
- result.sort();
+
+ result.par_sort();
result
}
diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs
index 281774261ad6..1c8fdf75c0f2 100644
--- a/crates/oxide/src/parser.rs
+++ b/crates/oxide/src/parser.rs
@@ -595,7 +595,7 @@ impl<'a> Extractor<'a> {
fn parse_start(&mut self) -> ParseAction<'a> {
match self.cursor.curr {
// Enter arbitrary property mode
- b'[' => {
+ b'[' if self.cursor.prev != b'\\' => {
trace!("Arbitrary::Start\t");
self.arbitrary = Arbitrary::Brackets {
start_idx: self.cursor.pos,
@@ -1634,4 +1634,18 @@ mod test {
]
);
}
+
+ #[test]
+ fn arbitrary_properties_are_not_picked_up_after_an_escape() {
+ _please_trace();
+ let candidates = run(
+ r#"
+
+ \\[a\\]\\:block]
+ "#,
+ false,
+ );
+
+ assert_eq!(candidates, vec!["!code", "a"]);
+ }
}
diff --git a/crates/oxide/src/scanner/allowed_paths.rs b/crates/oxide/src/scanner/allowed_paths.rs
index 459d17d27ca3..8a584d06fb2c 100644
--- a/crates/oxide/src/scanner/allowed_paths.rs
+++ b/crates/oxide/src/scanner/allowed_paths.rs
@@ -40,7 +40,6 @@ pub fn resolve_paths(root: &Path) -> impl Iterator- {
.filter_map(Result::ok)
}
-#[tracing::instrument(skip_all)]
pub fn read_dir(root: &Path, depth: Option) -> impl Iterator
- {
WalkBuilder::new(root)
.hidden(false)
diff --git a/integrations/cli/config.test.ts b/integrations/cli/config.test.ts
index 3e55fa113f46..dd93ade648cc 100644
--- a/integrations/cli/config.test.ts
+++ b/integrations/cli/config.test.ts
@@ -161,7 +161,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -214,7 +217,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -267,7 +273,10 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch')
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ )
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('dist/out.css', [
//
diff --git a/integrations/cli/index.test.ts b/integrations/cli/index.test.ts
index c6eac6fa782f..a1620617153e 100644
--- a/integrations/cli/index.test.ts
+++ b/integrations/cli/index.test.ts
@@ -1,7 +1,7 @@
import dedent from 'dedent'
import os from 'node:os'
import path from 'node:path'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
const STANDALONE_BINARY = (() => {
@@ -156,9 +156,10 @@ describe.each([
},
},
async ({ root, fs, spawn }) => {
- await spawn(`${command} --input src/index.css --output dist/out.css --watch`, {
+ let process = await spawn(`${command} --input src/index.css --output dist/out.css --watch`, {
cwd: path.join(root, 'project-a'),
})
+ await process.onStderr((m) => m.includes('Done in'))
await fs.expectFileToContain('project-a/dist/out.css', [
candidate`underline`,
@@ -491,7 +492,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm tailwindcss --input index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
@@ -722,7 +723,7 @@ test(
>`,
},
},
- async ({ fs, exec, spawn, root }) => {
+ async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
cwd: path.join(root, 'project-a'),
})
@@ -790,9 +791,13 @@ test(
`)
// Watch mode tests
- await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch', {
- cwd: path.join(root, 'project-a'),
- })
+ let process = await spawn(
+ 'pnpm tailwindcss --input src/index.css --output dist/out.css --watch',
+ {
+ cwd: path.join(root, 'project-a'),
+ },
+ )
+ await process.onStderr((m) => m.includes('Done in'))
// Changes to project-a should not be included in the output, we changed the
// base folder to project-b.
@@ -962,7 +967,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm tailwindcss --input index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
diff --git a/integrations/package.json b/integrations/package.json
index 900679dc5f68..8023842cd36a 100644
--- a/integrations/package.json
+++ b/integrations/package.json
@@ -4,7 +4,6 @@
"private": true,
"devDependencies": {
"dedent": "1.5.3",
- "fast-glob": "^3.3.2",
- "kill-port": "^2.0.1"
+ "fast-glob": "^3.3.2"
}
}
diff --git a/integrations/postcss/config.test.ts b/integrations/postcss/config.test.ts
index 30da4219d9ab..11a3c4bd6e1c 100644
--- a/integrations/postcss/config.test.ts
+++ b/integrations/postcss/config.test.ts
@@ -148,7 +148,8 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ await process.onStderr((m) => m.includes('Waiting for file changes'))
await fs.expectFileToContain('dist/out.css', [
//
@@ -218,7 +219,8 @@ test(
},
},
async ({ fs, spawn }) => {
- await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ let process = await spawn('pnpm postcss src/index.css --output dist/out.css --watch --verbose')
+ await process.onStderr((m) => m.includes('Waiting for file changes'))
await fs.expectFileToContain('dist/out.css', [
//
diff --git a/integrations/postcss/core-as-postcss-plugin.test.ts b/integrations/postcss/core-as-postcss-plugin.test.ts
index 85779e7b26f8..17545fea32d8 100644
--- a/integrations/postcss/core-as-postcss-plugin.test.ts
+++ b/integrations/postcss/core-as-postcss-plugin.test.ts
@@ -1,4 +1,4 @@
-import { expect } from 'vitest'
+import { describe } from 'vitest'
import { css, js, json, test } from '../utils'
const variantConfig = {
@@ -29,9 +29,9 @@ const variantConfig = {
},
}
-for (let variant of Object.keys(variantConfig)) {
+describe.each(Object.keys(variantConfig))('%s', (variant) => {
test(
- `can not use \`tailwindcss\` as a postcss module (${variant})`,
+ `can not use \`tailwindcss\` as a postcss module`,
{
fs: {
...variantConfig[variant],
@@ -47,7 +47,7 @@ for (let variant of Object.keys(variantConfig)) {
'src/index.css': css`@import 'tailwindcss';`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
expect(
exec('pnpm postcss src/index.css --output dist/out.css', undefined, { ignoreStdErr: true }),
).rejects.toThrowError(
@@ -55,4 +55,4 @@ for (let variant of Object.keys(variantConfig)) {
)
},
)
-}
+})
diff --git a/integrations/postcss/index.test.ts b/integrations/postcss/index.test.ts
index 883bc561560e..4da70ee1f390 100644
--- a/integrations/postcss/index.test.ts
+++ b/integrations/postcss/index.test.ts
@@ -1,6 +1,5 @@
import dedent from 'dedent'
import path from 'node:path'
-import { expect } from 'vitest'
import { candidate, css, html, js, json, test, ts, yaml } from '../utils'
test(
@@ -724,7 +723,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm postcss index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
@@ -954,7 +953,7 @@ test(
`,
},
},
- async ({ fs, exec, spawn, root }) => {
+ async ({ fs, exec, spawn, root, expect }) => {
await exec('pnpm postcss src/index.css --output dist/out.css --verbose', {
cwd: path.join(root, 'project-a'),
})
@@ -1020,7 +1019,6 @@ test(
cwd: path.join(root, 'project-a'),
},
)
-
await process.onStderr((message) => message.includes('Waiting for file changes...'))
// Changes to project-a should not be included in the output, we changed the
@@ -1215,7 +1213,7 @@ test(
'pages/nested/foo.jsx': 'content-["pages/nested/foo.jsx"] content-["BAD"]',
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm postcss index.css --output dist/out.css')
expect(await fs.dumpFiles('./dist/*.css')).toMatchInlineSnapshot(`
diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts
index b1472c5284aa..b133a9752840 100644
--- a/integrations/postcss/next.test.ts
+++ b/integrations/postcss/next.test.ts
@@ -1,4 +1,4 @@
-import { expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, fetchStyles, js, json, retryAssertion, test } from '../utils'
test(
@@ -56,7 +56,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm next build')
let files = await fs.glob('.next/static/css/**/*.css')
@@ -70,9 +70,10 @@ test(
])
},
)
-;['turbo', 'webpack'].forEach((bundler) => {
+
+describe.each(['turbo', 'webpack'])('%s', (bundler) => {
test(
- `dev mode (${bundler})`,
+ 'dev mode',
{
fs: {
'package.json': json`
@@ -126,26 +127,35 @@ test(
`,
},
},
- async ({ fs, spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''} --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn(`pnpm next dev ${bundler === 'turbo' ? '--turbo' : ''}`)
+
+ 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('Ready in'))
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
- await retryAssertion(async () => {
- await fs.write(
- 'app/page.js',
- js`
- export default function Page() {
- return
Hello, Next.js!
- }
- `,
- )
+ await fs.write(
+ 'app/page.js',
+ js`
+ export default function Page() {
+ return Hello, Next.js!
+ }
+ `,
+ )
+ await process.onStdout((m) => m.includes('Compiled in'))
- let css = await fetchStyles(port)
+ await retryAssertion(async () => {
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`text-red-500`)
})
diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts
index 42cff58dea40..e656253a9c77 100644
--- a/integrations/upgrade/index.test.ts
+++ b/integrations/upgrade/index.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, html, js, json, test, ts } from '../utils'
test(
@@ -26,7 +25,7 @@ test(
'src/fonts.css': css`/* Unrelated CSS file */`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade')
expect(output).toContain('Cannot find any CSS files that reference Tailwind CSS.')
@@ -95,7 +94,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -202,7 +201,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -274,7 +273,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -346,7 +345,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -423,7 +422,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -526,7 +525,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -633,7 +632,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain(
@@ -704,7 +703,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
@@ -751,7 +750,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
let packageJsonContent = await fs.read('package.json')
@@ -802,7 +801,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
@@ -877,7 +876,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
await fs.expectFileToContain('src/index.css', css`@import 'tailwindcss';`)
@@ -944,7 +943,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
@@ -991,7 +990,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.html')).toMatchInlineSnapshot(`
@@ -1035,7 +1034,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1107,7 +1106,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1218,7 +1217,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -1352,7 +1351,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
let output = await exec('npx @tailwindcss/upgrade --force')
expect(output).toMatch(
@@ -1476,7 +1475,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -1701,7 +1700,7 @@ test(
`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade --force', {}, { ignoreStdErr: true }).catch(
(e) => e.toString(),
)
@@ -1773,7 +1772,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -1909,7 +1908,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -2029,7 +2028,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.{html,css}')).toMatchInlineSnapshot(`
@@ -2109,7 +2108,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -2193,7 +2192,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
expect(await fs.dumpFiles('./src/**/*.css')).toMatchInlineSnapshot(`
@@ -2247,7 +2246,7 @@ test(
'tailwind.config.js': js`module.exports = {}`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
let pkg = JSON.parse(await fs.read('package.json'))
@@ -2326,7 +2325,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
// Files should not be modified
@@ -2425,7 +2424,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade --force')
// Files should not be modified
@@ -2532,7 +2531,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade ./src/index.css')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -2619,7 +2618,7 @@ test(
`,
},
},
- async ({ exec }) => {
+ async ({ exec, expect }) => {
let output = await exec('npx @tailwindcss/upgrade', {}, { ignoreStdErr: true }).catch((e) =>
e.toString(),
)
@@ -2692,7 +2691,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('./src/**/*.{css,html}')).toMatchInlineSnapshot(`
diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts
index e0173bcf367c..f2643143a124 100644
--- a/integrations/upgrade/js-config.test.ts
+++ b/integrations/upgrade/js-config.test.ts
@@ -1,5 +1,5 @@
import path from 'node:path'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, html, json, test, ts } from '../utils'
test(
@@ -151,7 +151,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,js,html}')).toMatchInlineSnapshot(`
@@ -350,7 +350,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -436,7 +436,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,ts}')).toMatchInlineSnapshot(`
@@ -522,7 +522,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -600,7 +600,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -674,7 +674,7 @@ test(
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -784,7 +784,7 @@ test(
'project-b/src/index.html': html``,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('project-{a,b}/**/*.{css,ts}')).toMatchInlineSnapshot(`
@@ -882,7 +882,7 @@ test(
'backend/mails/welcome.blade.php': html``,
},
},
- async ({ root, exec, fs }) => {
+ async ({ root, exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade', {
cwd: path.join(root, 'frontend'),
})
@@ -950,7 +950,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1012,7 +1012,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1074,7 +1074,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1113,7 +1113,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1184,7 +1184,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.css')).toMatchInlineSnapshot(`
@@ -1287,7 +1287,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -1397,7 +1397,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
@@ -1499,7 +1499,7 @@ describe('border compatibility', () => {
`,
},
},
- async ({ exec, fs }) => {
+ async ({ exec, fs, expect }) => {
await exec('npx @tailwindcss/upgrade')
expect(await fs.dumpFiles('src/**/*.{css,html}')).toMatchInlineSnapshot(`
diff --git a/integrations/utils.ts b/integrations/utils.ts
index 9e356c089a43..8f916a3d042f 100644
--- a/integrations/utils.ts
+++ b/integrations/utils.ts
@@ -1,12 +1,11 @@
import dedent from 'dedent'
import fastGlob from 'fast-glob'
-import killPort from 'kill-port'
import { exec, spawn } from 'node:child_process'
import fs from 'node:fs/promises'
-import net from 'node:net'
import { platform, tmpdir } from 'node:os'
import path from 'node:path'
-import { test as defaultTest, expect } from 'vitest'
+import { stripVTControlCharacters } from 'node:util'
+import { test as defaultTest, type ExpectStatic } from 'vitest'
const REPO_ROOT = path.join(__dirname, '..')
const PUBLIC_PACKAGES = (await fs.readdir(path.join(REPO_ROOT, 'dist'))).map((name) =>
@@ -35,9 +34,9 @@ interface TestConfig {
}
interface TestContext {
root: string
+ expect: ExpectStatic
exec(command: string, options?: ChildProcessOptions, execOptions?: ExecOptions): Promise
spawn(command: string, options?: ChildProcessOptions): Promise
- getFreePort(): Promise
fs: {
write(filePath: string, content: string): Promise
create(filePaths: string[]): Promise
@@ -54,6 +53,7 @@ interface TestContext {
type TestCallback = (context: TestContext) => Promise | void
interface TestFlags {
only?: boolean
+ skip?: boolean
debug?: boolean
}
@@ -73,11 +73,17 @@ export function test(
name: string,
config: TestConfig,
testCallback: TestCallback,
- { only = false, debug = false }: TestFlags = {},
+ { only = false, skip = false, debug = false }: TestFlags = {},
) {
- return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
+ return defaultTest(
name,
- { timeout: TEST_TIMEOUT, retry: process.env.CI ? 2 : 0 },
+ {
+ timeout: TEST_TIMEOUT,
+ retry: process.env.CI ? 2 : 0,
+ only: only || (!process.env.CI && debug),
+ skip,
+ concurrent: true,
+ },
async (options) => {
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
await fs.mkdir(rootDir, { recursive: true })
@@ -92,6 +98,7 @@ export function test(
let context = {
root,
+ expect: options.expect,
async exec(
command: string,
childProcessOptions: ChildProcessOptions = {},
@@ -155,7 +162,9 @@ export function test(
})
function dispose() {
- child.kill()
+ if (!child.kill()) {
+ child.kill('SIGKILL')
+ }
let timer = setTimeout(
() =>
@@ -199,14 +208,18 @@ export function test(
let content = result.toString()
if (debug || only) console.log(content)
combined.push(['stdout', content])
- stdoutMessages.push(content)
+ for (let line of content.split('\n')) {
+ stdoutMessages.push(stripVTControlCharacters(line))
+ }
notifyNext(stdoutActors, stdoutMessages)
})
child.stderr.on('data', (result) => {
let content = result.toString()
if (debug || only) console.error(content)
combined.push(['stderr', content])
- stderrMessages.push(content)
+ for (let line of content.split('\n')) {
+ stderrMessages.push(stripVTControlCharacters(line))
+ }
notifyNext(stderrActors, stderrMessages)
})
child.on('exit', onExit)
@@ -246,42 +259,6 @@ export function test(
},
}
},
- async getFreePort(): Promise {
- return new Promise((resolve, reject) => {
- let server = net.createServer()
- server.listen(0, () => {
- let address = server.address()
- let port = address === null || typeof address === 'string' ? null : address.port
-
- server.close(() => {
- if (port === null) {
- reject(new Error(`Failed to get a free port: address is ${address}`))
- } else {
- disposables.push(async () => {
- // Wait for 10ms in case the process was just killed
- await new Promise((resolve) => setTimeout(resolve, 10))
-
- // kill-port uses `lsof` on macOS which is expensive and can
- // block for multiple seconds. In order to avoid that for a
- // server that is no longer running, we check if the port is
- // still in use first.
- let isPortTaken = await testIfPortTaken(port)
- if (!isPortTaken) {
- return
- }
-
- try {
- await killPort(port)
- } catch {
- // If the process can not be killed, we can't do anything
- }
- })
- resolve(port)
- }
- })
- })
- })
- },
fs: {
async write(filename: string, content: string | Uint8Array): Promise {
let full = path.join(root, filename)
@@ -374,9 +351,9 @@ export function test(
let fileContent = await this.read(filePath)
for (let content of Array.isArray(contents) ? contents : [contents]) {
if (content instanceof RegExp) {
- expect(fileContent).toMatch(content)
+ options.expect(fileContent).toMatch(content)
} else {
- expect(fileContent).toContain(content)
+ options.expect(fileContent).toContain(content)
}
}
})
@@ -385,7 +362,7 @@ export function test(
return retryAssertion(async () => {
let fileContent = await this.read(filePath)
for (let content of contents) {
- expect(fileContent).not.toContain(content)
+ options.expect(fileContent).not.toContain(content)
}
})
},
@@ -448,6 +425,9 @@ export function test(
test.only = (name: string, config: TestConfig, testCallback: TestCallback) => {
return test(name, config, testCallback, { only: true })
}
+test.skip = (name: string, config: TestConfig, testCallback: TestCallback) => {
+ return test(name, config, testCallback, { skip: true })
+}
test.debug = (name: string, config: TestConfig, testCallback: TestCallback) => {
return test(name, config, testCallback, { debug: true })
}
@@ -462,16 +442,14 @@ async function overwriteVersionsInPackageJson(content: string): Promise
let json = JSON.parse(content)
// Resolve all workspace:^ versions to local tarballs
- ;['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach(
- (key) => {
- let dependencies = json[key] || {}
- for (let dependency in dependencies) {
- if (dependencies[dependency] === 'workspace:^') {
- dependencies[dependency] = resolveVersion(dependency)
- }
+ for (let key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
+ let dependencies = json[key] || {}
+ for (let dependency in dependencies) {
+ if (dependencies[dependency] === 'workspace:^') {
+ dependencies[dependency] = resolveVersion(dependency)
}
- },
- )
+ }
+ }
// Inject transitive dependency overwrite. This is necessary because
// @tailwindcss/vite internally depends on a specific version of
@@ -505,25 +483,6 @@ export function stripTailwindComment(content: string) {
return content.replace(/\/\*! tailwindcss .*? \*\//g, '').trim()
}
-function testIfPortTaken(port: number): Promise {
- return new Promise((resolve) => {
- let client = new net.Socket()
- client.once('connect', () => {
- resolve(true)
- client.end()
- })
- client.once('error', (error: any) => {
- if (error.code !== 'ECONNREFUSED') {
- resolve(true)
- } else {
- resolve(false)
- }
- client.end()
- })
- client.connect({ port: port, host: 'localhost' })
- })
-}
-
export let svg = dedent
export let css = dedent
export let html = dedent
@@ -642,8 +601,12 @@ export async function retryAssertion(
throw error
}
-export async function fetchStyles(port: number, path = '/'): Promise {
- let index = await fetch(`http://localhost:${port}${path}`)
+export async function fetchStyles(base: string, path = '/'): Promise {
+ while (base.endsWith('/')) {
+ base = base.slice(0, -1)
+ }
+
+ let index = await fetch(`${base}${path}`)
let html = await index.text()
let linkRegex = / {
stylesheets.push(
...(await Promise.all(
paths.map(async (path) => {
- let css = await fetch(`http://localhost:${port}${path}`, {
+ let css = await fetch(`${base}${path}`, {
headers: {
Accept: 'text/css',
},
diff --git a/integrations/vite/astro.test.ts b/integrations/vite/astro.test.ts
index 188d6e9f4d80..5b7407e4714b 100644
--- a/integrations/vite/astro.test.ts
+++ b/integrations/vite/astro.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, fetchStyles, html, json, retryAssertion, test, ts } from '../utils'
test(
@@ -35,12 +34,21 @@ test(
`,
},
},
- async ({ fs, spawn, getFreePort }) => {
- let port = await getFreePort()
- await spawn(`pnpm astro dev --port ${port}`)
+ async ({ fs, spawn, expect }) => {
+ let process = await spawn('pnpm astro 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)
+ })
+
+ await process.onStdout((m) => m.includes('watching for file changes'))
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
@@ -56,7 +64,7 @@ test(
`,
)
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
diff --git a/integrations/vite/config.test.ts b/integrations/vite/config.test.ts
index 3e0401f9e2a9..3bf560e074fb 100644
--- a/integrations/vite/config.test.ts
+++ b/integrations/vite/config.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, fetchStyles, html, js, json, retryAssertion, test, ts } from '../utils'
test(
@@ -51,7 +50,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -115,7 +114,7 @@ test(
`,
},
},
- async ({ fs, exec }) => {
+ async ({ fs, exec, expect }) => {
await exec('pnpm vite build')
let files = await fs.glob('dist/**/*.css')
@@ -181,12 +180,19 @@ test(
`,
},
},
- async ({ fs, getFreePort, spawn }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ fs, 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)
+ })
await retryAssertion(async () => {
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: blue')
})
@@ -194,7 +200,7 @@ test(
await retryAssertion(async () => {
await fs.write('my-color.cjs', js`module.exports = 'red'`)
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: red')
})
@@ -253,12 +259,19 @@ test(
`,
},
},
- async ({ fs, getFreePort, spawn }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ async ({ fs, 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)
+ })
await retryAssertion(async () => {
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: blue')
})
@@ -266,7 +279,7 @@ test(
await retryAssertion(async () => {
await fs.write('my-color.mjs', js`export default 'red'`)
- let css = await fetchStyles(port, '/index.html')
+ let css = await fetchStyles(url, '/index.html')
expect(css).toContain(candidate`text-primary`)
expect(css).toContain('color: red')
})
diff --git a/integrations/vite/css-modules.test.ts b/integrations/vite/css-modules.test.ts
index 31a4456a52c8..668281cb494e 100644
--- a/integrations/vite/css-modules.test.ts
+++ b/integrations/vite/css-modules.test.ts
@@ -1,67 +1,65 @@
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, html, test, ts, txt } from '../utils'
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- `dev mode`,
- {
- fs: {
- '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(
+ `dev mode`,
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^6"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
+ }
+ `,
+ '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()],
- })
- `,
- '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';
+ 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 80a2a50c4578..eebe11127a05 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,454 +14,365 @@ 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"
- }
- }
- `,
- '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/tailwind.config.js': js`
- export default {
- content: ['../project-b/src/**/*.js'],
+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/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 }) => {
- 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']`,
- ])
+ }
+ `,
+ '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/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 }
+ `,
},
- )
-
- 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"
- }
+ },
+ 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`
+ }
+ `,
+ '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`
- Tailwind Labs
+ 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, 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']`)
- })
+ )
+
+ 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 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')}
- 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"
- }
+ .red {
+ color: red;
}
`,
- '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;
+ )
+
+ 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`)
+ })
+ },
+ )
+
+ 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-b/src/index.html': html`
-
- `,
- 'project-b/src/index.js': js`
- const className = "content-['project-b/src/index.js']"
- module.exports = { className }
- `,
- },
+ }
+ `,
+ '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]
- })
+ },
+ 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'))
+
+ 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 fs.expectFileToContain(filename, [
- candidate`underline`,
- candidate`flex`,
+ await retryAssertion(async () => {
+ await fs.write(
+ 'project-a/src/custom-theme.css',
css`
- .text-primary {
- color: var(--color-primary);
+ /* Overriding the primary color */
+ @theme {
+ --color-primary: red;
}
`,
- ])
-
- await retryAssertion(async () => {
- await fs.write(
- 'project-a/src/custom-theme.css',
- css`
- /* Overriding the primary color */
- @theme {
- --color-primary: red;
- }
- `,
- )
-
- 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`
-
-
-
-
- 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']`)
- })
+ let files = await fs.glob('project-a/dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
- 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`)
- })
- },
- )
-
- 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`
+ 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!
- Hello, world!
-
-
+ Hello, world!
+
- Hello, world!
-
-
+ Hello, world!
+
+
+ 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']"
+ )
+
+ 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 }
`,
- },
- },
- 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]
-
- // `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"
- }
+ 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;
}
`,
- '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`
-
- `,
- '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 }) => {
- 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']`,
- ])
+ 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`
+
+ `,
+ '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 }
+ `,
},
- )
-
- 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"
- }
+ },
+ 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`
-
- `,
- '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 }
- `,
- },
+ }
+ `,
+ '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`
+
+ `,
+ '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 }) => {
- 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')
+ },
+ 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']`,
+ ])
- let files = await fs.glob('project-a/dist/**/*.css')
- expect(files).toHaveLength(0)
+ // 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`
+
+ `,
+ '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 d2d59f50e4a7..39da43690448 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 cfd521e48300..1b834525c923 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`
-
+
Hello world!
- `,
+ `,
)
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
expect(css).toContain(candidate`font-bold`)
})
})
-test('build', SETUP, async ({ spawn, getFreePort, exec }) => {
- let port = await getFreePort()
- await exec(`pnpm nuxt build`)
- await spawn(`pnpm nuxt preview`, {
+test('build', SETUP, async ({ spawn, exec, expect }) => {
+ await exec('pnpm nuxt build')
+ let process = await spawn('pnpm nuxt preview', {
env: {
- PORT: `${port}`,
+ TEST: 'false',
+ NODE_ENV: 'development',
},
})
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Listening on\s*(http.*)\/?/.exec(m)
+ if (match) url = match[1].replace('http://[::]', 'http://127.0.0.1')
+ return m.includes('Listening on')
+ })
+
await retryAssertion(async () => {
- let css = await fetchStyles(port)
+ let css = await fetchStyles(url)
expect(css).toContain(candidate`underline`)
})
})
diff --git a/integrations/vite/other-transforms.test.ts b/integrations/vite/other-transforms.test.ts
index a0ee01c19c52..6aef096a7853 100644
--- a/integrations/vite/other-transforms.test.ts
+++ b/integrations/vite/other-transforms.test.ts
@@ -1,5 +1,5 @@
import dedent from 'dedent'
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { css, fetchStyles, html, retryAssertion, test, ts, txt } from '../utils'
function createSetup(transformer: 'postcss' | 'lightningcss') {
@@ -58,115 +58,121 @@ function createSetup(transformer: 'postcss' | 'lightningcss') {
}
}
-for (let transformer of ['postcss', 'lightningcss'] as const) {
- describe(transformer, () => {
- test(`production build`, createSetup(transformer), async ({ fs, exec }) => {
- await exec('pnpm vite build')
+describe.each(['postcss', 'lightningcss'] as const)('%s', (transformer) => {
+ test(`production build`, createSetup(transformer), async ({ fs, exec, 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, [
+ css`
+ .foo {
+ color: blue;
+ }
+ `,
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `,
+ ])
+ })
+
+ test('dev mode', createSetup(transformer), 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)
+ })
- await fs.expectFileToContain(filename, [
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+ expect(styles).toContain(css`
+ .foo {
+ color: blue;
+ }
+ `)
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ expect(styles).toContain(dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `)
+ })
+
+ await retryAssertion(async () => {
+ await fs.write(
+ 'src/index.css',
css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+
.foo {
- color: blue;
+ background-color: red;
}
`,
- // Running the transforms on utilities generated by Tailwind might change in the future
- dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `,
- ])
+ )
+
+ let styles = await fetchStyles(url)
+ expect(styles).toContain(css`
+ .foo {
+ background-color: blue;
+ }
+ `)
})
+ })
- test(`dev mode`, createSetup(transformer), async ({ spawn, getFreePort, fs }) => {
- let port = await getFreePort()
- await spawn(`pnpm vite dev --port ${port}`)
+ test('watch mode', createSetup(transformer), async ({ spawn, fs, expect }) => {
+ let process = await spawn('pnpm vite build --watch')
+ await process.onStdout((m) => m.includes('built in'))
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
- expect(styles).toContain(css`
- .foo {
- color: blue;
- }
- `)
- // Running the transforms on utilities generated by Tailwind might change in the future
- expect(styles).toContain(dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `)
- })
-
- await retryAssertion(async () => {
- await fs.write(
- 'src/index.css',
- css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
-
- .foo {
- background-color: red;
- }
- `,
- )
-
- let styles = await fetchStyles(port)
- expect(styles).toContain(css`
- .foo {
- background-color: blue;
- }
- `)
- })
- })
+ await retryAssertion(async () => {
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
- test('watch mode', createSetup(transformer), async ({ spawn, fs }) => {
- await spawn(`pnpm vite build --watch`)
+ expect(styles).toContain(css`
+ .foo {
+ color: blue;
+ }
+ `)
+ // Running the transforms on utilities generated by Tailwind might change in the future
+ expect(styles).toContain(dedent`
+ .\[background-color\:red\] {
+ background-color: blue;
+ }
+ `)
+ })
- await retryAssertion(async () => {
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
+ await retryAssertion(async () => {
+ await fs.write(
+ 'src/index.css',
+ css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
- expect(styles).toContain(css`
- .foo {
- color: blue;
- }
- `)
- // Running the transforms on utilities generated by Tailwind might change in the future
- expect(styles).toContain(dedent`
- .\[background-color\:red\] {
- background-color: blue;
- }
- `)
- })
-
- await retryAssertion(async () => {
- await fs.write(
- 'src/index.css',
- css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
-
- .foo {
- background-color: red;
- }
- `,
- )
-
- let files = await fs.glob('dist/**/*.css')
- expect(files).toHaveLength(1)
- let [, styles] = files[0]
-
- expect(styles).toContain(css`
.foo {
- background-color: blue;
+ background-color: red;
}
- `)
- })
+ `,
+ )
+
+ let files = await fs.glob('dist/**/*.css')
+ expect(files).toHaveLength(1)
+ let [, styles] = files[0]
+
+ expect(styles).toContain(css`
+ .foo {
+ background-color: blue;
+ }
+ `)
})
})
-}
+})
diff --git a/integrations/vite/resolvers.test.ts b/integrations/vite/resolvers.test.ts
index b7098d47fc68..040f843912fd 100644
--- a/integrations/vite/resolvers.test.ts
+++ b/integrations/vite/resolvers.test.ts
@@ -1,143 +1,148 @@
-import { describe, expect } from 'vitest'
+import { describe } from 'vitest'
import { candidate, css, fetchStyles, html, js, retryAssertion, test, ts, txt } from '../utils'
-for (let transformer of ['postcss', 'lightningcss']) {
- describe(transformer, () => {
- test(
- `resolves aliases in production build`,
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^5.3.5"
- }
+describe.each(['postcss', 'lightningcss'])('%s', (transformer) => {
+ test(
+ 'resolves aliases in production build',
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^5.3.5"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
- import { fileURLToPath } from 'node:url'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+ import { fileURLToPath } from 'node:url'
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- resolve: {
- alias: {
- '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
- '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
- },
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
+ '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
- })
- `,
- 'index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'src/index.css': css`
- @import '#css-alias';
- @plugin '#js-alias';
- `,
- 'src/alias.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- `,
- 'src/plugin.js': js`
- export default function ({ addUtilities }) {
- addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
- }
- `,
- },
+ },
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/index.css': css`
+ @import '#css-alias';
+ @plugin '#js-alias';
+ `,
+ 'src/alias.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/plugin.js': js`
+ export default function ({ addUtilities }) {
+ addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
+ }
+ `,
},
- async ({ fs, exec }) => {
- await exec('pnpm vite build')
+ },
+ async ({ fs, exec, 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, [candidate`underline`, candidate`custom-underline`])
- },
- )
+ await fs.expectFileToContain(filename, [candidate`underline`, candidate`custom-underline`])
+ },
+ )
- test(
- `resolves aliases in dev mode`,
- {
- fs: {
- 'package.json': txt`
- {
- "type": "module",
- "dependencies": {
- "@tailwindcss/vite": "workspace:^",
- "tailwindcss": "workspace:^"
- },
- "devDependencies": {
- ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
- "vite": "^5.3.5"
- }
+ test(
+ 'resolves aliases in dev mode',
+ {
+ fs: {
+ 'package.json': txt`
+ {
+ "type": "module",
+ "dependencies": {
+ "@tailwindcss/vite": "workspace:^",
+ "tailwindcss": "workspace:^"
+ },
+ "devDependencies": {
+ ${transformer === 'lightningcss' ? `"lightningcss": "^1.26.0",` : ''}
+ "vite": "^5.3.5"
}
- `,
- 'vite.config.ts': ts`
- import tailwindcss from '@tailwindcss/vite'
- import { defineConfig } from 'vite'
- import { fileURLToPath } from 'node:url'
+ }
+ `,
+ 'vite.config.ts': ts`
+ import tailwindcss from '@tailwindcss/vite'
+ import { defineConfig } from 'vite'
+ import { fileURLToPath } from 'node:url'
- export default defineConfig({
- css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
- build: { cssMinify: false },
- plugins: [tailwindcss()],
- resolve: {
- alias: {
- '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
- '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
- },
+ export default defineConfig({
+ css: ${transformer === 'postcss' ? '{}' : "{ transformer: 'lightningcss' }"},
+ build: { cssMinify: false },
+ plugins: [tailwindcss()],
+ resolve: {
+ alias: {
+ '#css-alias': fileURLToPath(new URL('./src/alias.css', import.meta.url)),
+ '#js-alias': fileURLToPath(new URL('./src/plugin.js', import.meta.url)),
},
- })
- `,
- 'index.html': html`
-
-
-
-
- Hello, world!
-
- `,
- 'src/index.css': css`
- @import '#css-alias';
- @plugin '#js-alias';
- `,
- 'src/alias.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- `,
- 'src/plugin.js': js`
- export default function ({ addUtilities }) {
- addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
- }
- `,
- },
+ },
+ })
+ `,
+ 'index.html': html`
+
+
+
+
+ Hello, world!
+
+ `,
+ 'src/index.css': css`
+ @import '#css-alias';
+ @plugin '#js-alias';
+ `,
+ 'src/alias.css': css`
+ @import 'tailwindcss/theme' theme(reference);
+ @import 'tailwindcss/utilities';
+ `,
+ 'src/plugin.js': js`
+ export default function ({ addUtilities }) {
+ addUtilities({ '.custom-underline': { 'border-bottom': '1px solid green' } })
+ }
+ `,
},
- 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'))
- await retryAssertion(async () => {
- let styles = await fetchStyles(port, '/index.html')
- expect(styles).toContain(candidate`underline`)
- expect(styles).toContain(candidate`custom-underline`)
- })
- },
- )
- })
-}
+ let url = ''
+ await process.onStdout((m) => {
+ let match = /Local:\s*(http.*)\//.exec(m)
+ if (match) url = match[1]
+ return Boolean(url)
+ })
+
+ await retryAssertion(async () => {
+ let styles = await fetchStyles(url, '/index.html')
+ expect(styles).toContain(candidate`underline`)
+ expect(styles).toContain(candidate`custom-underline`)
+ })
+ },
+ )
+})
diff --git a/integrations/vite/ssr.test.ts b/integrations/vite/ssr.test.ts
new file mode 100644
index 000000000000..3c3f1cc4beec
--- /dev/null
+++ b/integrations/vite/ssr.test.ts
@@ -0,0 +1,138 @@
+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, expect }) => {
+ 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, expect }) => {
+ 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/integrations/vite/svelte.test.ts b/integrations/vite/svelte.test.ts
index 12acd63a579d..33eca5a13a2f 100644
--- a/integrations/vite/svelte.test.ts
+++ b/integrations/vite/svelte.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'vitest'
import { candidate, css, html, json, retryAssertion, test, ts } from '../utils'
test(
@@ -9,11 +8,11 @@ test(
{
"type": "module",
"dependencies": {
- "svelte": "^4.2.18",
+ "svelte": "^5",
"tailwindcss": "workspace:^"
},
"devDependencies": {
- "@sveltejs/vite-plugin-svelte": "^3.1.1",
+ "@sveltejs/vite-plugin-svelte": "^5",
"@tailwindcss/vite": "workspace:^",
"vite": "^6"
}
@@ -48,10 +47,7 @@ test(
target: document.body,
})
`,
- 'src/index.css': css`
- @import 'tailwindcss/theme' theme(reference);
- @import 'tailwindcss/utilities';
- `,
+ 'src/index.css': css`@import 'tailwindcss';`,
'src/App.svelte': html`
-
-