CodeVetter · Repo Unpack · Behavior traces
tailwindlabs/tailwindcss
Three flows matter most: a cold CLI build, a CLI watch-mode rebuild, and the Vite dev-server transform. All three share the same skeleton — create compiler, derive sources, construct `Scanner`, scan, build, write — but differ in how they decide between a full and an incremental rebuild. The browser CDN build is a fourth, MutationObserver-driven variant of the same loop. The distinguishing detail across all of them is that 'full rebuild' means *discarding and recreating the compiler*, while 'incremental' means only feeding new candidate strings into the existing one.
- Cold CLI build order: `run()` → `build.handle(flags)` → resolve `--cwd`/`--input`/`--output` to absolute paths and reject identical in/out → read input (or `drainStdin()`, or default `@import 'tailwindcss';`) → `createCompiler(input, I)` → `scanner.scan()` → `compiler.build(candidates)` → `write(output, map, args, I)` → print `Done in …`.
- `createCompiler` registers an `onDependency(path)` callback that pushes into `fullRebuildPaths`, and then deliberately appends a *negated* source for `path.basename(process.execPath)` so the standalone binary never scans itself — a rename-proof self-exclusion.
- Watch mode calls `loadWatcher()` first (so a missing `@parcel/watcher` fails with a useful message before any watcher is registered), then `createWatchers(await watchDirectories(scanner), handle)`.
- The watch handler short-circuits when the only changed file is `args['--output']`, preventing the write-triggers-rebuild infinite loop, then calls `getRebuildStrategy(files, fullRebuildPaths)` to choose a branch.
- On a `full` rebuild the CLI does, in order: re-read input → `clearRequireCache(resolvedFullRebuildPaths)` → snapshot `backupRebuildPaths` → reset `fullRebuildPaths` → `createCompiler` → `scanner.scan()` → set up new watchers → tear down old watchers → `compiler.build(candidates)`.
- On an `incremental` rebuild it calls `scanner.scanFiles(rebuildStrategy.changedFiles)` and returns early without writing if zero new candidates were found — so touching a file with no new classes costs no disk write.
- The watch `catch` block restores `fullRebuildPaths = backupRebuildPaths` before logging, with an inline comment explaining the recovery case: deleting a transitive dependency of `tailwind.config.js` would otherwise leave the compiler permanently unable to re-register that dependency.
- Polling mode is a separate code path from event-based watching: `--poll` is validated to be a positive number (default 250ms), and it drives a `fullRebuild(I)` closure plus cursor-hiding escape codes with `SIGINT`/`SIGTERM` handlers that exit 130/143.
- Vite dev transform order: `isPotentialCssRootFile(id)` gate → look up a `Root` in `rootsByEnv` keyed by `this.environment?.name` → `root.generate(src, addWatchFile, I)` → return `{ code, map }`; returning `false` from `generate` deletes the root from the map so it can be garbage collected.
- `Root.generate` decides to rebuild by calling `requiresBuild()`, which stats every entry in `buildDependencies` and returns true if any mtime advanced or any file is missing — an mtime-map strategy mirrored in the PostCSS and webpack plugins.
- After compiling, `Root.generate` bails with `false` unless the compiler reported at least one of `Features.AtApply | JsPluginCompat | ThemeFunction | Utilities | Variants` — this is how ordinary non-Tailwind `.css` files pass through Vite untouched.
- The PostCSS plugin has an even cheaper bail: before touching the cache it does a `root.walkAtRules` scan for any of `import|reference|theme|variant|config|plugin|apply|tailwind` and returns immediately if none are present.
- Browser CDN flow: `rebuild('full')` → `createCompiler()` gathers all `style[type="text/tailwindcss"]` text, compares against `lastCss` to skip needless recompiles → `build(kind)` collects `element.classList` across `[class]` elements, diffs against a global `classes` Set, and assigns `sheet.textContent = compiler.build(newClasses)`; builds are serialized through a `buildQueue` promise chain.
- Two MutationObservers drive the browser build: one watches each known `<style>` element for `characterData`/`childList` changes (→ full rebuild), the other watches `document.documentElement` for `class` attribute changes and added nodes (→ incremental), explicitly skipping the injected output `sheet` to avoid a feedback loop.
- Upgrade flow order: dirty-git check → version match check → glob `**/*.css` → `Stylesheet.load` → `analyzeStylesheets` → `linkConfigsToStylesheets` (v3 only) → `migrateJsConfig` → `migrateStylesheet` → `splitStylesheets` (v3 only) → `postcss([sortBuckets(), formatNodes()])` → `writeFileSafely` → package-manager upgrade → template migration.