# 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.
- Generate Tailwind v3/v4 CSS, or fully custom CSS, directly from in-memory tokens and source snippets.

## Installation

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

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

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

## CLI Usage

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

```bash
# 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:

```ts
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.

```ts
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.      |

### Style generation

Use the style generator when you want `tailwindcss-patch` to act as the CSS engine and drive your own output format. Tailwind v4 uses `@tailwindcss/node`; Tailwind v3 uses Tailwind v3's internal JIT engine (`createContext` + `generateRules`) without running the Tailwind PostCSS plugin.

```ts
import { writeFile } from 'node:fs/promises'
import { generateTailwindStyle } from 'tailwindcss-patch'

const result = await generateTailwindStyle({
  version: 4,
  css: '@import "tailwindcss";',
  candidates: ['text-red-500'],
  sources: [
    {
      content: '<view class="min-h-screen rounded-[18px]"></view>',
      extension: 'tsx',
    },
  ],
})

await writeFile('dist/style.css', result.css)
await writeFile('dist/tokens.json', JSON.stringify([...result.tokens], null, 2))
```

Generate Tailwind v3 CSS from the same candidate pipeline:

```ts
import { generateTailwindStyle } from 'tailwindcss-patch'

const result = await generateTailwindStyle({
  version: 3,
  candidates: ['text-red-500'],
  sources: [
    {
      content: '<view class="min-h-screen rounded-[18px]"></view>',
      extension: 'wxml',
    },
  ],
  config: {
    corePlugins: {
      preflight: false,
    },
  },
})
```

Or bypass Tailwind CSS output entirely and provide your own CSS generator:

```ts
import { escapeCssClassName, generateTailwindStyle } from 'tailwindcss-patch'

const result = await generateTailwindStyle({
  version: 'custom',
  candidates: ['token-card'],
  sources: [
    {
      content: '<view class="rounded-[18px]"></view>',
      extension: 'wxml',
    },
  ],
  generate({ tokens }) {
    return [...tokens]
      .map(token => `.${escapeCssClassName(token)}{--tw-token:"${token}"}`)
      .join('\n')
  },
})
```

The CLI loads `tailwindcss-patch.config.ts` via `@tailwindcss-mangle/config`. v9 expects the modern `registry` shape; use `tw-patch migrate` before upgrading if your config still uses deprecated keys.

### Public Type Names

The current alpha exports the modern option type names only:

```ts
import type {
  ApplyOptions,
  CacheOptions,
  ExtractOptions,
  TailwindCssOptions,
  TailwindCssPatchOptions,
  TailwindV2Options,
  TailwindV3Options,
  TailwindV4Options,
  NormalizedTailwindCssPatchOptions,
} from 'tailwindcss-patch'
```

Older aliases such as `TailwindcssPatchOptions`, `TailwindLocatorOptions`, and `TailwindTargetOptions` are intentionally removed in the alpha line.

### v9 upgrade flow

1. Run `pnpm dlx tw-patch migrate --dry-run` to preview required config rewrites.
2. Apply the migration, or rewrite the config manually to modern `registry` fields.
3. Confirm every config sets `registry.tailwindcss.version` explicitly.
4. Upgrade to v9 and rerun `tw-patch install` / `tw-patch extract` in your project.

Legacy to v9 example:

```ts
// before
export default defineConfig({
  registry: {
    output: {
      file: '.tw-patch/tw-class-list.json',
    },
    tailwind: {
      package: 'tailwindcss',
      classic: {
        cwd: 'apps/web',
      },
    },
  },
})

// after
export default defineConfig({
  registry: {
    extract: {
      file: '.tw-patch/tw-class-list.json',
    },
    tailwindcss: {
      version: 3,
      packageName: 'tailwindcss',
      v3: {
        cwd: 'apps/web',
      },
    },
  },
})
```

### 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

```bash
# 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

```ts
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 the modern object shown above only in v9.

Migration mapping:

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

Deprecated fields are rejected at runtime in v9. Run `tw-patch migrate --dry-run` to preview required rewrites.

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

```ts
// 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

```ts
// 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](./MIGRATION.md). Review it when updating from tailwindcss-patch v7.x or earlier.

## License

MIT © ice breaker
