Skip to content

Commit 06f4d62

Browse files
RobinMalfaitphilipp-spiess
authored andcommitted
Add @content support to tailwindcss (#14079)
This PR adds `@content` support to `tailwindcss`'s core package. We will handle the `@content` and call the `onContentPath` function when it's encountered. The `@tailwindcss/cli`, `@tailwindcss/vite` and `@tailwindcss/postcss` packages have to implement the `onContentPath` such that the necessary globs are scanned and watchers should be setup with this information. Example usage: ```css @content "../../packages/my-sibling-package/src/components/*.tsx"; ``` If you are in a monorepo setup, then you could point to other packages if you want. Another common use case is for Laravel projects if you want to point to Laravel blade files since they won't be covered by Vite's module graph: ```css /* ./resources/css/app.css */ @content "../views/*.blade.php" ``` Note: all globs are relative to the current file you are in.
1 parent 6ed972b commit 06f4d62

File tree

11 files changed

+542
-77
lines changed

11 files changed

+542
-77
lines changed

Cargo.lock

Lines changed: 254 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/node/src/lib.rs

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,50 +22,79 @@ impl From<ChangedContent> for tailwindcss_oxide::ChangedContent {
2222
}
2323

2424
#[derive(Debug, Clone)]
25-
#[napi(object)]
25+
#[napi]
2626
pub struct ScanResult {
2727
pub globs: Vec<GlobEntry>,
2828
pub files: Vec<String>,
2929
pub candidates: Vec<String>,
3030
}
3131

32+
#[napi]
33+
impl ScanResult {
34+
#[napi]
35+
pub fn scan_files(&self, input: Vec<ChangedContent>) -> Vec<String> {
36+
tailwindcss_oxide::scan_files_with_globs(
37+
input.into_iter().map(Into::into).collect(),
38+
self.globs.clone().into_iter().map(Into::into).collect(),
39+
)
40+
}
41+
}
42+
3243
#[derive(Debug, Clone)]
3344
#[napi(object)]
3445
pub struct GlobEntry {
3546
pub base: String,
3647
pub glob: String,
3748
}
3849

50+
impl From<GlobEntry> for tailwindcss_oxide::GlobEntry {
51+
fn from(globs: GlobEntry) -> Self {
52+
tailwindcss_oxide::GlobEntry {
53+
base: globs.base,
54+
glob: globs.glob,
55+
}
56+
}
57+
}
58+
59+
impl From<tailwindcss_oxide::GlobEntry> for GlobEntry {
60+
fn from(globs: tailwindcss_oxide::GlobEntry) -> Self {
61+
GlobEntry {
62+
base: globs.base,
63+
glob: globs.glob,
64+
}
65+
}
66+
}
67+
3968
#[derive(Debug, Clone)]
4069
#[napi(object)]
4170
pub struct ScanOptions {
71+
/// Base path to start scanning from
4272
pub base: String,
43-
pub globs: Option<bool>,
73+
/// Glob content paths
74+
pub content_paths: Option<Vec<GlobEntry>>,
4475
}
4576

4677
#[napi]
4778
pub fn clear_cache() {
48-
tailwindcss_oxide::clear_cache();
79+
tailwindcss_oxide::clear_cache();
4980
}
5081

5182
#[napi]
5283
pub fn scan_dir(args: ScanOptions) -> ScanResult {
5384
let result = tailwindcss_oxide::scan_dir(tailwindcss_oxide::ScanOptions {
5485
base: args.base,
55-
globs: args.globs.unwrap_or(false),
86+
content_paths: args
87+
.content_paths
88+
.unwrap_or_default()
89+
.into_iter()
90+
.map(Into::into)
91+
.collect(),
5692
});
5793

5894
ScanResult {
5995
files: result.files,
6096
candidates: result.candidates,
61-
globs: result
62-
.globs
63-
.into_iter()
64-
.map(|g| GlobEntry {
65-
base: g.base,
66-
glob: g.glob,
67-
})
68-
.collect(),
97+
globs: result.globs.into_iter().map(Into::into).collect(),
6998
}
7099
}
71100

crates/oxide/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
1515
walkdir = "2.3.3"
1616
ignore = "0.4.20"
1717
lazy_static = "1.4.0"
18+
glob-match = "0.2.1"
19+
serial_test = "3.1.1"
1820

1921
[dev-dependencies]
2022
tempfile = "3.5.0"

crates/oxide/src/glob.rs

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
use glob_match::glob_match;
12
use std::iter;
23
use std::path::{Path, PathBuf};
34

5+
use crate::GlobEntry;
6+
47
pub fn fast_glob(
5-
base_path: &Path,
6-
patterns: &Vec<String>,
8+
patterns: &Vec<GlobEntry>,
79
) -> Result<impl iter::Iterator<Item = PathBuf>, std::io::Error> {
8-
Ok(get_fast_patterns(base_path, patterns)
10+
Ok(get_fast_patterns(patterns)
911
.into_iter()
1012
.flat_map(|(base_path, patterns)| {
1113
globwalk::GlobWalkerBuilder::from_patterns(base_path, &patterns)
@@ -40,10 +42,13 @@ pub fn fast_glob(
4042
/// tailwind --pwd ./project/pages --content "**/*.js"
4143
/// tailwind --pwd ./project/components --content "**/*.js"
4244
/// ```
43-
fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf, Vec<String>)> {
45+
pub fn get_fast_patterns(patterns: &Vec<GlobEntry>) -> Vec<(PathBuf, Vec<String>)> {
4446
let mut optimized_patterns: Vec<(PathBuf, Vec<String>)> = vec![];
4547

4648
for pattern in patterns {
49+
let base_path = PathBuf::from(&pattern.base);
50+
let pattern = &pattern.glob;
51+
4752
let is_negated = pattern.starts_with('!');
4853
let mut pattern = pattern.clone();
4954
if is_negated {
@@ -54,13 +59,13 @@ fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf,
5459

5560
if folders.len() <= 1 {
5661
// No paths we can simplify, so let's use it as-is.
57-
optimized_patterns.push((base_path.to_path_buf(), vec![pattern]));
62+
optimized_patterns.push((base_path, vec![pattern]));
5863
} else {
5964
// We do have folders because `/` exists. Let's try to simplify the globs!
6065
// Safety: We know that the length is greater than 1, so we can safely unwrap.
6166
let file_pattern = folders.pop().unwrap();
6267
let all_folders = folders.clone();
63-
let mut temp_paths = vec![base_path.to_path_buf()];
68+
let mut temp_paths = vec![base_path];
6469

6570
let mut bail = false;
6671

@@ -131,6 +136,14 @@ fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf,
131136
optimized_patterns
132137
}
133138

139+
pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool {
140+
let path = path.to_string_lossy();
141+
142+
globs
143+
.iter()
144+
.any(|g| glob_match(&format!("{}/{}", g.base, g.glob), &path))
145+
}
146+
134147
/// Given this input: a-{b,c}-d-{e,f}
135148
/// We will get:
136149
/// [
@@ -228,30 +241,38 @@ fn expand_braces(input: &str) -> Vec<String> {
228241
#[cfg(test)]
229242
mod tests {
230243
use super::get_fast_patterns;
244+
use crate::GlobEntry;
231245
use std::path::PathBuf;
232246

233247
#[test]
234248
fn it_should_keep_globs_that_start_with_file_wildcards_as_is() {
235-
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["*.html".to_string()]);
249+
let actual = get_fast_patterns(&vec![GlobEntry {
250+
base: "/projects".to_string(),
251+
glob: "*.html".to_string(),
252+
}]);
236253
let expected = vec![(PathBuf::from("/projects"), vec!["*.html".to_string()])];
237254

238255
assert_eq!(actual, expected,);
239256
}
240257

241258
#[test]
242259
fn it_should_keep_globs_that_start_with_folder_wildcards_as_is() {
243-
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["**/*.html".to_string()]);
260+
let actual = get_fast_patterns(&vec![GlobEntry {
261+
base: "/projects".to_string(),
262+
glob: "**/*.html".to_string(),
263+
}]);
264+
244265
let expected = vec![(PathBuf::from("/projects"), vec!["**/*.html".to_string()])];
245266

246267
assert_eq!(actual, expected,);
247268
}
248269

249270
#[test]
250271
fn it_should_move_the_starting_folder_to_the_path() {
251-
let actual = get_fast_patterns(
252-
&PathBuf::from("/projects"),
253-
&vec!["example/*.html".to_string()],
254-
);
272+
let actual = get_fast_patterns(&vec![GlobEntry {
273+
base: "/projects".to_string(),
274+
glob: "example/*.html".to_string(),
275+
}]);
255276
let expected = vec![(
256277
PathBuf::from("/projects/example"),
257278
vec!["*.html".to_string()],
@@ -262,10 +283,10 @@ mod tests {
262283

263284
#[test]
264285
fn it_should_move_the_starting_folders_to_the_path() {
265-
let actual = get_fast_patterns(
266-
&PathBuf::from("/projects"),
267-
&vec!["example/other/*.html".to_string()],
268-
);
286+
let actual = get_fast_patterns(&vec![GlobEntry {
287+
base: "/projects".to_string(),
288+
glob: "example/other/*.html".to_string(),
289+
}]);
269290
let expected = vec![(
270291
PathBuf::from("/projects/example/other"),
271292
vec!["*.html".to_string()],
@@ -276,10 +297,11 @@ mod tests {
276297

277298
#[test]
278299
fn it_should_branch_expandable_folders() {
279-
let actual = get_fast_patterns(
280-
&PathBuf::from("/projects"),
281-
&vec!["{foo,bar}/*.html".to_string()],
282-
);
300+
let actual = get_fast_patterns(&vec![GlobEntry {
301+
base: "/projects".to_string(),
302+
glob: "{foo,bar}/*.html".to_string(),
303+
}]);
304+
283305
let expected = vec![
284306
(PathBuf::from("/projects/foo"), vec!["*.html".to_string()]),
285307
(PathBuf::from("/projects/bar"), vec!["*.html".to_string()]),
@@ -290,10 +312,10 @@ mod tests {
290312

291313
#[test]
292314
fn it_should_expand_multiple_expansions_in_the_same_folder() {
293-
let actual = get_fast_patterns(
294-
&PathBuf::from("/projects"),
295-
&vec!["a-{b,c}-d-{e,f}-g/*.html".to_string()],
296-
);
315+
let actual = get_fast_patterns(&vec![GlobEntry {
316+
base: "/projects".to_string(),
317+
glob: "a-{b,c}-d-{e,f}-g/*.html".to_string(),
318+
}]);
297319
let expected = vec![
298320
(
299321
PathBuf::from("/projects/a-b-d-e-g"),
@@ -318,10 +340,10 @@ mod tests {
318340

319341
#[test]
320342
fn multiple_expansions_per_folder_starting_at_the_root() {
321-
let actual = get_fast_patterns(
322-
&PathBuf::from("/projects"),
323-
&vec!["{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string()],
324-
);
343+
let actual = get_fast_patterns(&vec![GlobEntry {
344+
base: "/projects".to_string(),
345+
glob: "{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string(),
346+
}]);
325347
let expected = vec![
326348
(
327349
PathBuf::from("/projects/a-c-d-f/b-d-e-g"),
@@ -394,10 +416,11 @@ mod tests {
394416

395417
#[test]
396418
fn it_should_stop_expanding_once_we_hit_a_wildcard() {
397-
let actual = get_fast_patterns(
398-
&PathBuf::from("/projects"),
399-
&vec!["{foo,bar}/example/**/{baz,qux}/*.html".to_string()],
400-
);
419+
let actual = get_fast_patterns(&vec![GlobEntry {
420+
base: "/projects".to_string(),
421+
glob: "{foo,bar}/example/**/{baz,qux}/*.html".to_string(),
422+
}]);
423+
401424
let expected = vec![
402425
(
403426
PathBuf::from("/projects/foo/example"),
@@ -414,10 +437,10 @@ mod tests {
414437

415438
#[test]
416439
fn it_should_keep_the_negation_symbol_for_all_new_patterns() {
417-
let actual = get_fast_patterns(
418-
&PathBuf::from("/projects"),
419-
&vec!["!{foo,bar}/*.html".to_string()],
420-
);
440+
let actual = get_fast_patterns(&vec![GlobEntry {
441+
base: "/projects".to_string(),
442+
glob: "!{foo,bar}/*.html".to_string(),
443+
}]);
421444
let expected = vec![
422445
(PathBuf::from("/projects/foo"), vec!["!*.html".to_string()]),
423446
(PathBuf::from("/projects/bar"), vec!["!*.html".to_string()]),
@@ -428,10 +451,10 @@ mod tests {
428451

429452
#[test]
430453
fn it_should_expand_a_complex_example() {
431-
let actual = get_fast_patterns(
432-
&PathBuf::from("/projects"),
433-
&vec!["a/{b,c}/d/{e,f}/g/*.html".to_string()],
434-
);
454+
let actual = get_fast_patterns(&vec![GlobEntry {
455+
base: "/projects".to_string(),
456+
glob: "a/{b,c}/d/{e,f}/g/*.html".to_string(),
457+
}]);
435458
let expected = vec![
436459
(
437460
PathBuf::from("/projects/a/b/d/e/g"),

0 commit comments

Comments
 (0)