Open-Source Wikis

/

Tailwind CSS

/

Features

/

Scanning and extraction

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-memory ChangedContent (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's BINARY_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:

  1. The byte slice is run through a per-extension pre-processor (see below) to normalize template syntax.
  2. A Cursor wraps the bytes.
  3. The driver loops, advancing the cursor and feeding bytes to the active state machines.
  4. When a machine emits Done(span), the span is trimmed by boundary.rs and pushed as Extracted::Candidate(...) or Extracted::CssVariable(...).

Each machine handles one shape:

  • candidate_machine.rs — drives the others; matches the entire (variant:)*name(/modifier)? shape.
  • named_utility_machine.rsbg-red-500, text-center, flex.
  • named_variant_machine.rshover:, 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 — bare var(--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.

Scanning and extraction – Tailwind CSS wiki | Factory