hydrogen icon indicating copy to clipboard operation
hydrogen copied to clipboard

Fix Tailwind v4 scaffolding FOUC issues

Open juanpprieto opened this issue 4 months ago β€’ 0 comments

WHY are these changes introduced?

Fixes critical scaffolding failures and test instability blocking first-party Tailwind CSS v4 support in Hydrogen. Enables Tailwind v4 as the default CSS framework for new projects with production-ready optimizations and comprehensive CLI integration.

WHAT is this pull request doing?

🎯 First-Party Tailwind v4 Integration

Before:                             After:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ h2 init --quickstartβ”‚            β”‚ h2 init --quickstartβ”‚
β”‚ └─ CSS: No default  β”‚            β”‚ └─ CSS: Tailwind v4 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ --styling tailwind  β”‚            β”‚ --styling tailwind  β”‚
β”‚ └─ Remote templates β”‚            β”‚ └─ Full CLI Setup   β”‚
β”‚    had no CSS       β”‚            β”‚                     β”‚
β”‚    strategy support β”‚            β”‚                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CLI Path Resolution β”‚            β”‚ CLI Path Resolution β”‚
β”‚ └─ Hard-coded dist/ β”‚            β”‚ └─ Dynamic detectionβ”‚
β”‚    paths only       β”‚            β”‚    for both configs β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Component Before After Impact
Quickstart No default CSS Tailwind v4 default Zero-config styling
CLI Commands --styling unsupported for remote Full CSS strategy Reliable scaffolding
Version v4.1.6 (beta) v4.1.12 (stable) Production ready
Build Output Single path structure Dynamic viteEnvironmentApi support All CLI commands work

πŸ”§ Core Infrastructure

CLI Path Resolution (Breaking Change)

React Router Build Outputs:
β”œβ”€ viteEnvironmentApi: false (legacy)
β”‚  └─ dist/
β”‚     β”œβ”€ index.js
β”‚     └─ metafile.server.json
└─ viteEnvironmentApi: true (modern)
   └─ dist/
      β”œβ”€ client/
      └─ server/
         β”œβ”€ index.js
         └─ metafile.server.json

CLI Auto-Detection: All commands (build, deploy, preview, debug) 
now dynamically resolve paths based on React Router config

CSS Strategy Integration

  • Remote Templates: Add CSS strategy handling for --styling tailwind
  • Import Resolution: Correct appStyles β†’ tailwindStyles replacement
  • Performance: CSS preload optimization with fetchPriority: 'high'
  • Bundle Strategy: Unified CSS (cssCodeSplit: false) prevents route-based splitting

πŸ“¦ Files Changed

packages/cli/                                       
β”œβ”€β”€ assets/tailwind/package.json          β†’ v4.1.12 stable
β”œβ”€β”€ src/commands/hydrogen/init.ts          β†’ Tailwind default for quickstart
β”œβ”€β”€ src/lib/onboarding/remote.ts           β†’ CSS strategy handling  
β”œβ”€β”€ src/lib/setups/css/replacers.ts        β†’ Import replacement fixes
β”œβ”€β”€ src/lib/setups/css/tailwind.ts         β†’ Path corrections
β”œβ”€β”€ src/lib/vite-config.ts                 β†’ viteEnvironmentApi support
└── src/**/*.test.ts                       β†’ Comprehensive test coverage

packages/hydrogen/
└── src/vite/plugin.ts                     β†’ cssCodeSplit: false

templates/skeleton/
β”œβ”€β”€ app/root.tsx                           β†’ CSS preload optimization
β”œβ”€β”€ package.json                           β†’ Exact dependency versions
└── react-router.config.ts                β†’ viteEnvironmentApi: true

Test Coverage: 21 files changed, 2,122 insertions(+), 20 deletions(-)

Quality Assurance

E2E Test Pipeline

Scaffold β†’ Verify Files β†’ Install Deps β†’ Type Check β†’ Build β†’ Validate
   ↓           ↓             ↓           ↓          ↓         ↓
 Success    All Present    Clean       Pass     Success   Optimized
  • Complete Scaffolding Flow: From init β†’ production build
  • Cross-Language Support: TypeScript and JavaScript variants
  • Version Matrix: viteEnvironmentApi enabled/disabled compatibility
  • Regression Prevention: Import handling, dependency resolution

