tailwindlabs/tailwindcss
Scanning and extraction
How class names get from source files into the compiler.
The contract between adapter and scanner
A Scanner (Rust struct in crates/oxide/src/scanner/mod.rs, exposed via crates/node/src/lib.rs) takes a list of source entries and exposes:
scan()— walk disk, return all found candidates.scan_files(inputs)— extract from in-memoryChangedContent(file or string).get_candidates_with_positions(input)— for tooling that needs source positions.files,globs,normalizedSources— getters that the adapter uses to set up file watchers.
Adapters construct the scanner from the compiler's outputs:
const compiler = await compile(input, {
/* ... */
});
const sources = (() => {
if (compiler.root === 'none') return [];
if (compiler.root === null)
return [{ base, pattern: '**/*', negated: false }];
return [{ ...compiler.root, negated: false }];
})().concat(compiler.sources);
const scanner = new Scanner({ sources });The compiler's root field reflects whatever the input CSS specified via @tailwind utilities source(...); compiler.sources is the list of @source directives.
Walking the project
The scanner uses the vendored crates/ignore/ crate (a fork of BurntSushi's ignore). It:
- Honors
.gitignore,.tailwindcssignore, and.ignore. - Skips hidden files.
- Skips binary extensions (defined in
crates/oxide/src/scanner/auto_source_detection.rs'sBINARY_EXTENSIONS_GLOB). - Skips known build-output directories (
dist/,target/,node_modules/unless explicitly added).
Walking is parallelized by Rayon. A Mutex<FxHashSet<PathBuf>> collects discovered files; an FxHashMap records mtimes for incremental scans.
The fast_skip heuristic
crates/oxide/src/fast_skip.rs runs a small SIMD-ish prefilter on each file's bytes. If the file has no high-information bytes (alphanumeric content, [, (, :, -), it's discarded before extraction. This drops most static assets and binary-looking content that slipped past the extension filter.
The extractor
crates/oxide/src/extractor/mod.rs implements the actual candidate extraction. It works on &[u8] (not &str) so Cursor advances in bytes, not chars. The flow:
- The byte slice is run through a per-extension pre-processor (see below) to normalize template syntax.
- A
Cursorwraps the bytes. - The driver loops, advancing the cursor and feeding bytes to the active state machines.
- When a machine emits
Done(span), the span is trimmed byboundary.rsand pushed asExtracted::Candidate(...)orExtracted::CssVariable(...).
Each machine handles one shape:
candidate_machine.rs— drives the others; matches the entire(variant:)*name(/modifier)?shape.named_utility_machine.rs—bg-red-500,text-center,flex.named_variant_machine.rs—hover:,md:,dark:,group-hover:, etc.arbitrary_value_machine.rs—[#fff],[20px],[var(--x)].arbitrary_property_machine.rs—[--my-var:1].arbitrary_variable_machine.rs—(--var)shorthand.modifier_machine.rs—/50,/[…],/(--var).css_variable_machine.rs— barevar(--foo)references in CSS.string_machine.rs,utility_machine.rs,variant_machine.rs— generic helpers for the others.boundary.rs— decides where a candidate ends.bracket_stack.rs— tracks(),[],{}balance during extraction.
Pre-processors
crates/oxide/src/extractor/pre_processors/ contains per-language fix-ups that run before extraction:
| Language | Behavior |
|---|---|
| Pug | Strip indentation; replace class.foo.bar shorthand with attribute syntax. |
| Slim | Similar to Pug but for Slim's syntax. |
| Haml | Translate %div.foo shorthand into class="foo". |
| Razor (.cshtml) | Strip @code { ... } blocks; recognize attribute helpers. |
| Ruby (ERB) | Strip <% ... %> blocks but keep <%= ... %> content (it can contain class strings). |
| Vue | Recognize class: and :class binding syntax. |
| Svelte | Recognize class:foo, class:foo={enabled} and class={…}. |
Mtime caching
After the first scan finishes, Scanner::has_scanned_once flips to true and the scanner starts populating mtimes. Subsequent calls skip files whose mtime hasn't changed. The optimization is gated behind the first scan because populating the mtime cache during the cold path would slow it down for the most common case (one-off CLI invocations).
How adapters drive the scanner
| Adapter | Strategy |
|---|---|
@tailwindcss/cli |
Calls scanner.scan() once for the initial build. In --watch mode, subscribes to @parcel/watcher events on scanner.files + scanner.globs and calls scanner.scan_files([{ file, ext }]) for each change. |
@tailwindcss/vite |
Calls scanner.scan() per Vite environment + module. |
@tailwindcss/postcss |
Calls scanner.scan() per cached input file. The cache is keyed on (inputFile, base, optimize). |
@tailwindcss/webpack |
Calls scanner.scan() per resource. Reports every found file via this.addDependency and every glob via this.addContextDependency so Webpack knows when to re-invoke the loader. |
@tailwindcss/browser |
Does not use the Rust scanner at all. Replaces it with a MutationObserver that collects classes from document.querySelectorAll('[class]'). |
@tailwindcss/upgrade |
Uses scanner.get_candidates_with_positions(...) so it can rewrite class lists in templates with their original positions intact. |
See oxide and crates/node for implementation details.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.