Skip to content

Latest commit

 

History

History

README.md

tailwindcss-patch

Modern tooling to patch Tailwind CSS, capture runtime contexts, and materialise every class that Tailwind can generate. The package now ships with a redesigned architecture focused on clarity, predictable configuration, and first-class Tailwind v4 support.

  • Export runtime contexts for Tailwind v2/v3 without editing source files manually.
  • Traverse Tailwind v4 projects by scanning CSS outputs and content sources.
  • Write class inventories to disk or keep them in memory for tooling integrations.
  • Control caching, filtering, and custom unit extensions from a single, typed entrypoint.

Installation

pnpm add -D tailwindcss-patch
pnpm dlx tw-patch install

Keep the patch applied after installs by adding a prepare hook:

{
  "scripts": {
    "prepare": "tw-patch install"
  }
}

CLI Usage

Run the CLI through tw-patch (or tailwindcss-patch) from your project root.

# Apply runtime patches to the local Tailwind installation
pnpm dlx tw-patch install

# Extract all classes into the configured output file
pnpm dlx tw-patch extract

# Capture every token (candidate) with file/position metadata
pnpm dlx tw-patch tokens --format lines

# Check which patches are applied
pnpm dlx tw-patch status --json

# Migrate deprecated config fields to modern keys
pnpm dlx tw-patch migrate --dry-run

# Restore configs from a migration report backup snapshot
pnpm dlx tw-patch restore --report-file .tw-patch/migrate-report.json --dry-run

# Validate migration report compatibility without modifying files
pnpm dlx tw-patch validate --report-file .tw-patch/migrate-report.json --json

Embed into another CLI

Reuse the same commands inside your own cac program:

import cac from 'cac'
import { mountTailwindcssPatchCommands } from 'tailwindcss-patch'

const cli = cac('my-tool')
mountTailwindcssPatchCommands(cli, {
  commandPrefix: 'tw:', // optional
  commands: ['install', 'tokens'], // mount a subset if needed (defaults to all)
  commandOptions: {
    install: { name: 'patch-install', aliases: ['tw-install'] }, // override names/aliases
  },
})
cli.help()
cli.parse()

Custom command hooks

Hosts can override per-command lifecycles by supplying commandHandlers. Each handler receives a context object (with the resolved cwd, parsed args, memoized loadConfig/createPatcher helpers, and the shared logger) plus a next() callback that runs the built-in action.

mountTailwindcssPatchCommands(cli, {
  commandHandlers: {
    install: async (ctx) => {
      const patcher = await ctx.createPatcher()
      await clearTailwindcssPatcherCache(ctx.cwd)
      await patcher.patch()
      await saveCliPatchTargetRecord({ cwd: ctx.cwd })
    },
    extract: async (ctx, next) => {
      const result = await next() // run the default extract implementation
      ctx.logger.success(`[host] wrote ${result.classList.length} classes`)
      return result
    },
  },
  commandOptions: {
    extract: {
      description: 'Localised extract command',
      appendDefaultOptions: false,
      optionDefs: [
        { flags: '--entry <file>', description: 'Tailwind CSS entry file' },
        { flags: '--preview', description: 'Print a preview instead of writing' },
      ],
    },
  },
})

Skip next() to fully replace a command (e.g. custom init or cache clearing before install). Calling next() returns the default result—ExtractResult, TailwindTokenReport, etc.—so hosts can log metadata or feed it into their own telemetry without re-implementing the commands.

Extract options

Flag Description
--cwd <dir> Use a different working directory when loading configuration.
--output <file> Override the target file for the generated class list.
--format <json|lines> Switch between JSON output (default) and newline-delimited text.
--css <file> Provide a CSS entry file when working with Tailwind v4 projects.
--no-write Skip writing to disk and only return the collected classes.

The CLI loads tailwindcss-patch.config.ts via @tailwindcss-mangle/config. Legacy configs continue to work; see the migration guide for hints on the new fields.

Migrate options