HOW to test your changes?

Prerequisites

# Ensure you're on the fix/tailwind-v4-fouc-clean branch
git checkout fix/tailwind-v4-fouc-clean

# Build the CLI
cd packages/cli
npm run build

Test 1: Quickstart Mode (Tailwind Default)

Setup

# Scaffold with quickstart (Tailwind is now default)
node dist/create-app.js --path /tmp/test-quickstart --quickstart --no-install-deps

Validation Commands

cd /tmp/test-quickstart

# 1. Verify Tailwind CSS file exists
ls -la app/styles/tailwind.css
# Expected: File exists with @import 'tailwindcss'

# 2. Check package.json has Tailwind v4.1.12
grep -E "tailwindcss|@tailwindcss/vite" package.json
# Expected:
#   "tailwindcss": "^4.1.12"
#   "@tailwindcss/vite": "^4.1.12",

# 3. Verify tailwindStyles import is present
grep "import.*Styles" app/root.jsx
# Expected:
#   import resetStyles from '~/styles/reset.css?url';
#   import tailwindStyles from '~/styles/tailwind.css?url';

# 4. Check fetchPriority optimization
grep "fetchPriority: 'high'" app/root.jsx
# Expected: 2 occurrences for CSS preload hints

# 5. Verify React Router viteEnvironmentApi flag
grep "unstable_viteEnvironmentApi: true" react-router.config.js
# Expected: Flag is present

# 6. Check Vite config has Tailwind plugin
grep "@tailwindcss/vite" vite.config.js
# Expected: import tailwindcss from '@tailwindcss/vite';

Test 2: Explicit Tailwind Setup (JavaScript)

Setup

# Use the local CLI from this branch
cd packages/cli
node dist/create-app.js --path /tmp/test-explicit-tailwind \
  --template skeleton --styling tailwind --language js --no-install-deps

Validation Commands

cd /tmp/test-explicit-tailwind

# 1. JavaScript root file has correct imports
grep "import.*Styles" app/root.jsx
# Expected:
#   import resetStyles from '~/styles/reset.css?url';
#   import tailwindStyles from '~/styles/tailwind.css?url';

# 2. No appStyles references remain
grep "appStyles" app/root.jsx
# Expected: No matches (appStyles completely replaced)

# 3. Verify Tailwind CSS v4 syntax
cat app/styles/tailwind.css
# Expected: @import 'tailwindcss'; (NOT @tailwind directives)

# 4. Check package.json has Tailwind v4.1.12
grep -E "tailwindcss|@tailwindcss/vite" package.json
# Expected:
#   "tailwindcss": "^4.1.12"
#   "@tailwindcss/vite": "^4.1.12",

Test 3: Full Build Validation

Setup

cd /tmp/test-quickstart
npm install
npm run build

Validation

# 1. Build completes successfully
# Expected: dist/ directory created

