-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
[Oxide] Automatic content detection #11173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Hi, and first of all thanks for all the great work, the thoughts puts in every APIs and Tailwind in general, it totally changed the way I author CSS in the last three years. Would it be possible for Tailwind to offer a more bundler friendly API that let another tool select the appropriate files to be scanned (and also handle the change detection)? The last point you made still feels like you want Tailwind to detect the build tool, instead of providing an API for build tools to create plugins on top of it. I know the current setup is nice for people from other languages to even be able to run Tailwind without node, but for ESM first tools like Vite, we have some hacks in the HMR handling specifically for Tailwind. Letting the bundler dictate the content being scan means that you only scan the code that is in the final bundle, which leads to multiple benefits:
|
|
This seems like a great DX improvement for common projects. And this is coming from someone who doesn't have a problem with needing to set content.
Why would this be better than setting content? I will definitely need to use this or the content because we use a component library via an npm package and node_modules are usually gitignored. |
|
@ArnaudBarre I think that all sounds great and would love to explore it more concretely — any interest in connecting about it sometime in the next week or two? |
@jpsc It's the same really — another thing we're trying to do for v4 is support more configuration from your CSS file instead of needing the JS config is all. You already need a CSS file no matter what, it would be nice if you could do everything in one place instead of needing two files. |
| !(await fs | ||
| .stat(filePath) | ||
| .then(() => true) | ||
| .catch(() => false)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
existsSync is a better solution for this, and will avoid calling catch. Alternatively, statSync has { throwIfNoEntry: true } option to avoid the try/catch as well.
import fs from 'node:fs'
if (!fs.existsSync(filePath)
|
|
||
| export function validateConfig(config) { | ||
| if (config.content.files.length === 0) { | ||
| if (config.content.files !== 'auto' && config.content.files.length === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems this change is unnecessary, since files.length === 0 invalidates !== 'auto'
|
@adamwathan I would be happy to discuss/explore this! I am quite available until May 17 (on UTC+2 TZ) |
This part is solved now ✅ |
Ok, that's understandable. Why would one file be less than two? So you could completely drop |
|
Hmm, as you mentioned this is not quite so easy to find heuristics that reliably find the files that are relevant. Good trick skipping binary files though. |
|
Going to merge this in so it's easier for people to play with and test — no guarantees we don't change directions here but hard to know how it plays out without just giving it a shot and trying to iterate on it 👍 |
1. Files in the root should be listed statically instead of using globs.
2. Files and folders in special known direct child folders should be
listed statically instead of using globs (e.g.: `public`). This is
because these special folders are often used to store generated AND
source files at the same time. Using globs could trigger infinite
loops because we are watching and acting upon dist files.
3. All file extensions found in the project, should be used in the globs
in addition to a known set of extensions.
4. Direct folders seen from the root, can use the glob syntax
`<root>/src/**/*.{...known-extensions}`
Not 100% convinced yet, but seems cleaner so far.
This reverts commit 879c124.
This will make it a bit easier to organize in the future.
The config file will automatically trigger a rebuild when this file is changed. However, this should not be part of the template files because that could cause additional css that's not being used.
- In the oxide engine, the default `content: []` will be dropped from
the default configuration (config.simple.js, config.full.js).
- If you have `content: []` or `content: { files: [] }` then the auto
content feature won't be active. However if those arrays are empty a
warning will still be shown. Adding files/globs or dropping the
`content` section completely will enable auto content.
This way we don't run into the issue where the `config.content.files` is set and the `config.content.auto` is set to true.
Thanks, Clippy!
This will also make sure that if we have (deeply) nested ignored
folders, then we won't use deeply nested globs (**/*.{js,html}) for the
parent(s) of the nested ignored folders but instead use a shallow glob
for each directory (*/*.{js,html}).
Then each sibling directory of the parent can use deeply nested globs
again except for the direct parent.
On a big test project this goes from ~6s to ~200ms
We started with a ~6s duration Then in the previous commit, we improved it by ~30x and it went down to ~200ms Now with this change, it takes about ~40ms. That's another ~5x improvement. Or in total a ~150x improvement.
This is only called once so won't do anything to the main performance of Tailwind CSS. But always nice to make small performance improvements!
b66d8ae to
289bc20
Compare
* resolve all _existing_ content paths
* pin `@napi-rs/cli`
* WIP: Log all resolved content files/globs
* only filter out raw changed content in non-auto mode
* skip parseCandidateFiles cache in `auto` mode
* improve algorithm of detecting content paths
1. Files in the root should be listed statically instead of using globs.
2. Files and folders in special known direct child folders should be
listed statically instead of using globs (e.g.: `public`). This is
because these special folders are often used to store generated AND
source files at the same time. Using globs could trigger infinite
loops because we are watching and acting upon dist files.
3. All file extensions found in the project, should be used in the globs
in addition to a known set of extensions.
4. Direct folders seen from the root, can use the glob syntax
`<root>/src/**/*.{...known-extensions}`
* inline wanted-extensions
Not 100% convinced yet, but seems cleaner so far.
* ensure writing an file also makes the parent folder(s)
* add integration tests for the auto content feature
* add pnpm and bun lock files
* Revert "inline wanted-extensions"
This reverts commit 879c124.
* sort binary-extensions and add lockb
* sort + add `lock` to ignored extensions
* drop `yarn.lock`, because lock extensions are already covered
* group template extensions
This will make it a bit easier to organize in the future.
* drop empty lines and commented lines from template-extensions
* skip the config path when resolving template files
The config file will automatically trigger a rebuild when this file is
changed. However, this should not be part of the template files because
that could cause additional css that's not being used.
* make `auto content` the default in the oxide engine
- In the oxide engine, the default `content: []` will be dropped from
the default configuration (config.simple.js, config.full.js).
- If you have `content: []` or `content: { files: [] }` then the auto
content feature won't be active. However if those arrays are empty a
warning will still be shown. Adding files/globs or dropping the
`content` section completely will enable auto content.
* only test the auto content integration test in the oxide engine
* set `content.files` to `auto` instead of using `auto: boolean`
This way we don't run into the issue where the `config.content.files` is
set and the `config.content.auto` is set to true.
* drop log
* ensure we validate the config in the CLI
* show experimental warning for automatic content detection
* use cached version of the getCandidateFiles instead of bypassing it
* use `is_empty()` shorthand
Thanks, Clippy!
* add test to ensure nested ignored folders are not scanned
* add `tempfile` for tests
* add auto content tests in Rust
* refactor auto content detection
This will also make sure that if we have (deeply) nested ignored
folders, then we won't use deeply nested globs (**/*.{js,html}) for the
parent(s) of the nested ignored folders but instead use a shallow glob
for each directory (*/*.{js,html}).
Then each sibling directory of the parent can use deeply nested globs
again except for the direct parent.
* use consistent comments
* ensure ignored static listed files are not present
* improve performance by ~30x
On a big test project this goes from ~6s to ~200ms
* improve performance by ~5x
We started with a ~6s duration
Then in the previous commit, we improved it by ~30x and it went down to
~200ms
Now with this change, it takes about ~40ms. That's another ~5x
improvement.
Or in total a ~150x improvement.
* ensure nested folders in `public/` are also explicitly listed
* add shortcut for normalizing files
This is only called once so won't do anything to the main performance of
Tailwind CSS. But always nice to make small performance improvements!
* run Rust tests in CI
* fix lint warnings
* update changelog
* Update CHANGELOG.md
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
This PR adds experimental support for what we're calling "automatic content detection" — a feature that lets Tailwind detect the paths it needs to scan to figure out which classes it needs to generate completely automatically, no
contentconfiguration required.To use it, just omit the
contentoption from your configuration file:// tailwind.config.js module.exports = { // Bye bye, you won't be missed - content: [ - "./app/**/*.{js,ts,jsx,tsx,mdx}", - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - ], theme: { extend: {}, }, plugins: [], }You can also enable it explicitly by configuring
contentto'auto':Tailwind will automatically scan every file in your project (excluding gitignored files) that might contain classes and generate all the CSS you need with no configuration.
Since this feature is currently experimental, a warning is issued in the terminal any time it's enabled to make sure you know it's not stable.
This feature is only planned for the new Oxide engine, so it will only be available on the
oxide-insiderstag and not the regularinsiderstag. Will share a lot more about what Oxide is and when we really want people to start playing with it when it's a bit further along over the coming weeks.How it works
Getting this to "just work" given all of the different places people use Tailwind is a serious challenge. This first stab at the problem uses a few heuristics and assumptions that are working very well for the types of projects we've tested it in:
cdto the right folder in your scripts or usenpm runwith the--prefixoption to explicitly set the CWD..gitignorefile will not be scanned for classes. This prevents giant dependency folders likenode_modulesfrom being scanned, as well as any directories where you are storing generated files (like compiled CSS or JS), which avoids infinite rebuild loops../components/**/*.jsand./pages/**/*.js. This makes sure we notice any new files or folders you create and scan them without you needing to restart your build process../public— we explicitly don't watch./public/**/*.{whatever}because it's common to store generated assets like compiled CSS and JS in the./publicfolder which can cause infinite rebuild loops, particularly in webpack where we can't actually register globs to watch and can only register directories. Instead, we explicitly watch each individual file in./publicthat could contain classes, like anindex.htmlfile for instance../**/*because webpack doesn't support globs which means we'd end up watching./node_modules, so instead we watch every top-level file that might contain classes individually. This means creating a new top-level file currently requires restarting your build process. In practice though it's extremely rare to create new top-level files that contain classes.css,scss,sass,less, orstylfiles.package-lock.json.tailwind.config.jsfile is never going to be a source of classes to include in the final CSS so we don't scan it..js,.html,.php, even.json. This way you don't need to restart your dev server the first time you create one of these files in an existing project.*.potatoextension, we'll watch*.potatofiles in all folders as long as we see at least one*.potatofile when the build process starts.In our testing these heuristics work great, and we've been able to remove the
contentconfiguration from every one of our own projects that we've tried it in.If for whatever reason these heuristics don't work properly for your project, you can explicitly configure
contentjust like you were doing before, and Tailwind will respect that configuration and not try to do any automatic content detection at all. This way you always have the option of full control over which files are scanned for classes.Known limitations
How things work currently isn't perfect, and there are a few known limitations you might run into depending on how your project is structured.
*.piledriverfile, Tailwind won't notice it and you'll need to restart your script.npm run {command}because that's hownpm runalready works.node_modulesbut you want to ignore everything else innode_modules, you currently need to opt-out of automatic content detection and go back to explicitly configuring your content paths../componentsfolder for the first time in a Next.js project while your dev server is already running.contentexplicitly.Despite these limitations, we're still finding automatic content detection to be miles ahead of explicit content configuration in terms of developer experience, and for projects that are structured in a conventional way you pretty much don't ever see or feel these limitations at all.
Planned improvements
While we're ready to start shipping support for this in our
oxide-insidersbuilds as-is, we do have some improvements we plan to explore that will hopefully make the experience even better:./src/node_modules, we still scan that folder. We should be able to solve this though, maybe even before we merge this PR.@source "./node_modules/my-library/dist/**/*.js"directive in your CSS, we hope to make it possible to scan paths that live within ignored directories without opting out of automatic content detection. This will also make it possible to scan for classes in parent/sibling directories, which some people might need in certain monorepo setups.Really excited about this one, I think it's the biggest step-function improvement to the developer experience in Tailwind since the JIT engine. Looking forward to getting everyone playing with it so we can refine our heuristics and get things feeling as rock-solid as possible.