Flag Description
--cwd <dir> Working directory used to locate config files.
--config <file> Migrate only one specific config file path.
--workspace Recursively scan the workspace for supported config filenames.
--max-depth <n> Maximum recursion depth for --workspace mode (default: 6).
--include <glob> Only migrate files matching this glob pattern (repeatable).
--exclude <glob> Skip files matching this glob pattern (repeatable).
--report-file <file> Write the migration report JSON to this file.
--backup-dir <dir> Store pre-migration file backups in this directory.
--check Check mode for CI. Exits with an error if files still need migration.
--json Print the migration report as JSON.
--dry-run Preview planned changes without writing files.

tw-patch migrate scans tailwindcss-patch.config.* and tailwindcss-mangle.config.* in the target directory. With --workspace, it recursively scans sub-projects (excluding folders like node_modules, .git, and dist). Use --include / --exclude to control monorepo scanning ranges. It rewrites deprecated keys (for example registry.output -> registry.extract, registry.tailwind -> registry.tailwindcss) and prints a per-file change summary.

When writing files, migration uses a transactional strategy by default: if a later file write fails, already written migration files are rolled back to avoid partial updates. Use --backup-dir if you want explicit backup snapshots for audit/manual recovery. Use --report-file to keep a machine-readable migration artifact.

Migration reports now include envelope metadata: reportKind, schemaVersion, generatedAt, and tool (name / version). This metadata helps restore tooling validate report compatibility.

Restore options

Flag Description
--cwd <dir> Working directory used to resolve report and target paths.
--report-file <file> Migration report file path (defaults to .tw-patch/migrate-report.json).
--dry-run Preview restore targets without writing files.
--strict Fail when any backup file in the report is missing.
--json Print restore summary as JSON.

tw-patch restore validates report schema metadata when available. Reports with unsupported reportKind or newer schemaVersion are rejected to avoid unsafe restores. Legacy reports without metadata are still supported. With --json, restore output includes reportKind / reportSchemaVersion when report metadata is present.

Validate options

Flag Description
--cwd <dir> Working directory used to resolve report paths.
--report-file <file> Migration report file path (defaults to .tw-patch/migrate-report.json).
--strict Fail when any backup file in the report is missing.
--json Print validation result as JSON.

tw-patch validate performs migration report compatibility checks without writing restored files. It runs report schema validation and scans backup references in dry-run mode. On failure, validate uses dedicated exit codes for CI: 21 report incompatibility, 22 strict missing backups, 23 I/O errors, 24 unknown errors. With --json, validate emits a stable payload: success => { ok: true, ...restoreFields }, failure => { ok: false, reason, exitCode, message }.

Schemas are published at package subpaths: tailwindcss-patch/migration-report.schema.json, tailwindcss-patch/restore-result.schema.json, tailwindcss-patch/validate-result.schema.json. Programmatic consumers can also import report helpers/types from package entry: migrateConfigFiles, restoreConfigFiles, MIGRATION_REPORT_KIND, MIGRATION_REPORT_SCHEMA_VERSION, ConfigFileMigrationReport, VALIDATE_EXIT_CODES.

CI recipe

# 1) fail fast when workspace configs still need migration
pnpm dlx tw-patch migrate --workspace --check --report-file .tw-patch/migrate-report.json

# 2) validate report schema/backup references and keep machine-readable output
set +e
pnpm dlx tw-patch validate --report-file .tw-patch/migrate-report.json --strict --json > .tw-patch/validate-result.json
status=$?
set -e

case "$status" in
  0)  echo "validate ok" ;;
  21) echo "report schema/kind incompatible"; exit 1 ;;
  22) echo "missing backups under --strict"; exit 1 ;;
  23) echo "I/O failure while reading report/backups"; exit 1 ;;
  *)  echo "unknown validate failure"; exit "$status" ;;
esac

