From 08972f294f58bc17e3df33c2cea5fee8be7bbaa7 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 18 Feb 2025 11:51:58 +0100 Subject: [PATCH] Scan Next.js dynamic route segments with manual `@source` rules (#16457) Part of #16287 ## Test plan Added unit and integration tests --------- Co-authored-by: Robin Malfait --- CHANGELOG.md | 1 + Cargo.lock | 23 ++++++--- crates/oxide/Cargo.toml | 2 +- crates/oxide/src/glob.rs | 4 +- crates/oxide/src/lib.rs | 2 +- crates/oxide/tests/scanner.rs | 26 ++++++++++ integrations/postcss/next.test.ts | 86 +++++++++++++++++++++++++++++++ 7 files changed, 133 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05fee5027c13..3ad10731b5cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure drop shadow utilities don't inherit unexpectedly ([#16471](https://github.com/tailwindlabs/tailwindcss/pull/16471)) - Export backwards compatible config and plugin types from `tailwindcss/plugin` ([#16505](https://github.com/tailwindlabs/tailwindcss/pull/16505)) - Ensure JavaScript plugins that emit nested rules referencing to the utility name work as expected ([#16539](https://github.com/tailwindlabs/tailwindcss/pull/16539)) +- Ensure that Next.js splat routes are automatically scanned for classes ([#16457](https://github.com/tailwindlabs/tailwindcss/pull/16457)) - Pin exact versions of `tailwindcss` and `@tailwindcss/*` ([#16623](https://github.com/tailwindlabs/tailwindcss/pull/16623)) - Upgrade: Report errors when updating dependencies ([#16504](https://github.com/tailwindlabs/tailwindcss/pull/16504)) - Upgrade: Ensure a `darkMode` JS config setting with block syntax converts to use `@slot` ([#16507](https://github.com/tailwindlabs/tailwindcss/pull/16507)) diff --git a/Cargo.lock b/Cargo.lock index eb9602407af0..f742de3eefb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "bexpand" version = "1.2.0" @@ -142,16 +148,19 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.1.1" +name = "fast-glob" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "0eca69ef247d19faa15ac0156968637440824e5ff22baa5ee0cd35b2f7ea6a0f" +dependencies = [ + "arrayvec", +] [[package]] -name = "glob-match" -version = "0.2.1" +name = "fastrand" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "globset" @@ -524,7 +533,7 @@ dependencies = [ "bstr", "crossbeam", "dunce", - "glob-match", + "fast-glob", "globwalk", "ignore", "log", diff --git a/crates/oxide/Cargo.toml b/crates/oxide/Cargo.toml index 287e0cd61092..96d847b96e02 100644 --- a/crates/oxide/Cargo.toml +++ b/crates/oxide/Cargo.toml @@ -16,7 +16,7 @@ walkdir = "2.5.0" ignore = "0.4.23" dunce = "1.0.5" bexpand = "1.2.0" -glob-match = "0.2.1" +fast-glob = "0.4.3" [dev-dependencies] tempfile = "3.13.0" diff --git a/crates/oxide/src/glob.rs b/crates/oxide/src/glob.rs index 92c83908bc3a..6c7676ece953 100644 --- a/crates/oxide/src/glob.rs +++ b/crates/oxide/src/glob.rs @@ -1,5 +1,5 @@ +use fast_glob::glob_match; use fxhash::{FxHashMap, FxHashSet}; -use glob_match::glob_match; use std::path::{Path, PathBuf}; use tracing::event; @@ -173,7 +173,7 @@ pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool { globs .iter() - .any(|g| glob_match(&format!("{}/{}", g.base, g.pattern), &path)) + .any(|g| glob_match(&format!("{}/{}", g.base, g.pattern), path.as_bytes())) } #[cfg(test)] diff --git a/crates/oxide/src/lib.rs b/crates/oxide/src/lib.rs index 557e87ef5f78..03fe9e8f190f 100644 --- a/crates/oxide/src/lib.rs +++ b/crates/oxide/src/lib.rs @@ -4,9 +4,9 @@ use crate::scanner::allowed_paths::resolve_paths; use crate::scanner::detect_sources::DetectSources; use bexpand::Expression; use bstr::ByteSlice; +use fast_glob::glob_match; use fxhash::{FxHashMap, FxHashSet}; use glob::optimize_patterns; -use glob_match::glob_match; use paths::Path; use rayon::prelude::*; use scanner::allowed_paths::read_dir; diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index e14bfc7316f7..6f30ee5222ec 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -415,6 +415,32 @@ mod scanner { assert_eq!(candidates, vec!["content-['foo.styl']"]); } + #[test] + fn it_should_scan_next_dynamic_folders() { + let candidates = scan_with_globs( + &[ + // We know that `.styl` extensions are ignored, so they are not covered by auto content + // detection. + ("app/[slug]/page.styl", "content-['[slug]']"), + ("app/[...slug]/page.styl", "content-['[...slug]']"), + ("app/[[...slug]]/page.styl", "content-['[[...slug]]']"), + ("app/(theme)/page.styl", "content-['(theme)']"), + ], + vec!["./**/*.{styl}"], + ) + .1; + + assert_eq!( + candidates, + vec![ + "content-['(theme)']", + "content-['[...slug]']", + "content-['[[...slug]]']", + "content-['[slug]']", + ], + ); + } + #[test] fn it_should_scan_absolute_paths() { // Create a temporary working directory diff --git a/integrations/postcss/next.test.ts b/integrations/postcss/next.test.ts index b133a9752840..48c6a18dfcc0 100644 --- a/integrations/postcss/next.test.ts +++ b/integrations/postcss/next.test.ts @@ -162,3 +162,89 @@ describe.each(['turbo', 'webpack'])('%s', (bundler) => { }, ) }) + +test( + 'should scan dynamic route segments', + { + fs: { + 'package.json': json` + { + "dependencies": { + "react": "^18", + "react-dom": "^18", + "next": "^14" + }, + "devDependencies": { + "@tailwindcss/postcss": "workspace:^", + "tailwindcss": "workspace:^" + } + } + `, + 'postcss.config.mjs': js` + /** @type {import('postcss-load-config').Config} */ + const config = { + plugins: { + '@tailwindcss/postcss': {}, + }, + } + + export default config + `, + 'next.config.mjs': js` + /** @type {import('next').NextConfig} */ + const nextConfig = {} + + export default nextConfig + `, + 'app/a/[slug]/page.js': js` + export default function Page() { + return

Hello, Next.js!

+ } + `, + 'app/b/[...slug]/page.js': js` + export default function Page() { + return

Hello, Next.js!

+ } + `, + 'app/c/[[...slug]]/page.js': js` + export default function Page() { + return

Hello, Next.js!

+ } + `, + 'app/d/(theme)/page.js': js` + export default function Page() { + return

Hello, Next.js!

+ } + `, + 'app/layout.js': js` + import './globals.css' + + export default function RootLayout({ children }) { + return ( + + {children} + + ) + } + `, + 'app/globals.css': css` + @import 'tailwindcss/utilities' source(none); + @source './**/*.{js,ts,jsx,tsx,mdx}'; + `, + }, + }, + async ({ fs, exec, expect }) => { + await exec('pnpm next build') + + let files = await fs.glob('.next/static/css/**/*.css') + expect(files).toHaveLength(1) + let [filename] = files[0] + + await fs.expectFileToContain(filename, [ + candidate`content-['[slug]']`, + candidate`content-['[...slug]']`, + candidate`content-['[[...slug]]']`, + candidate`content-['(theme)']`, + ]) + }, +)