Skip to content

Latest commit

 

History

History
1162 lines (922 loc) · 41.7 KB

File metadata and controls

1162 lines (922 loc) · 41.7 KB

TailwindPHP - Development Guide

A 1:1 port of TailwindCSS 4.x to PHP. See README.md for scope and features.

Table of Contents


Critical Rules for LLMs

1. Never Modify Test Files to Make Tests Pass

FORBIDDEN: Changing assertions, expected values, or normalization logic in *.test.php files to make failing tests pass.

ALLOWED: Only fix test infrastructure bugs (parsing issues, file loading) - never change expected outputs.

2. Always Fix at the Source

When a test fails, the fix belongs in the source code (src/*.php), not in the test file. The tests define the contract - we must match TailwindCSS output exactly.

3. LightningCSS Functionality

TailwindCSS uses lightningcss (Rust) for CSS transformations. Since we can't use Rust in PHP, equivalent functionality goes in:

src/_tailwindphp/LightningCss.php

This includes:

  • CSS nesting transformation (flattening & selectors)
  • @media query hoisting
  • calc() simplification
  • Leading zero removal (0.5.5)
  • Transform function spacing
  • Grid value normalization

4. The _tailwindphp Directory

src/_tailwindphp/ contains PHP-specific helpers that are NOT part of the TailwindCSS port:

  • LightningCss.php - CSS optimizations (lightningcss equivalent)
  • CssMinifier.php - CSS minification
  • lib/ - Companion library ports (clsx, tailwind-merge, cva)

Everything else in src/ should mirror TailwindCSS structure.

5. Companion Libraries (clsx, tailwind-merge)

We include PHP ports of clsx and tailwind-merge because they are essential companion libraries in the Tailwind ecosystem:

  • clsx - Conditional class name construction (used in virtually every Tailwind project)
  • tailwind-merge - Intelligent class conflict resolution (p-2 p-4p-4)
  • cn() - Combines both (the pattern popularized by shadcn/ui)

By including these, TailwindPHP provides a complete Tailwind development experience without requiring Node.js for anything.

Important for LLMs: These libraries live in src/_tailwindphp/lib/ with their own namespace (TailwindPHP\Lib\*) to clearly separate them from the core TailwindCSS port. The public API functions (cn, clsx, twMerge, twJoin) are exposed in src/index.php under the main TailwindPHP namespace.

6. Plugin System

TailwindPHP includes PHP ports of official TailwindCSS plugins:

  • @tailwindcss/typography - The prose class for beautiful typographic defaults
  • @tailwindcss/forms - Form element reset and styling utilities

These are 1:1 ports following the exact same logic as the JavaScript originals. The plugin system architecture:

src/plugin.php                    # Core plugin system
├── PluginInterface               # Contract for plugins (getName, __invoke, getThemeExtensions)
├── PluginAPI                     # API passed to plugins (addBase, addUtilities, addComponents, etc.)
└── PluginManager                 # Registry and execution of plugins

src/plugin/plugins/
├── typography-plugin.php         # @tailwindcss/typography port
└── forms-plugin.php              # @tailwindcss/forms port

test-coverage/plugins/
├── typography/                   # Extracted typography tests
│   ├── summary.json
│   └── tests/*.json
└── extract-typography-tests.php  # Extraction script

PluginAPI Methods (same as TailwindCSS):

  • addBase(array $css) - Add base styles
  • addUtilities(array $utilities) - Add static utilities
  • matchUtilities(array $utilities, array $options) - Add functional utilities with values
  • addComponents(array $components) - Add component classes
  • matchComponents(array $components, array $options) - Add functional components
  • addVariant(string $name, string|array $variant) - Add custom variants
  • matchVariant(string $name, callable $callback, array $options) - Add functional variants
  • theme(string $path, mixed $default) - Access theme values

Usage in CSS:

@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms" {
    strategy: "class";
}
@import "tailwindcss/utilities.css";

Important for LLMs: The plugin implementations are PHP ports, not JavaScript execution. They follow the same logic and produce the same output, but are implemented natively in PHP. Tests are extracted from the reference JavaScript test files where available.

Creating Custom Plugins:

use TailwindPHP\Plugin\PluginInterface;
use TailwindPHP\Plugin\PluginAPI;

class MyPlugin implements PluginInterface
{
    public function getName(): string
    {
        return 'my-plugin';
    }

    public function __invoke(PluginAPI $api, array $options = []): void
    {
        // Add static utilities
        $api->addUtilities([
            '.btn' => ['padding' => '0.5rem 1rem', 'border-radius' => '0.25rem'],
        ]);

        // Add functional utilities (generates .tab-1, .tab-2, .tab-4, .tab-8)
        $api->matchUtilities(
            ['tab' => fn($value) => ['tab-size' => $value]],
            ['values' => ['1' => '1', '2' => '2', '4' => '4', '8' => '8']]
        );

        // Add components
        $api->addComponents(['.card' => ['background' => 'white', 'padding' => '1rem']]);

        // Add variants
        $api->addVariant('hocus', '&:hover, &:focus');

        // Access theme
        $color = $api->theme('colors.blue.500', '#3b82f6');
    }

    public function getThemeExtensions(array $options = []): array
    {
        return []; // Theme additions
    }
}

// Register and use
registerPlugin(new MyPlugin());
$css = tw::generate($html, '@plugin "my-plugin"; @import "tailwindcss/utilities.css";');

7. Performance Optimizations

While this is a 1:1 port focused on correctness, PHP-specific optimizations are applied where they improve performance without changing output. These are marked with @port-deviation:performance in file headers.

Current optimizations:

  • ast.php toCss(): Array accumulation + implode instead of string concatenation, pre-computed indent strings, standalone function instead of closure (~50% faster)
  • css-parser.php parse(): Direct character comparison instead of ord() calls, tracked buffer lengths instead of strlen() calls (~20-30% faster)

Guidelines for performance work:

  1. Always preserve identical output - tests must pass
  2. Document with @port-deviation:performance in file header
  3. Run benchmarks (composer bench) to verify improvement
  4. Focus on hot paths identified by profiling

8. Code Quality Tools

The project uses Pint (formatting) and PHPStan (static analysis):

composer lint      # Check formatting (Pint --test)
composer format    # Fix formatting (Pint)
composer analyse   # Static analysis (PHPStan level 4)
composer quality   # Run both lint + analyse

inline0 profile note: TailwindPHP is registered in _cp as a Tailwind reproduction special case. Its colocated .test.php files and Pint workflow are intentional because they preserve upstream Tailwind traceability; do not move tests into tests/Unit / tests/Integration or replace Pint with PHPCS for profile alignment.

PHPStan Notes:

  • Level 4 is used (higher levels conflict with function-based architecture)
  • Some patterns are intentionally ignored (dynamic arrays, cross-file functions)
  • Source files are analyzed, test files are excluded

Test System

Test Types

  1. Extraction-based tests (test-coverage/ directory)

    • Tests extracted from TailwindCSS TypeScript source using extraction scripts
    • Stored as .json files in test-coverage/*/tests/
    • PHP test files load these via DataProvider at runtime
    • Used for complex tests (utilities, variants, integration, css-functions, ui.spec)
    • Must pass completely - no exceptions
  2. Unit test ports (src/*.test.php)

    • Direct PHP ports of simpler TypeScript test files
    • NOT extracted - manually ported from corresponding .test.ts files
    • Used for: AST, parsing, escaping, candidate parsing, value parsing, etc.
    • Examples: ast.test.phpast.test.ts, escape.test.phpescape.test.ts
    • Test structure and assertions mirror the TypeScript originals

Test Coverage Structure

test-coverage/
├── utilities/tests/     # From utilities.test.ts
├── variants/tests/      # From variants.test.ts
├── index/tests/         # From index.test.ts (JSON format)
├── css-functions/tests/ # From css-functions.test.ts
├── candidate/tests/     # From candidate.test.ts
├── ui-spec/tests/       # From tests/ui.spec.ts (Playwright browser tests)
└── extract-*.php        # Extraction scripts

Extraction Scripts

Located in test-coverage/:

  • extract-run-tests.php - Extracts utilities/variants tests
  • extract-index-tests.php - Extracts index.test.ts to JSON
  • extract-css-functions-tests.php - Extracts css-functions tests
  • extract-candidate-tests.php - Extracts candidate tests
  • extract-ui-spec-tests.php - Extracts ui.spec.ts browser tests

Library Test Extraction (test-coverage/lib/)

  • extract-clsx-tests.php - Extracts clsx tests from reference/clsx/test/
  • extract-tailwind-merge-tests.php - Extracts tailwind-merge tests from reference/tailwind-merge/tests/
  • extract-cva-tests.php - Extracts CVA tests from reference/cva/packages/cva/src/
  • verify-test-counts.php - Verifies PHP tests match reference test counts

Commands

# Extract tests from TypeScript source
composer extract

# Extract companion library tests
composer extract-libs

# Extract all tests (core + libs)
composer extract-all

# Run all tests
composer test

# Run library tests only
composer test-libs

# Extract and run tests
composer extract-and-test

# Run specific test file
./vendor/bin/phpunit src/utilities.test.php

# Run tests matching a pattern
./vendor/bin/phpunit --filter="hover"

# Check current versions of all references
composer versions

# Update TailwindCSS reference
composer update-tailwind

# Update companion library references
composer update-libs

Development Workflow

Standard Workflow

# 1. Extract latest tests from TypeScript
composer extract

# 2. Run tests
composer test

# 3. If tests fail, fix in src/*.php (NOT in test files)

# 4. When all tests pass, commit and push
git add -A && git commit -m "message" && git push

When Tests Fail

  1. Read the failure message - understand what output differs
  2. Check the source - find the relevant PHP implementation
  3. Compare with TypeScript - reference reference/tailwindcss/
  4. Fix the source code - make PHP output match TypeScript
  5. Re-run tests - verify the fix

Priority Order

  1. First ensure utilities.test.php passes (364 tests)
  2. Then variants.test.php (144 tests)
  3. Then index.test.php (integration tests)
  4. Then remaining test files

Project Structure

tailwindphp/
├── bin/
│   └── tailwindphp             # CLI entry point
│
├── src/
│   ├── Cli/                    # CLI (1:1 port of @tailwindcss/cli)
│   │   ├── Application.php     # Main CLI application
│   │   └── Console/            # Input/Output handling
│   │       ├── Input.php
│   │       └── Output.php
│   │
│   ├── _tailwindphp/           # PHP-specific (NOT part of port)
│   │   ├── LightningCss.php    # CSS transformations
│   │   ├── CssMinifier.php     # CSS minification
│   │   └── lib/                # Companion library ports
│   │       ├── clsx/           # clsx port (27 tests)
│   │       │   ├── clsx.php
│   │       │   └── clsx.test.php
│   │       ├── tailwind-merge/ # tailwind-merge port (52 tests)
│   │       │   ├── index.php   # Main entry, cn(), twMerge(), twJoin()
│   │       │   ├── default-config.php  # tailwind-merge class group config
│   │       │   ├── merger.php  # ClassNameMerger logic
│   │       │   ├── lru-cache.php
│   │       │   └── tailwind_merge.test.php
│   │       └── cva/            # CVA port (50 tests)
│   │           ├── cva.php     # cva(), cx(), compose(), defineConfig()
│   │           └── cva.test.php
│   │
│   ├── plugin/                 # Plugin system
│   │   └── plugins/            # Built-in plugin implementations
│   │       ├── typography-plugin.php  # @tailwindcss/typography port
│   │       ├── typography_plugin.test.php
│   │       └── forms-plugin.php       # @tailwindcss/forms port
│   │
│   ├── utilities/              # Split from utilities.ts
│   │   ├── accessibility.php
│   │   ├── backgrounds.php
│   │   ├── borders.php
│   │   └── ... (15 files)
│   │
│   ├── utils/                  # Helper functions
│   │
│   ├── index.php               # Main entry + public API (cn, cva, merge, join)
│   ├── plugin.php              # Plugin system (PluginInterface, PluginAPI, PluginManager)
│   ├── *.php                   # Core implementation
│   └── *.test.php              # Unit test files
│
├── tests/
│   ├── TestHelper.php          # Test utilities
│   └── ui_spec.test.php        # Browser/integration tests (from tests/ui.spec.ts)
│
├── test-coverage/
│   ├── */tests/                # Extracted test data
│   ├── lib/                    # Library test extraction scripts
│   │   ├── extract-clsx-tests.php
│   │   ├── extract-tailwind-merge-tests.php
│   │   ├── extract-cva-tests.php
│   │   └── verify-test-counts.php
│   └── extract-*.php           # Core extraction scripts
│
├── resources/
│   ├── theme.css               # Default Tailwind theme (copied from reference)
│   ├── preflight.css           # CSS reset (copied from reference)
│   ├── utilities.css           # Utilities directive
│   └── index.css               # Main entry point
│
├── reference/
│   ├── tailwindcss/            # Git submodule - TailwindCSS source
│   ├── tailwindcss-typography/ # Git submodule - @tailwindcss/typography source
│   ├── tailwindcss-forms/      # Git submodule - @tailwindcss/forms source
│   ├── clsx/                   # Git submodule - clsx source
│   ├── tailwind-merge/         # Git submodule - tailwind-merge source
│   └── cva/                    # Git submodule - CVA source
│
├── scripts/
│   ├── update-tailwind.php     # Update TailwindCSS reference + copy CSS files
│   └── update-libs.php         # Update companion library references
│
└── CLAUDE.md                   # This file

Key Files

File Purpose
src/index.php Main entry point, compile(), cn(), variants(), etc.
src/at-import.php @import resolution and substitution
tests/EdgeCasesTest.php Edge case tests for complex scenarios
src/utilities.php Utility registration and compilation
src/variants.php Variant handling (hover, focus, etc.)
src/compile.php Candidate to CSS compilation
src/design-system.php Central registry
src/theme.php Theme value management
src/ast.php AST nodes and toCss()
src/plugin.php Plugin system (PluginInterface, PluginAPI, PluginManager)
src/plugin/plugins/typography-plugin.php @tailwindcss/typography port
src/plugin/plugins/forms-plugin.php @tailwindcss/forms port
src/_tailwindphp/LightningCss.php CSS optimizations (for test parity)
src/_tailwindphp/CssMinifier.php CSS minification
src/_tailwindphp/lib/clsx/clsx.php clsx implementation
src/_tailwindphp/lib/tailwind-merge/index.php tailwind-merge + cn() implementation
src/_tailwindphp/lib/cva/cva.php CVA (class variance authority) implementation
tests/ImportPathsTest.php Import paths feature tests
tests/TestHelper.php TestHelper::run() for tests
scripts/update-libs.php Update companion library references

Public API

Core CSS Compilation

use TailwindPHP\tw;

// Generate CSS from HTML
$css = tw::generate('<div class="flex p-4">');

// With custom CSS input
$css = tw::generate($html, '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');

// With file-based imports
$css = tw::generate([
    'content' => '<div class="flex p-4 btn">',
    'importPaths' => '/path/to/styles.css',
]);

// With multiple import paths (files and directories)
$css = tw::generate([
    'content' => $html,
    'importPaths' => [
        '/path/to/base.css',
        '/path/to/components/',  // Directory loads all .css files
    ],
]);

// Combine inline CSS with file imports
$css = tw::generate([
    'content' => $html,
    'css' => '.custom { color: red; }',
    'importPaths' => '/path/to/styles.css',
]);

// Custom resolver (for virtual file systems, databases, etc.)
$css = tw::generate([
    'content' => $html,
    'importPaths' => function (?string $uri, ?string $fromFile): ?string {
        if ($uri === null) return '@import "tailwindcss";';
        // Return CSS content for uri, or null to skip
        return null;
    },
]);

// Extract class candidates from content
$classes = tw::extractCandidates('<div class="flex p-4">');

// Minify during generation
$css = tw::generate([
    'content' => '<div class="flex p-4">',
    'minify' => true,
]);

// Or minify separately
$css = tw::generate('<div class="flex p-4">');
$minified = tw::minify($css);

// Enable caching (default temp directory)
$css = tw::generate([
    'content' => '<div class="flex p-4">',
    'cache' => true,
]);

// Cache to custom directory with TTL
$css = tw::generate([
    'content' => '<div class="flex p-4">',
    'cache' => '/path/to/cache',
    'cacheTtl' => 3600, // 1 hour
]);

// Clear cache
tw::clearCache(); // Default directory
tw::clearCache('/path/to/cache'); // Custom directory

TailwindCompiler Instance

Create a reusable compiler instance for multiple operations:

use TailwindPHP\tw;

// Create compiler with default Tailwind
$compiler = tw::compile();

// Create compiler with custom CSS
$compiler = tw::compile('@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');

// Extract candidates and generate CSS
$candidates = $compiler->extractCandidates('<div class="flex p-4 bg-brand">');
$css = $compiler->css($candidates);

// Or minify the output
$minified = $compiler->minify($css);

CSS Property Inspection

Get raw or computed CSS properties for classes:

use TailwindPHP\tw;

// Raw properties (unresolved CSS variables)
tw::properties('p-4');
// ['padding' => 'calc(var(--spacing) * 4)']

tw::properties(['flex', 'items-center', 'p-4']);
// ['display' => 'flex', 'align-items' => 'center', 'padding' => 'calc(var(--spacing) * 4)']

// Computed properties (resolved values)
tw::computedProperties('p-4');
// ['padding' => '1rem']

tw::computedProperties('text-blue-500');
// ['color' => 'oklch(.546 .245 262.881)']

// Single value extraction
tw::value('p-4');           // 'calc(var(--spacing) * 4)'
tw::computedValue('p-4');   // '1rem'

// From compiler instance
$compiler = tw::compile();
$compiler->properties('p-4');
$compiler->computedProperties('p-4');
$compiler->value('p-4');
$compiler->computedValue('p-4');

Input Formats

All static methods accept multiple input formats:

// Format 1: String only
tw::generate('<div class="flex">');
tw::properties('p-4');

// Format 2: String + CSS string
tw::generate('<div class="flex">', '@import "tailwindcss"; @theme { ... }');
tw::properties('bg-brand', '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');

// Format 3: Array with 'content' and optional 'css'
tw::generate(['content' => '<div class="flex">', 'css' => '@import "tailwindcss";']);
tw::properties(['content' => 'p-4', 'css' => '@import "tailwindcss";']);

Class Name Utilities

use function TailwindPHP\cn;
use function TailwindPHP\merge;
use function TailwindPHP\join;

// cn() - Recommended: conditional classes + conflict resolution
cn('px-2 py-1', 'px-4');                    // => 'py-1 px-4'
cn('btn', ['btn-primary' => $active]);      // => 'btn btn-primary' (if $active)

// merge() - Conflict resolution only
merge('px-2 py-1', 'px-4');                 // => 'py-1 px-4'
merge('hover:bg-red-500', 'hover:bg-blue-500'); // => 'hover:bg-blue-500'

// join() - Simple joining without conflict resolution
join('foo', 'bar', null);                   // => 'foo bar'

// React-style component using cn()
function Card(array $props = []): string {
    $class = cn(
        'rounded-lg border bg-card text-card-foreground shadow-sm',
        $props['class'] ?? null
    );
    return "<div class=\"{$class}\">" . ($props['children'] ?? '') . "</div>";
}

Card(['class' => 'p-6', 'children' => 'Content']);

Variants (CVA Port)

use function TailwindPHP\variants;
use function TailwindPHP\compose;

// variants() - Create component variants (CVA port)
$button = variants([
    'base' => 'btn font-semibold border rounded',
    'variants' => [
        'intent' => [
            'primary' => 'bg-blue-500 text-white',
            'secondary' => 'bg-gray-200 text-gray-800',
        ],
        'size' => [
            'sm' => 'text-sm px-2 py-1',
            'md' => 'text-base px-4 py-2',
        ],
    ],
    'defaultVariants' => [
        'intent' => 'primary',
        'size' => 'md',
    ],
]);

// Usage (React-style single props object)
$button();                                              // => 'btn font-semibold ... bg-blue-500 text-white text-base px-4 py-2'
$button(['intent' => 'secondary']);                     // => 'btn font-semibold ... bg-gray-200 text-gray-800 text-base px-4 py-2'
$button(['size' => 'sm', 'class' => 'my-class']);       // Appends custom classes

// Example component using variants() + cn() for class extension
function Button(array $props = []): string {
    static $styles = null;
    $styles ??= variants([
        'base' => 'inline-flex items-center justify-center rounded-md font-medium',
        'variants' => [
            'variant' => [
                'default' => 'bg-primary text-primary-foreground hover:bg-primary/90',
                'outline' => 'border border-input bg-background hover:bg-accent',
                'ghost' => 'hover:bg-accent hover:text-accent-foreground',
            ],
            'size' => [
                'default' => 'h-10 px-4 py-2',
                'sm' => 'h-9 px-3',
                'lg' => 'h-11 px-8',
            ],
        ],
        'defaultVariants' => ['variant' => 'default', 'size' => 'default'],
    ]);

    // cn() merges variant output with custom classes, resolving conflicts
    $class = cn($styles($props), $props['class'] ?? null);
    $text = $props['children'] ?? 'Button';
    return "<button class=\"{$class}\">{$text}</button>";
}

// Custom classes override variant defaults via cn()
Button(['variant' => 'outline', 'size' => 'sm', 'class' => 'mt-4 px-8']);

// compose() - Merge multiple variant components
$box = variants(['variants' => ['shadow' => ['sm' => 'shadow-sm', 'md' => 'shadow-md']]]);
$stack = variants(['variants' => ['gap' => ['1' => 'gap-1', '2' => 'gap-2']]]);
$card = compose($box, $stack);
$card(['shadow' => 'md', 'gap' => '2']); // => 'shadow-md gap-2'

Common Patterns

Adding LightningCSS Functionality

When tests fail because of CSS transformation differences:

// In src/_tailwindphp/LightningCss.php

public static function someTransformation(string $value): string
{
    // Implement the transformation that lightningcss does
    return $transformedValue;
}

Then call it from the appropriate place in the pipeline.

Test File Structure

Tests that load from test-coverage/ follow this pattern:

class SomeTest extends TestCase
{
    private static array $testCases = [];

    private static function loadTestCases(): void
    {
        // Load from test-coverage/*/tests/
    }

    public static function dataProvider(): array
    {
        self::loadTestCases();
        return self::$testCases;
    }

    #[DataProvider('dataProvider')]
    public function test_case(array $test): void
    {
        // Run test and compare output
    }
}

CSS Comparison

When comparing CSS output:

  1. Normalize whitespace
  2. Normalize leading zeros
  3. Handle selector escaping differences
  4. Don't break pseudo-selectors (:hover, :root)

Updating Library References

When clsx or tailwind-merge releases a new version:

# Check current versions
composer versions

# Update all libraries to latest
composer update-libs

# Update specific library
php scripts/update-libs.php clsx
php scripts/update-libs.php tailwind-merge

The update script will:

  1. Fetch the latest tag from the reference repo
  2. Checkout that tag
  3. Re-extract tests from the new source
  4. Update README badges automatically
  5. Run tests to verify compatibility

If tests fail after updating, it means the library made breaking changes that require updating the PHP implementation.

Library Test Extraction

Tests for companion libraries are extracted from their original JavaScript/TypeScript test files:

clsx: Tests extracted from reference/clsx/test/index.js and classnames.js

  • Parser handles JavaScript function call syntax
  • Converts JS truthiness semantics to PHP (empty arrays are truthy in JS)
  • Strips JS comments before parsing

tailwind-merge: Tests extracted from reference/tailwind-merge/tests/*.test.ts

  • Parser handles TypeScript test syntax (test(), expect().toBe())
  • Processes escape sequences in string literals
  • Some tests N/A (require custom config, extendTailwindMerge)

Debugging Tips

Check TypeScript Reference

# The original TailwindCSS source is in:
reference/tailwindcss/packages/tailwindcss/src/

Run Single Test

./vendor/bin/phpunit --filter="test_name_here"

Verbose Output

./vendor/bin/phpunit --filter="test" --testdox

Print Debug Info

// In test file
fwrite(STDERR, "Debug: " . print_r($value, true) . "\n");

Current Status

Total: 4,427 tests (all passing)

Reference: TailwindCSS v4.3.3 (see composer versions).

Core Tests (extracted from TypeScript test suites)

The extractors handle describe()-nested tests and every run() call shape of the v4.3.3 test helper (inline arrays, Prettier-multiline calls, css template second arguments, template-variable bindings, and the { polyfills: Polyfills.None } options argument).

Test File Status Tests
utilities.test.php 706
variants.test.php 164
index.test.php 148 (file-system/JS-runtime cases N/A)
css_functions.test.php 64 (7 N/A for JS tooling)
ui_spec.test.php 68

Plugin Tests (src/plugin/)

Test File Status Tests
plugin.test.php 9 (core plugin functionality)
typography_plugin.test.php 16 (13 N/A - v3-specific behavior)
forms_plugin.test.php 29

Plugin System:

  • plugin.php - Core plugin system (PluginInterface, PluginAPI, PluginManager)
  • typography-plugin.php - @tailwindcss/typography port
  • forms-plugin.php - @tailwindcss/forms port

Typography tests are extracted from reference/tailwindcss-typography/src/index.test.js. Tests marked N/A use v3-specific behavior (dark mode via .dark selector, responsive variants) that differs in v4.

Library Tests (src/_tailwindphp/lib/)

PHP ports of utility libraries with tests extracted from their reference implementations:

Library Test File Status Tests Reference
clsx clsx.test.php 27 27/27 (100%)
tailwind-merge tailwind_merge.test.php 52 52/67 applicable (78%)
cva cva.test.php 50 50/60 extracted

clsx - Class name string builder (TailwindPHP\Lib\Clsx\clsx)

tailwind-merge - Tailwind class conflict resolver (TailwindPHP\Lib\TailwindMerge\twMerge)

  • Reference: https://github.com/dcastil/tailwind-merge
  • 52 tests from 14 applicable test files
  • 32 tests N/A (require custom config, extendTailwindMerge, etc.)
  • Includes cn() function that combines clsx + twMerge

cva - Class Variance Authority (TailwindPHP\Lib\Cva\cva)

  • Reference: https://github.com/joe-bell/cva
  • 50 tests for core functionality (cx, cva, compose, defineConfig)
  • Declarative component variant management with base, variants, compoundVariants, defaultVariants

CSS Minifier Tests (src/_tailwindphp/)

Test File Status Tests
CssMinifier.test.php 17

Tests cover comment removal, whitespace collapsing, hex color shortening, zero unit removal, font-weight shortening, empty rule removal, and public API integration.

API Coverage Tests (tests/tailwindphp/)

Test File Status Tests
UtilitiesTest.php 904
ModifiersTest.php 338
VariantsTest.php 282
DirectivesTest.php 189
PluginsTest.php 61
ApiTest.php 140

These tests provide exhaustive coverage of the TailwindPHP public API including:

  • All utility classes with various values and modifiers
  • Color opacity combinations (5-95 in increments of 5)
  • All responsive, state, and pseudo-class variants
  • Deep variant stacking (3-4 levels)
  • Arbitrary values for all utility types
  • Container queries, aria/data attributes
  • @apply, @theme, @utility, @layer directives
  • @import virtual modules (tailwindcss/preflight, tailwindcss/theme, tailwindcss/utilities)
  • @layer order declarations and layer() modifiers
  • preflight option for easy CSS reset inclusion
  • @plugin directive and plugin system (typography, forms)
  • tw::generate(), tw::compile(), tw::properties(), tw::computedProperties(), tw::value(), tw::computedValue()
  • TailwindCompiler instance methods for reusable compilation
  • All input formats (string, string+css, array with content/css keys)

Unit Tests (ported from TypeScript)

Test File Status Tests
css_parser.test.php 70
candidate.test.php 66
decode_arbitrary_value.test.php 60
constant_fold_declaration.test.php 63
selector_parser.test.php 40
attribute_selector_parser.test.php 20
value_parser.test.php 19
ast.test.php 18
walk.test.php 16
compare.test.php 14
brace_expansion.test.php 13
segment.test.php 12
replace_shadow_colors.test.php 14
escape.test.php 10
prefix.test.php 9
expand_declaration.test.php 4
important.test.php 4

PHP-Specific Unit Tests

Tests for PHP-specific implementations and helpers (not direct TypeScript ports):

Test File Status Tests
_tailwindphp/LightningCss.test.php 70
theme_unit.test.php 46
utils/infer_data_type.test.php 45
utils/is_color.test.php 33
utils/math_operators.test.php 22
utils/is_valid_arbitrary.test.php 17
property_order.test.php 15
design_system.test.php 13
utils/dimensions.test.php 10
utils/default_map.test.php 12
utils/compare_breakpoints.test.php 9
utils/topological_sort.test.php 9

Import Path Tests (tests/ImportPathsTest.php)

Test File Status Tests
ImportPathsTest.php 42

Tests for the importPaths feature including:

  • Single file and directory imports
  • Array of import paths
  • Nested @import resolution
  • Circular import protection via deduplication
  • Custom resolver functions
  • Virtual module deduplication (tailwindcss, tailwindcss/preflight, etc.)
  • Multiple @theme blocks and overrides
  • @utility from imported files
  • All CSS @import modifiers: layer(), supports(), and media queries
  • Anonymous and named layer imports
  • Combined modifiers (layer + supports + media)

Edge Case Tests (tests/EdgeCasesTest.php)

Test File Status Tests
EdgeCasesTest.php 57

Tests for edge cases and complex scenarios:

  • Deep variant stacking (3-4 levels)
  • Arbitrary values with special characters
  • Container queries with arbitrary breakpoints
  • @apply with complex selectors
  • nth-child, has(), aria, and data selectors
  • Important modifier combinations
  • Opacity modifier edge cases
  • Print, motion, and contrast media variants
  • Dark mode with variant combinations
  • Pseudo-element variants with modifiers

Regression Tests (tests/)

Test File Status Tests
PropertyRegistrationTest.php 98
ContainerUtilityTest.php 3
NestedApplyRegressionTest.php 3
  • PropertyRegistrationTest.php@property registration of composed --tw-* variable groups (translate, scale, transform, filters, backdrop-filters, transitions, typography, font-variant-numeric, contain, touch-action, scroll-snap, border-spacing), including registration details, the @layer properties fallback, and the negative cases that must register nothing
  • ContainerUtilityTest.php — the container layout component (per-breakpoint media queries, unit-then-value breakpoint sorting, custom theme breakpoints)
  • NestedApplyRegressionTest.php — nested rules surviving a parent @apply that expands to multiple declarations

tw-animate-css Tests (tests/TwAnimateCssTest.php)

Test File Status Tests
TwAnimateCssTest.php 23

Tests for tw-animate-css integration:

  • Import via @import "tw-animate-css"
  • animate-in/animate-out utilities
  • fade-in/fade-out with opacity values
  • zoom-in/zoom-out with scale values
  • spin-in/spin-out with rotation values
  • slide-in/slide-out directional animations
  • animation-duration, delay, repeat, direction, fill-mode utilities
  • play-state (running/paused) utilities
  • blur-in/blur-out utilities
  • accordion-down/accordion-up animations
  • collapsible-down/collapsible-up animations
  • caret-blink animation
  • RTL/LTR-aware start/end directional slides
  • Common shadcn/ui patterns (dialogs, dropdowns)

Cache Tests (tests/)

Test File Status Tests
CacheTest.php 14
StateCacheTest.php 6

Tests for file-based caching and the persistent construction-state cache:

  • Cache to default and custom directories
  • Cache key generation from content, CSS, and minify option
  • Cache TTL (time-to-live) expiration
  • clearCache() function and tw::clearCache() static method
  • Cache directory auto-creation

CLI Tests (tests/)

Test File Status Tests
CliTest.php 25
CliInputTest.php 35
CliOutputTest.php 24

Tests for the CLI application (1:1 port of @tailwindcss/cli):

  • Input argument and option parsing (-i, -o, -w, -m, --cwd, etc.)
  • Short and long option formats
  • Build with input/output files
  • @source directive for content scanning
  • Minify and optimize options
  • Output directory creation
  • Error handling (missing files, invalid paths)
  • Application help and version output
  • CLI Input class (argument parsing, options, commands)
  • CLI Output class (colors, formatting, quiet/verbose modes)

@source Directive Tests (tests/SourceDirectiveTest.php)

Test File Status Tests
SourceDirectiveTest.php 24

Tests for the @source directive:

  • Basic file/directory patterns (@source "./templates")
  • Glob patterns (@source "./src/**/*.html")
  • Negated patterns (@source not "./ignored")
  • Inline candidates (@source inline("flex p-4"))
  • Ignored candidates (@source not inline("legacy"))
  • Brace expansion in inline patterns
  • Validation (no body, no nesting, quoted paths)
  • Quote styles (single and double quotes)
  • CLI integration (sources array returned)

Outside Scope (0 tests - intentionally empty)

These PHP test files exist but contain no tests because the TypeScript originals test features outside the scope of this port (JS runtime, IDE tooling):

PHP Test File TypeScript Original Reason
canonicalize_candidates.test.php canonicalize-candidates.test.ts IDE tooling - Prettier plugin
intellisense.test.php intellisense.test.ts IDE tooling - VS Code extension
sort.test.php sort.test.ts IDE tooling - class sorting
to_key_path.test.php to-key-path.test.ts Not needed

Other TypeScript test files not ported: config.test.ts, resolve-config.test.ts, container-config.test.ts, screens-config.test.ts, flatten-color-palette.test.ts, apply-config-to-theme.test.ts, apply-keyframes-to-theme.test.ts, legacy-utilities.test.ts, source-map.test.ts, line-table.test.ts, translation-map.test.ts.

Implemented Features

  • ✅ All utility classes (364 utilities)
  • ✅ All variants (hover, focus, responsive, dark mode, container queries, etc.)
  • @apply directive with nested selectors
  • @theme customization with namespace clearing
  • @utility custom utilities
  • @custom-variant support
  • @import resolution for virtual modules (tailwindcss, tailwindcss/preflight, tw-animate-css, etc.)
  • @import resolution for file paths via importPaths option
  • ✅ Nested @import resolution (files importing other files)
  • ✅ Import deduplication (virtual modules and files)
  • ✅ Custom import resolvers (callable for virtual file systems)
  • theme() function with dot notation (colors.red.500)
  • --theme() function with initial fallback handling
  • --spacing() and --alpha() functions
  • color-mix() to oklab() conversion (LightningCSS equivalent)
  • ✅ Stacking opacity in @theme definitions
  • ✅ Prefix support (tw:)
  • ✅ Shadow/ring stacking with --tw-* variables
  • @property rules with @layer properties fallback
  • ✅ Vendor prefixes (autoprefixer equivalent)
  • ✅ Keyframe handling and hoisting
  • ✅ Built-in plugins (@tailwindcss/typography, @tailwindcss/forms)
  • ✅ Invalid theme() candidates filtered out
  • ✅ File-based caching with TTL support
  • ✅ CLI (1:1 port of @tailwindcss/cli with -i, -o, -w, -m, --optimize, --cwd options)
  • @source directive with full support (not, inline(), brace expansion)

Port Deviation Markers

All 44 implementation files are documented with @port-deviation markers explaining where and why the PHP implementation differs from TypeScript.

Deviation Types

Marker Meaning
@port-deviation:none Direct 1:1 port with no significant deviations
@port-deviation:async PHP uses synchronous code instead of async/await
@port-deviation:storage Different data structures (PHP array vs JS Map/Set)
@port-deviation:types PHPDoc annotations instead of TypeScript types
@port-deviation:sourcemaps Source map tracking omitted
@port-deviation:enum PHP constants instead of TypeScript enums
@port-deviation:caching Different caching strategy
@port-deviation:errors Different error handling approach
@port-deviation:stub Placeholder for unneeded functionality
@port-deviation:replacement PHP implementation replacing external library (e.g., lightningcss)
@port-deviation:helper PHP-specific helper not in original
@port-deviation:omitted Entire module not ported (not needed for PHP)

Usage Examples

File header deviation:

/**
 * Port of: packages/tailwindcss/src/compile.ts
 *
 * @port-deviation:bigint TypeScript uses BigInt for variant order bitmask.
 * PHP uses regular integers (sufficient for current variant count).
 *
 * @port-deviation:sorting TypeScript uses Map<AstNode, ...> for nodeSorting.
 * PHP embeds sorting info directly in nodes via '__sorting' key.
 */

Inline deviation:

// @port-deviation: PHP regex syntax differs from JS
$pattern = '/.../';