GitHub Actions templates:

  • single job: packages/tailwindcss-patch/examples/github-actions/validate-migration-report.yml
  • monorepo matrix shards (root/apps/packages): packages/tailwindcss-patch/examples/github-actions/validate-migration-report-matrix.yml
  • monorepo affected shards (PR diff-aware): packages/tailwindcss-patch/examples/github-actions/validate-migration-report-affected.yml
  • shared composite action (used by all templates): packages/tailwindcss-patch/examples/github-actions/actions/validate-migration-report/action.yml
  • affected-shard resolver script: packages/tailwindcss-patch/examples/github-actions/scripts/resolve-shards.mjs
  • resolver JSON contract schema: packages/tailwindcss-patch/examples/github-actions/resolve-shards-result.schema.json
  • resolver dispatch snapshot fixture: packages/tailwindcss-patch/examples/github-actions/resolve-shards-result.dispatch.snapshot.json

For the affected-shards template, you can customize shard matching and run-all triggers by adding .tw-patch/ci-shards.json in your repo. A sample config is available at packages/tailwindcss-patch/examples/github-actions/ci-shards.example.json.

The shared composite action now supports optional environment bootstrap inputs: setup-pnpm, setup-node, node-version, cache-dependency-path, install-deps, and install-command. This lets you choose between action-managed setup or workflow-managed setup depending on your CI strategy.

CI copy checklist

  1. Pick one workflow template based on your repository shape: validate-migration-report.yml (single job), validate-migration-report-matrix.yml (fixed shards), or validate-migration-report-affected.yml (PR diff-aware shards).
  2. Always copy the shared composite action: packages/tailwindcss-patch/examples/github-actions/actions/validate-migration-report/action.yml.
  3. If you use the affected-shards template, also copy: packages/tailwindcss-patch/examples/github-actions/scripts/resolve-shards.mjs, packages/tailwindcss-patch/examples/github-actions/resolve-shards-result.schema.json, packages/tailwindcss-patch/examples/github-actions/resolve-shards-result.dispatch.snapshot.json.
  4. If your workspace paths differ from defaults, add .tw-patch/ci-shards.json (based on ci-shards.example.json) and adjust shard patterns/report files.
  5. Confirm the composite action inputs match your runner setup: action-managed setup (setup-pnpm/setup-node/install-deps) or pre-provisioned setup (false + custom install command).
  6. Keep permissions.contents: read and ensure pnpm-lock.yaml path matches cache-dependency-path.

CI troubleshooting

  • uses: ./.../validate-migration-report not found: the workflow references a local action path; copy the action directory with the workflow file.
  • No affected shards for migration report validation. in PR: either files are outside configured shard patterns or base diff resolution returned empty; verify .tw-patch/ci-shards.json and PR base branch.
  • Unknown scope in composite action: scope currently accepts only all, root, apps, packages unless you customize action logic.
  • validate exits 21/22/23: 21 incompatible report schema/kind, 22 missing backups under --strict, 23 report/backup I/O failure.
  • Resolver snapshot diff failure in workflow-lint: you changed resolver contract behavior; update both schema/snapshot fixtures and corresponding tests in one commit.

Token report options

Flag Description
--cwd <dir> Use a different working directory when loading configuration.
--output <file> Override the token report target file (defaults to .tw-patch/tw-token-report.json).
--format <json|lines|grouped-json> Choose between a JSON payload (default), newline summaries, or JSON grouped by file path.
--group-key <relative|absolute> Control grouped-json keys (defaults to relative paths).
--no-write Skip writing to disk and only print a preview.

Programmatic API

import { TailwindcssPatcher } from 'tailwindcss-patch'

const patcher = new TailwindcssPatcher({
  projectRoot: process.cwd(),
  cache: {
    enabled: true,
    dir: '.tw-patch/cache',
    strategy: 'merge',
    driver: 'file',
  },
  extract: {
    write: true,
    file: '.tw-patch/tw-class-list.json',
    format: 'json',
  },
  apply: {
    overwrite: true,
    exposeContext: { refProperty: 'runtimeContexts' },
    extendLengthUnits: {
      units: ['rpx'],
    },
  },
  tailwindcss: {
    version: 4,
    v4: {
      base: './src',
      cssEntries: ['dist/tailwind.css'],
    },
  },
})

