CodeVetter · Repo Unpack · Risk map
tailwindlabs/tailwindcss
Security surface is narrow but real: the compiler loads and executes arbitrary user JS via `@plugin`/`@config` through jiti, the upgrade CLI shells out to git and the package manager and rewrites source files in place, and the scanner walks the filesystem outside the repo root whenever `@source` points there. The larger operational risks are correctness-of-caching risks — every integration hand-rolls its own mtime-based invalidation, and the core `build()` explicitly assumes candidates are only ever added, never removed. Cross-platform fragility (Windows paths, symlinks, Bun/USERPROFILE) shows up repeatedly as inline workarounds, which is a reliable signal of where regressions land.
- `@plugin` and `@config` execute arbitrary user code: `loadModule` resolves the id and calls `importModule`, which falls back to `createJiti(...).import(path)` when native `import()` fails — so a `.ts` config file is transpiled and run in-process with full Node privileges.
- `globalThis.__tw_resolve` and `globalThis.__tw_load` are ambient global escape hatches consulted before every CSS and JS resolution; the standalone binary installs them to serve embedded assets, but any code loaded into the process can override module resolution by setting them.
- `@tailwindcss/upgrade` shells out with `execSync` to `git rev-parse --show-toplevel`, `git status --porcelain`, and `npm ls tailwindcss --json`, then calls `pkgManager.add(...)` to install packages — it mutates the user's dependency tree as part of a 'migration'.
- The upgrade tool's only safety net for in-place source rewriting is the `isRepoDirty()` check, which `--force` disables outright; after that it rewrites every matched CSS file and every template file via `writeFileSafely` with no backup.
- Template rewriting is heuristic and acknowledged as such: `isSafeMigration` maintains hand-written regex denylists for Vue `v-if`, Alpine `wire:`, shadcn `variant:` props, Next.js `placeholder=`, and `$emit(` — any unlisted pattern risks a class-name rewrite inside a string that is not a class attribute.
- File IO reaches outside the repo root by design: `SourceEntry::External` exists specifically to let `@source "../node_modules/my-lib"` bypass the default ignore rules, and `resolveJsId` extends module search with `process.env.NODE_PATH`.
- `build()` carries a documented soundness assumption — 'this currently assumes that we only add new candidates and never remove any' — appearing twice as an early-return condition, so a candidate disappearing from source will not shrink the emitted CSS without a full rebuild.
- Cache invalidation is duplicated four times with slightly different shapes: `Root.requiresBuild()` in Vite, the `context.mtimes` loop in PostCSS, `getRebuildStrategy` in the CLI, and per-loader `CacheEntry.mtimes` in webpack/Turbopack — a bug fixed in one will not propagate to the others.
- `@tailwindcss/webpack` and `@tailwindcss/turbopack` are near-verbatim copies of each other down to identical `LoaderOptions`, `CacheEntry`, `getCacheKey`, and `getContextFromCache` definitions — divergence between them is a live duplication hazard.
- The PostCSS plugin deliberately swallows errors — it `console.error`s and then throws `root.error(...)` after resetting `context.compiler = null`, with a comment that throwing directly 'will cause PostCSS to no longer watch for changes' — so genuine compile failures can surface as an empty stylesheet plus console noise.
- The Vite plugin carries two hardcoded upstream-bug workarounds — skipping `addWatchFile` for the input file itself (Qwik) and ignoring `.svg` paths containing `#` or `?` (tailwindcss#16877) — meaning edits to those files silently do not trigger rebuilds.
- Windows and symlink handling is a recurring fragility: the scanner canonicalizes via `dunce::canonicalize` with a cached parent-directory map, inserts both symlinked and canonical paths into `self.files`, and `scan_content` has to re-walk the tree to validate previously-unknown changed files.
- The standalone build re-spawns itself as a nested Bun process with `USERPROFILE` blanked out to work around Bun binary downloads failing on Windows CI when Turborepo passes the variable through — a build-time landmine with no test coverage.
- `packages/tailwindcss/src/feature-flags.ts` is an empty file still present in the tree — dead scaffolding that suggests a removed flag system.
- `crates/ignore` is a vendored fork of BurntSushi's crate carrying local `# CHANGED:` edits (e.g. pinning `edition = "2024"` instead of inheriting from the workspace), so upstream security or correctness fixes require a manual re-vendor.
- The CLI watch path is a ~600-line function with no unit tests of its own — `getRebuildStrategy`, `watchDirectories`, `createWatchers`, and the polling loop are only exercised indirectly through `integrations/cli/index.test.ts`. (inferred)