Skip to content

Commit

Permalink
Add @content support to tailwindcss (#14079)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RobinMalfait authored Aug 1, 2024
1 parent 7ee134a commit f2cd0b3
Show file tree
Hide file tree
Showing 11 changed files with 542 additions and 77 deletions.
262 changes: 254 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

53 changes: 41 additions & 12 deletions crates/node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,79 @@ impl From<ChangedContent> for tailwindcss_oxide::ChangedContent {
}

#[derive(Debug, Clone)]
#[napi(object)]
#[napi]
pub struct ScanResult {
pub globs: Vec<GlobEntry>,
pub files: Vec<String>,
pub candidates: Vec<String>,
}

#[napi]
impl ScanResult {
#[napi]
pub fn scan_files(&self, input: Vec<ChangedContent>) -> Vec<String> {
tailwindcss_oxide::scan_files_with_globs(
input.into_iter().map(Into::into).collect(),
self.globs.clone().into_iter().map(Into::into).collect(),
)
}
}

#[derive(Debug, Clone)]
#[napi(object)]
pub struct GlobEntry {
pub base: String,
pub glob: String,
}

impl From<GlobEntry> for tailwindcss_oxide::GlobEntry {
fn from(globs: GlobEntry) -> Self {
tailwindcss_oxide::GlobEntry {
base: globs.base,
glob: globs.glob,
}
}
}

impl From<tailwindcss_oxide::GlobEntry> for GlobEntry {
fn from(globs: tailwindcss_oxide::GlobEntry) -> Self {
GlobEntry {
base: globs.base,
glob: globs.glob,
}
}
}

#[derive(Debug, Clone)]
#[napi(object)]
pub struct ScanOptions {
/// Base path to start scanning from
pub base: String,
pub globs: Option<bool>,
/// Glob content paths
pub content_paths: Option<Vec<GlobEntry>>,
}

#[napi]
pub fn clear_cache() {
tailwindcss_oxide::clear_cache();
tailwindcss_oxide::clear_cache();
}

#[napi]
pub fn scan_dir(args: ScanOptions) -> ScanResult {
let result = tailwindcss_oxide::scan_dir(tailwindcss_oxide::ScanOptions {
base: args.base,
globs: args.globs.unwrap_or(false),
content_paths: args
.content_paths
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
});

ScanResult {
files: result.files,
candidates: result.candidates,
globs: result
.globs
.into_iter()
.map(|g| GlobEntry {
base: g.base,
glob: g.glob,
})
.collect(),
globs: result.globs.into_iter().map(Into::into).collect(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/oxide/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
walkdir = "2.3.3"
ignore = "0.4.20"
lazy_static = "1.4.0"
glob-match = "0.2.1"
serial_test = "3.1.1"

[dev-dependencies]
tempfile = "3.5.0"
Expand Down
103 changes: 63 additions & 40 deletions crates/oxide/src/glob.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use glob_match::glob_match;
use std::iter;
use std::path::{Path, PathBuf};

use crate::GlobEntry;

pub fn fast_glob(
base_path: &Path,
patterns: &Vec<String>,
patterns: &Vec<GlobEntry>,
) -> Result<impl iter::Iterator<Item = PathBuf>, std::io::Error> {
Ok(get_fast_patterns(base_path, patterns)
Ok(get_fast_patterns(patterns)
.into_iter()
.flat_map(|(base_path, patterns)| {
globwalk::GlobWalkerBuilder::from_patterns(base_path, &patterns)
Expand Down Expand Up @@ -40,10 +42,13 @@ pub fn fast_glob(
/// tailwind --pwd ./project/pages --content "**/*.js"
/// tailwind --pwd ./project/components --content "**/*.js"
/// ```
fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf, Vec<String>)> {
pub fn get_fast_patterns(patterns: &Vec<GlobEntry>) -> Vec<(PathBuf, Vec<String>)> {
let mut optimized_patterns: Vec<(PathBuf, Vec<String>)> = vec![];

for pattern in patterns {
let base_path = PathBuf::from(&pattern.base);
let pattern = &pattern.glob;

let is_negated = pattern.starts_with('!');
let mut pattern = pattern.clone();
if is_negated {
Expand All @@ -54,13 +59,13 @@ fn get_fast_patterns(base_path: &Path, patterns: &Vec<String>) -> Vec<(PathBuf,

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

let mut bail = false;

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

pub fn path_matches_globs(path: &Path, globs: &[GlobEntry]) -> bool {
let path = path.to_string_lossy();

globs
.iter()
.any(|g| glob_match(&format!("{}/{}", g.base, g.glob), &path))
}

/// Given this input: a-{b,c}-d-{e,f}
/// We will get:
/// [
Expand Down Expand Up @@ -228,30 +241,38 @@ fn expand_braces(input: &str) -> Vec<String> {
#[cfg(test)]
mod tests {
use super::get_fast_patterns;
use crate::GlobEntry;
use std::path::PathBuf;

#[test]
fn it_should_keep_globs_that_start_with_file_wildcards_as_is() {
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["*.html".to_string()]);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "*.html".to_string(),
}]);
let expected = vec![(PathBuf::from("/projects"), vec!["*.html".to_string()])];

assert_eq!(actual, expected,);
}

#[test]
fn it_should_keep_globs_that_start_with_folder_wildcards_as_is() {
let actual = get_fast_patterns(&PathBuf::from("/projects"), &vec!["**/*.html".to_string()]);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "**/*.html".to_string(),
}]);

let expected = vec![(PathBuf::from("/projects"), vec!["**/*.html".to_string()])];

assert_eq!(actual, expected,);
}

#[test]
fn it_should_move_the_starting_folder_to_the_path() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["example/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "example/*.html".to_string(),
}]);
let expected = vec![(
PathBuf::from("/projects/example"),
vec!["*.html".to_string()],
Expand All @@ -262,10 +283,10 @@ mod tests {

#[test]
fn it_should_move_the_starting_folders_to_the_path() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["example/other/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "example/other/*.html".to_string(),
}]);
let expected = vec![(
PathBuf::from("/projects/example/other"),
vec!["*.html".to_string()],
Expand All @@ -276,10 +297,11 @@ mod tests {

#[test]
fn it_should_branch_expandable_folders() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["{foo,bar}/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "{foo,bar}/*.html".to_string(),
}]);

let expected = vec![
(PathBuf::from("/projects/foo"), vec!["*.html".to_string()]),
(PathBuf::from("/projects/bar"), vec!["*.html".to_string()]),
Expand All @@ -290,10 +312,10 @@ mod tests {

#[test]
fn it_should_expand_multiple_expansions_in_the_same_folder() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["a-{b,c}-d-{e,f}-g/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "a-{b,c}-d-{e,f}-g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a-b-d-e-g"),
Expand All @@ -318,10 +340,10 @@ mod tests {

#[test]
fn multiple_expansions_per_folder_starting_at_the_root() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "{a,b}-c-{d,e}-f/{b,c}-d-{e,f}-g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a-c-d-f/b-d-e-g"),
Expand Down Expand Up @@ -394,10 +416,11 @@ mod tests {

#[test]
fn it_should_stop_expanding_once_we_hit_a_wildcard() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["{foo,bar}/example/**/{baz,qux}/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "{foo,bar}/example/**/{baz,qux}/*.html".to_string(),
}]);

let expected = vec![
(
PathBuf::from("/projects/foo/example"),
Expand All @@ -414,10 +437,10 @@ mod tests {

#[test]
fn it_should_keep_the_negation_symbol_for_all_new_patterns() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["!{foo,bar}/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "!{foo,bar}/*.html".to_string(),
}]);
let expected = vec![
(PathBuf::from("/projects/foo"), vec!["!*.html".to_string()]),
(PathBuf::from("/projects/bar"), vec!["!*.html".to_string()]),
Expand All @@ -428,10 +451,10 @@ mod tests {

#[test]
fn it_should_expand_a_complex_example() {
let actual = get_fast_patterns(
&PathBuf::from("/projects"),
&vec!["a/{b,c}/d/{e,f}/g/*.html".to_string()],
);
let actual = get_fast_patterns(&vec![GlobEntry {
base: "/projects".to_string(),
glob: "a/{b,c}/d/{e,f}/g/*.html".to_string(),
}]);
let expected = vec![
(
PathBuf::from("/projects/a/b/d/e/g"),
Expand Down
Loading

0 comments on commit f2cd0b3

Please sign in to comment.