# 2. CSS is not code-split
ls -la dist/client/*.css
# Expected: Single CSS file (not split per route)

# 3. Preview without styling flash
npm run preview
# Open http://localhost:3000
# Navigate between routes
# Expected: NO flash of unstyled content

Test 4: E2E Test Suite

Run Tests

cd packages/cli
npm run test:e2e

What the E2E Tests Validate

Test 1: Complete Tailwind v4 Project Scaffolding

  • Step 1: Runs init command with Tailwind styling
  • Step 2: Verifies all files created correctly
    • Tailwind CSS file exists at app/styles/tailwind.css
    • Package.json has tailwindcss: ^4.1.12
    • Package.json has @tailwindcss/vite: ^4.1.12
    • Root file imports tailwindStyles (not appStyles)
    • CSS preload hints have fetchPriority: 'high'
    • React Router config has unstable_viteEnvironmentApi: true
    • Vite config includes Tailwind plugin
    • No beta/alpha versions in dependencies
  • Step 3: Installs dependencies
  • Step 4: Runs TypeScript check (no errors)
  • Step 5: Builds the project successfully
  • Step 6: Verifies build output in dist/
  • Step 7: Validates CSS is not code-split (single CSS file)

Test 2: Quickstart Mode with Tailwind Default

  • Verifies Tailwind is enabled by default in quickstart
  • Checks all optimizations are applied
  • Validates package.json has v4.1.12

Expected Console Output

Test project path: /tmp/...
Starting E2E test for Tailwind v4 scaffolding...
Step 1: Running init command...
Step 2: Verifying file creation...
Tailwind CSS file exists: true
Tailwind in dependencies: ^4.1.12
@tailwindcss/vite in devDependencies: ^4.1.12
Style imports found:
  import resetStyles from '~/styles/reset.css?url';
  import tailwindStyles from '~/styles/tailwind.css?url';
Step 3: Installing dependencies...
Step 4: Running TypeScript check...
Step 5: Building the project...
Step 6: Verifying build output...
Step 7: Validating optimizations...
Found 1-3 CSS files in build
E2E test completed successfully!
Testing quickstart mode with Tailwind...
Quickstart test completed successfully!

Note: Tests have 2-minute timeout to accommodate full build process. If npm install fails in CI due to React Router conflicts, the test may need retry.

Test 5: One-Line Validation Script

Run this comprehensive validation in one command:

# Clean test
rm -rf /tmp/tophat-test && \
cd packages/cli && \
node dist/create-app.js --path /tmp/tophat-test --quickstart --no-install-deps && \
cd /tmp/tophat-test && \
echo "=== VALIDATION RESULTS ===" && \
echo "1. Tailwind CSS:" && ls app/styles/tailwind.css 2>/dev/null && echo "File exists" || echo "Missing" && \
echo "2. Package.json:" && grep -q "tailwindcss.*4.1.12" package.json && echo "Has v4.1.12" || echo "Wrong version" && \
echo "3. Import:" && grep -q "import tailwindStyles" app/root.jsx && echo "Import present" || echo "Import missing" && \
echo "4. FetchPriority:" && grep -q "fetchPriority: 'high'" app/root.jsx && echo "Optimization applied" || echo "Missing" && \
echo "5. ViteEnvironmentApi:" && grep -q "viteEnvironmentApi: true" react-router.config.js && echo "Flag enabled" || echo "Missing" && \
echo "=== ALL CHECKS COMPLETE ==="

Expected Output:

=== VALIDATION RESULTS ===
1. Tailwind CSS:
app/styles/tailwind.css
File exists
2. Package.json:
Has v4.1.12
3. Import:
Import present
4. FetchPriority:
Optimization applied
5. ViteEnvironmentApi:
Flag enabled
=== ALL CHECKS COMPLETE ===

Key Fixes Implemented

1. Remote Template CSS Strategy Support

  • Added handleCssStrategy to setupRemoteTemplate
  • Fixed issue where --styling tailwind had no effect on remote templates

2. Import Statement Fix

  • Fixed replacers to correctly replace appStyles import with tailwindStyles
  • Fixed import path generation to use ~/ prefix

3. Performance Optimizations

  • fetchPriority: 'high' on CSS preload hints
  • unstable_viteEnvironmentApi: true in React Router config
  • cssCodeSplit: false in Hydrogen Vite plugin

4. Quickstart Default

  • Tailwind CSS v4 now default for --quickstart mode
  • Updated from v4.1.6 to v4.1.12 (latest stable)

Manual Fix (if needed)

If for any reason the import is missing after scaffolding, add this line after the resetStyles import in root.tsx/jsx:

import tailwindStyles from '~/styles/tailwind.css?url';

Success Criteria

All new Hydrogen projects with Tailwind CSS v4 should:

  1. Have no flash of unstyled content in production
  2. Use Tailwind CSS v4.1.12 (stable release)
  3. Have proper imports and no undefined variables
  4. Include all performance optimizations
  5. Work with both TypeScript and JavaScript

Impact: Tailwind v4 production-ready β€’ Zero-config quickstart β€’ All CLI commands compatible β€’ Comprehensive test coverage

juanpprieto avatar Aug 22 '25 04:08 juanpprieto