mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
This PR is an umbrella PR where we will add support for the new `@source` directive. This will allow you to add explicit content glob patterns if you want to look for Tailwind classes in other files that are not automatically detected yet. Right now this is an addition to the existing auto content detection that is automatically enabled in the `@tailwindcss/postcss` and `@tailwindcss/cli` packages. The `@tailwindcss/vite` package doesn't use the auto content detection, but uses the module graph instead. From an API perspective there is not a lot going on. There are only a few things that you have to know when using the `@source` directive, and you probably already know the rules: 1. You can use multiple `@source` directives if you want. 2. The `@source` accepts a glob pattern so that you can match multiple files at once 3. The pattern is relative to the current file you are in 4. The pattern includes all files it is matching, even git ignored files 1. The motivation for this is so that you can explicitly point to a `node_modules` folder if you want to look at `node_modules` for whatever reason. 6. Right now we don't support negative globs (starting with a `!`) yet, that will be available in the near future. Usage example: ```css /* ./src/input.css */ @import "tailwindcss"; @source "../laravel/resources/views/**/*.blade.php"; @source "../../packages/monorepo-package/**/*.js"; ``` It looks like the PR introduced a lot of changes, but this is a side effect of all the other plumbing work we had to do to make this work. For example: 1. We added dedicated integration tests that run on Linux and Windows in CI (just to make sure that all the `path` logic is correct) 2. We Have to make sure that the glob patterns are always correct even if you are using `@import` in your CSS and use `@source` in an imported file. This is because we receive the flattened CSS contents where all `@import`s are inlined. 3. We have to make sure that we also listen for changes in the files that match any of these patterns and trigger a rebuild. PRs: - [x] https://github.com/tailwindlabs/tailwindcss/pull/14063 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14085 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14079 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14067 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14076 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14080 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14127 - [x] https://github.com/tailwindlabs/tailwindcss/pull/14135 Once all the PRs are merged, then this umbrella PR can be merged. > [!IMPORTANT] > Make sure to merge this without rebasing such that each individual PR ends up on the main branch. --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
380 lines
14 KiB
Rust
380 lines
14 KiB
Rust
#[cfg(test)]
|
|
mod scan_dir {
|
|
use serial_test::serial;
|
|
use std::process::Command;
|
|
use std::{fs, path};
|
|
|
|
use tailwindcss_oxide::*;
|
|
use tempfile::tempdir;
|
|
|
|
fn scan_with_globs(
|
|
paths_with_content: &[(&str, Option<&str>)],
|
|
globs: Vec<&str>,
|
|
) -> (Vec<String>, Vec<String>) {
|
|
// Ensure that every test truly runs in isolation without any cache
|
|
clear_cache();
|
|
|
|
// Create a temporary working directory
|
|
let dir = tempdir().unwrap().into_path();
|
|
|
|
// Initialize this directory as a git repository
|
|
let _ = Command::new("git").arg("init").current_dir(&dir).output();
|
|
|
|
// Create the necessary files
|
|
for (path, contents) in paths_with_content {
|
|
// Ensure we use the right path separator for the current platform
|
|
let path = dir.join(path.replace('/', path::MAIN_SEPARATOR.to_string().as_str()));
|
|
let parent = path.parent().unwrap();
|
|
if !parent.exists() {
|
|
fs::create_dir_all(parent).unwrap();
|
|
}
|
|
|
|
match contents {
|
|
Some(contents) => fs::write(path, contents).unwrap(),
|
|
None => fs::write(path, "").unwrap(),
|
|
}
|
|
}
|
|
|
|
let base = format!("{}", dir.display());
|
|
|
|
// Resolve all content paths for the (temporary) current working directory
|
|
let result = scan_dir(ScanOptions {
|
|
base: Some(base.clone()),
|
|
sources: globs
|
|
.iter()
|
|
.map(|x| GlobEntry {
|
|
base: base.clone(),
|
|
pattern: x.to_string(),
|
|
})
|
|
.collect(),
|
|
});
|
|
|
|
let mut paths: Vec<_> = result
|
|
.files
|
|
.into_iter()
|
|
.map(|x| x.replace(&format!("{}{}", &base, path::MAIN_SEPARATOR), ""))
|
|
.collect();
|
|
|
|
for glob in result.globs {
|
|
paths.push(format!(
|
|
"{}{}{}",
|
|
glob.base,
|
|
path::MAIN_SEPARATOR,
|
|
glob.pattern
|
|
));
|
|
}
|
|
|
|
paths = paths
|
|
.into_iter()
|
|
.map(|x| {
|
|
let parent_dir = format!("{}{}", &base.to_string(), path::MAIN_SEPARATOR);
|
|
x.replace(&parent_dir, "")
|
|
// Normalize paths to use unix style separators
|
|
.replace('\\', "/")
|
|
})
|
|
.collect();
|
|
|
|
// Sort the output for easier comparison (depending on internal datastructure the order
|
|
// _could_ be random)
|
|
paths.sort();
|
|
|
|
(paths, result.candidates)
|
|
}
|
|
|
|
fn scan(paths_with_content: &[(&str, Option<&str>)]) -> (Vec<String>, Vec<String>) {
|
|
scan_with_globs(paths_with_content, vec![])
|
|
}
|
|
|
|
fn test(paths_with_content: &[(&str, Option<&str>)]) -> Vec<String> {
|
|
scan(paths_with_content).0
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_work_with_a_set_of_root_files() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("a.html", None),
|
|
("b.html", None),
|
|
("c.html", None),
|
|
]);
|
|
assert_eq!(globs, vec!["a.html", "b.html", "c.html", "index.html"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_work_with_a_set_of_root_files_and_ignore_ignored_files() {
|
|
let globs = test(&[
|
|
(".gitignore", Some("b.html")),
|
|
("index.html", None),
|
|
("a.html", None),
|
|
("b.html", None),
|
|
("c.html", None),
|
|
]);
|
|
assert_eq!(globs, vec!["a.html", "c.html", "index.html"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_list_all_files_in_the_public_folder_explicitly() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("public/a.html", None),
|
|
("public/b.html", None),
|
|
("public/c.html", None),
|
|
]);
|
|
assert_eq!(
|
|
globs,
|
|
vec![
|
|
"index.html",
|
|
"public/a.html",
|
|
"public/b.html",
|
|
"public/c.html",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_list_nested_folders_explicitly_in_the_public_folder() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("public/a.html", None),
|
|
("public/b.html", None),
|
|
("public/c.html", None),
|
|
("public/nested/a.html", None),
|
|
("public/nested/b.html", None),
|
|
("public/nested/c.html", None),
|
|
("public/nested/again/a.html", None),
|
|
("public/very/deeply/nested/a.html", None),
|
|
]);
|
|
assert_eq!(
|
|
globs,
|
|
vec![
|
|
"index.html",
|
|
"public/a.html",
|
|
"public/b.html",
|
|
"public/c.html",
|
|
"public/nested/a.html",
|
|
"public/nested/again/a.html",
|
|
"public/nested/b.html",
|
|
"public/nested/c.html",
|
|
"public/very/deeply/nested/a.html",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_list_all_files_in_the_public_folder_explicitly_except_ignored_files() {
|
|
let globs = test(&[
|
|
(".gitignore", Some("public/b.html\na.html")),
|
|
("index.html", None),
|
|
("public/a.html", None),
|
|
("public/b.html", None),
|
|
("public/c.html", None),
|
|
]);
|
|
assert_eq!(globs, vec!["index.html", "public/c.html",]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_use_a_glob_for_top_level_folders() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("src/a.html", None),
|
|
("src/b.html", None),
|
|
("src/c.html", None),
|
|
]);
|
|
assert_eq!(globs, vec![
|
|
"index.html",
|
|
"src/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"src/a.html",
|
|
"src/b.html",
|
|
"src/c.html"
|
|
]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_ignore_binary_files() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("a.mp4", None),
|
|
("b.png", None),
|
|
("c.lock", None),
|
|
]);
|
|
assert_eq!(globs, vec!["index.html"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_ignore_known_extensions() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("a.css", None),
|
|
("b.sass", None),
|
|
("c.less", None),
|
|
]);
|
|
assert_eq!(globs, vec!["index.html"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_ignore_known_files() {
|
|
let globs = test(&[
|
|
("index.html", None),
|
|
("package-lock.json", None),
|
|
("yarn.lock", None),
|
|
]);
|
|
assert_eq!(globs, vec!["index.html"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_ignore_and_expand_nested_ignored_folders() {
|
|
let globs = test(&[
|
|
// Explicitly listed root files
|
|
("foo.html", None),
|
|
("bar.html", None),
|
|
("baz.html", None),
|
|
// Nested folder A, using glob
|
|
("nested-a/foo.html", None),
|
|
("nested-a/bar.html", None),
|
|
("nested-a/baz.html", None),
|
|
// Nested folder B, with deeply nested files, using glob
|
|
("nested-b/deeply-nested/foo.html", None),
|
|
("nested-b/deeply-nested/bar.html", None),
|
|
("nested-b/deeply-nested/baz.html", None),
|
|
// Nested folder C, with ignored sub-folder
|
|
("nested-c/foo.html", None),
|
|
("nested-c/bar.html", None),
|
|
("nested-c/baz.html", None),
|
|
// Ignored folder
|
|
("nested-c/.gitignore", Some("ignored-folder/")),
|
|
("nested-c/ignored-folder/foo.html", None),
|
|
("nested-c/ignored-folder/bar.html", None),
|
|
("nested-c/ignored-folder/baz.html", None),
|
|
// Deeply nested, without issues
|
|
("nested-c/sibling-folder/foo.html", None),
|
|
("nested-c/sibling-folder/bar.html", None),
|
|
("nested-c/sibling-folder/baz.html", None),
|
|
// Nested folder D, with deeply nested ignored folder
|
|
("nested-d/foo.html", None),
|
|
("nested-d/bar.html", None),
|
|
("nested-d/baz.html", None),
|
|
("nested-d/.gitignore", Some("deep/")),
|
|
("nested-d/very/deeply/nested/deep/foo.html", None),
|
|
("nested-d/very/deeply/nested/deep/bar.html", None),
|
|
("nested-d/very/deeply/nested/deep/baz.html", None),
|
|
("nested-d/very/deeply/nested/foo.html", None),
|
|
("nested-d/very/deeply/nested/bar.html", None),
|
|
("nested-d/very/deeply/nested/baz.html", None),
|
|
("nested-d/very/deeply/nested/directory/foo.html", None),
|
|
("nested-d/very/deeply/nested/directory/bar.html", None),
|
|
("nested-d/very/deeply/nested/directory/baz.html", None),
|
|
("nested-d/very/deeply/nested/directory/again/foo.html", None),
|
|
]);
|
|
|
|
assert_eq!(
|
|
globs,
|
|
vec![
|
|
"bar.html",
|
|
"baz.html",
|
|
"foo.html",
|
|
"nested-a/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-a/bar.html",
|
|
"nested-a/baz.html",
|
|
"nested-a/foo.html",
|
|
"nested-b/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-b/deeply-nested/bar.html",
|
|
"nested-b/deeply-nested/baz.html",
|
|
"nested-b/deeply-nested/foo.html",
|
|
"nested-c/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-c/bar.html",
|
|
"nested-c/baz.html",
|
|
"nested-c/foo.html",
|
|
"nested-c/sibling-folder/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-c/sibling-folder/bar.html",
|
|
"nested-c/sibling-folder/baz.html",
|
|
"nested-c/sibling-folder/foo.html",
|
|
"nested-d/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-d/bar.html",
|
|
"nested-d/baz.html",
|
|
"nested-d/foo.html",
|
|
"nested-d/very/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-d/very/deeply/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-d/very/deeply/nested/*/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-d/very/deeply/nested/bar.html",
|
|
"nested-d/very/deeply/nested/baz.html",
|
|
"nested-d/very/deeply/nested/directory/**/*.{py,tpl,js,vue,php,mjs,cts,jsx,tsx,rhtml,slim,handlebars,twig,rs,njk,svelte,liquid,pug,md,ts,heex,mts,astro,nunjucks,rb,eex,haml,cjs,html,hbs,jade,aspx,razor,erb,mustache,mdx}",
|
|
"nested-d/very/deeply/nested/directory/again/foo.html",
|
|
"nested-d/very/deeply/nested/directory/bar.html",
|
|
"nested-d/very/deeply/nested/directory/baz.html",
|
|
"nested-d/very/deeply/nested/directory/foo.html",
|
|
"nested-d/very/deeply/nested/foo.html"
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_scan_for_utilities() {
|
|
let mut ignores = String::new();
|
|
ignores.push_str("# md:font-bold\n");
|
|
ignores.push_str("foo.html\n");
|
|
|
|
let candidates = scan(&[
|
|
// The gitignore file is used to filter out files but not scanned for candidates
|
|
(".gitignore", Some(&ignores)),
|
|
// A file that should definitely be scanned
|
|
("index.html", Some("font-bold md:flex")),
|
|
// A file that should definitely not be scanned
|
|
("foo.jpg", Some("xl:font-bold")),
|
|
// A file that is ignored
|
|
("foo.html", Some("lg:font-bold")),
|
|
// A svelte file with `class:foo="bar"` syntax
|
|
("index.svelte", Some("<div class:px-4='condition'></div>")),
|
|
])
|
|
.1;
|
|
|
|
assert_eq!(
|
|
candidates,
|
|
vec!["condition", "div", "font-bold", "md:flex", "px-4"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_scan_content_paths() {
|
|
let candidates = scan_with_globs(
|
|
&[
|
|
// We know that `.styl` extensions are ignored, so they are not covered by auto content
|
|
// detection.
|
|
("foo.styl", Some("content-['foo.styl']")),
|
|
],
|
|
vec!["*.styl"],
|
|
)
|
|
.1;
|
|
|
|
assert_eq!(candidates, vec!["content-['foo.styl']"]);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn it_should_scan_content_paths_even_when_they_are_git_ignored() {
|
|
let candidates = scan_with_globs(
|
|
&[
|
|
(".gitignore", Some("foo.styl")),
|
|
// We know that `.styl` extensions are ignored, so they are not covered by auto content
|
|
// detection.
|
|
("foo.styl", Some("content-['foo.styl']")),
|
|
],
|
|
vec!["*.styl"],
|
|
)
|
|
.1;
|
|
|
|
assert_eq!(candidates, vec!["content-['foo.styl']"]);
|
|
}
|
|
}
|