await patcher.patch()
const { classList, filename } = await patcher.extract()
const tokenReport = await patcher.collectContentTokens()
console.log(tokenReport.entries[0]) // { rawCandidate, file, line, column, ... }
const groupedTokens = await patcher.collectContentTokensByFile()
console.log(groupedTokens['src/button.tsx'][0].rawCandidate)
// Preserve absolute file paths:
// await patcher.collectContentTokensByFile({ key: 'absolute', stripAbsolutePaths: false })
const patchStatus = await patcher.getPatchStatus()
console.log(patchStatus.entries)

The constructor accepts either the new object shown above or historical shapes. Conversions happen internally so existing configs remain backwards compatible.

Deprecated fields kept temporarily (to be removed in the next major): cwd, overwrite, tailwind, features, output.

Migration mapping:

  • cwd -> projectRoot
  • overwrite -> apply.overwrite
  • tailwind -> tailwindcss
  • features -> apply
  • output -> extract

When deprecated fields are detected at runtime, normalizeOptions logs a one-time warning to help migration.

Use cache.driver to switch between the default file-backed cache, an in-memory cache (memory), or a no-op cache (noop) when filesystem permissions are restricted.

Cache governance (schema v2)

tailwindcss-patch now isolates cache entries by context fingerprint to prevent cross-project pollution in monorepos.

  • Cache file format uses an indexed schema (schemaVersion: 2) with per-context entries.
  • A cache hit requires both fingerprint and metadata consistency.
  • Legacy array caches are read safely and treated as misses, then lazily rebuilt on write.
  • Writes are protected by lock file + atomic temp-file rename to avoid concurrent corruption.

Fingerprint components:

  • realpath-normalized process.cwd()
  • realpath-normalized project root / cache cwd
  • Tailwind config absolute path (if found) + config mtime
  • Tailwind package root + version
  • tailwindcss-patch package version
  • deterministic hash of key patch options (stable key ordering)

The fingerprint is computed once in the patcher constructor and reused during all cache operations.

Clearing cache explicitly

// default: clear current context only
const current = await patcher.clearCache()
// => { scope: 'current', filesRemoved, entriesRemoved, contextsRemoved }

// clear all contexts from the cache index
const all = await patcher.clearCache({ scope: 'all' })

Debug observability:

  • cache hit logs include fingerprint + schema
  • cache miss logs include miss reason and mismatch details (config/version/path/options)

Helper utilities

  • normalizeOptions – normalise raw user input to the runtime shape.
  • CacheStore – read/write class caches (file, memory, or noop drivers) respecting merge or overwrite semantics.
  • extractProjectCandidatesWithPositions – gather Tailwind tokens for every configured source file with location metadata.
  • groupTokensByFile – convert a token report into a { [filePath]: TailwindTokenLocation[] } map.
  • extractValidCandidates – scan Tailwind v4 CSS/content sources with the Tailwind Oxide scanner.
  • runTailwindBuild – run the Tailwind PostCSS plugin for v2/v3 projects to prime runtime contexts.

All helpers are exported from the package root for direct consumption in custom tooling.

Configuration Example

// tailwindcss-patch.config.ts
import { defineConfig } from 'tailwindcss-patch'

export default defineConfig({
  registry: {
    projectRoot: '.',
    extract: {
      file: '.tw-patch/tw-class-list.json',
      removeUniversalSelector: true,
      format: 'json',
    },
    tailwindcss: {
      version: 4,
      v4: {
        cssEntries: ['dist/tailwind.css'],
        sources: [{ base: 'src', pattern: '**/*.{html,tsx}', negated: false }],
      },
    },
    apply: {
      overwrite: true,
      exposeContext: true,
      extendLengthUnits: {
        units: ['rpx'],
      },
    },
  },
})

defineConfig supports both modern registry fields (projectRoot, tailwindcss, apply, extract) and historical keys. The patcher normalizer handles both and always prefers modern fields when both are present.

Migration

Breaking changes, module moves, and upgrade paths are documented in MIGRATION.md. Review it when updating from tailwindcss-patch v7.x or earlier.

License

MIT © ice breaker