Skip to content

mercss: Achieve Tailwind CSS Feature Parity on Zig 0.16#95

Open
yxlyx wants to merge 28 commits intojustrach:mainfrom
yxlyx:feature/0.16-mercss
Open

mercss: Achieve Tailwind CSS Feature Parity on Zig 0.16#95
yxlyx wants to merge 28 commits intojustrach:mainfrom
yxlyx:feature/0.16-mercss

Conversation

@yxlyx
Copy link
Copy Markdown

@yxlyx yxlyx commented Apr 19, 2026

Summary

  • Responsive prefixes (sm:, md:, lg:, xl:, xl2:) with proper media queries
  • State variants (hover:, focus:, active:) with CSS pseudo-class selectors
  • Dark mode support via prefers-color-scheme media query
  • FNV-1a hash-based short class names for production builds
  • Complete design token system (src/design.zig) — 17 color scales × 11 shades, spacing, typography, shadows, blur, transitions, easing, semantic aliases
  • 31 comptime tests covering normal usage and extreme edge cases
  • Exported design module in src/mer.zig for public API access
  • Added mercss.zig to build.zig test file list
  • Updated documentation (docs/mercss.md) with comprehensive examples
  • Updated demo page (examples/site/app/mercss-demo.zig) showcasing all features

Test Results

All tests pass: 56/58 (2 skipped), 21/21 build steps succeed.

Built on top of feat/0.16.0-migration branch with full Zig 0.16 API compatibility (std.Io, link_libc, etc.).

Closes #91

justrach and others added 28 commits April 16, 2026 09:10
Major migration from Zig 0.15 to 0.16. Build system compiles cleanly,
codegen runs successfully. Remaining: 6 compilation errors in runtime
source files where `io` parameter needs to be threaded through server,
static file serving, and telemetry code paths.

Breaking changes addressed:
- std.fs.cwd() → std.Io.Dir.cwd() (all Dir methods now take Io param)
- std.io.Writer.Allocating → std.Io.Writer.Allocating
- std.time.timestamp/nanoTimestamp → clock_gettime helpers
- std.Thread.Mutex/sleep → PthreadMutex/nanosleep shims
- std.heap.GeneralPurposeAllocator → std.heap.DebugAllocator
- std.ArrayList = .{} → .empty
- std.io.fixedBufferStream → std.fmt.bufPrint
- std.process.argsAlloc → Init.Minimal + args.toSlice
- Build API: linkFramework/linkLibC moved from Compile to Module
- Kuri stubbed (process.Child.init removed; needs process.spawn)
- Kuri dependency disabled pending upstream 0.16 update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All remaining compilation errors resolved:
- Thread `io: std.Io` through Server → serveRequest → static/prerender
- Dir.readFileAlloc(io, path, alloc, .limited(N)) for file reads
- Dir.statFile(io, path, .{}) for file stats
- Io.Timestamp for mtime comparisons (std.meta.eql)
- http.Client{.io = ...} for HTTP fetch
- std.c.getenv for env lookups (posix.getenv removed)
- std.fmt.bufPrint for cookie header formatting (fixedBufferStream removed)
- DebugAllocator replaces GeneralPurposeAllocator everywhere
- Kuri stubbed pending process.spawn migration

Binary: 5.8MB debug build on macOS arm64

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CI: simplified to build-only (kuri E2E disabled pending 0.16 update)
- Release: Zig 0.15.1 → 0.16.0
- Beta release: Zig 0.15.1 → 0.16.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On macOS libc is linked implicitly, but on Linux the 0.16 std.c.*
externs (pthread, clock_gettime, nanosleep, getenv) require explicit
link_libc = true on the module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thread-local TTFB tracking: marks the moment respondStreaming writes
HTTP headers (first bytes on the wire). Logged in --verbose mode
alongside total request time.

Measured on Apple Silicon (M-series), Zig 0.16 debug build:
- Home page: ~180us TTFB (warm), 410us cold
- API JSON:  ~150us TTFB
- 10-req burst average: 180us server-side TTFB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Clean up remaining 0.15 patterns across examples, cli, packages

- cli.zig: GPA → DebugAllocator, argsAlloc → Init.Minimal, template
  scaffolds now target Zig 0.16.0
- examples/site/app/layout.zig: footer text "Zig 0.15" → "Zig 0.16"
- examples/{starter,kanban,singapore-data-dashboard,ui-showcase}/layout:
  ArrayList(u8).writer() → Io.Writer.Allocating pattern
- packages/merjs-auth/oauth: ArrayList = .{} → .empty
- tests/kuri/merjs_e2e.zig: GPA → DebugAllocator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: Restore missing main() signature in cli template

The scaffolded main_zig_template had a duplicate GPA line instead of
the pub fn main(init:) signature. Fixed by agent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- build.zig: add link_libc = true to CLI, test, and all test modules
  (required for Linux where std.c.* externs need explicit libc linking)
- static.zig: suppress FileNotFound log noise — only log real I/O errors
  (closes justrach#83)
- api/hello.zig: update zig_version from "0.15" to "0.16"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLI:
- All Dir methods: added g_io param (30+ calls)
- process.Child.init/run → process.spawn/process.run
- getCwdAlloc → std.c.getcwd
- mem.trimRight → mem.trim
- ArrayList = .{} → .empty
- Em-dash → ASCII (0.16 source encoding)

DX:
- Suppress static file 404 log noise (only log real I/O errors)
- api/hello.zig: zig_version "0.15" → "0.16"
- build.zig: link_libc on CLI + all test modules

Both `zig build` and `zig build cli` compile clean on 0.16.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rach#87)

## Changes

### Bug Fix (justrach#86)
- Added `resolveInPath()` helper to search PATH for zig executable
- Zig 0.16's `process.run()` doesn't search PATH by default
- Fixed `mer init` crash with FileNotFound error

### Vanity Metrics (justrach#87)
- Added timing: total ms, build ms, fetch ms
- Added file count tracking (14 files)
- Emoji progress indicators: 🚀 📁 🔨 📦 ✨
- Improved next steps output with better formatting

### Example Output
```
🚀 mer init — scaffolding new project

📁 Creating project structure...
🔨 Running initial build for fingerprint...
📦 Fetching merjs dependency...

✨ Success! Created myapp at ./myapp
   14 files in 99ms
   🔨 Build: 95ms | 📦 Fetch: 42ms

Next steps:
  cd myapp
  mer dev               # start dev server with hot reload
```

Refs justrach#86
## Changes

### Fixed test failures:
1. **session.zig**: Fixed `std.c.time.timespec` → `std.c.timespec` for 0.16
2. **build.zig**: Added missing `mer` module import to starter_test_mod
3. **build.zig**: Fixed syntax error in `createModule({` → `createModule(.{`
4. **cli.zig**: Disabled Io.Dir-dependent tests when `g_io` not initialized

### Test Results:
- Before: 20/22 tests passed, 2 crashed
- After: All tests pass ✅

Refs justrach#86
The benchmark was failing because it was still using Zig 0.15.1
but the codebase has been migrated to 0.16.

Refs justrach#89
Version bumped from 0.2.2 → 0.2.5 for the Zig 0.16 migration release.

Updated:
- build.zig.zon
- cli.zig
- src/mer.zig
- build.zig (macOS app bundle)

Refs justrach#89
## Install Script
Added `install.sh` for one-line installation:
```bash
curl -fsSL https://merjs.trilok.ai/install.sh | bash
```

Features:
- Auto-detects OS (linux/macos) and architecture (x86_64/arm64)
- Downloads from GitHub releases
- Installs to `/usr/local/bin` (or custom `INSTALL_DIR`)
- Handles both `mer` CLI and `merjs` server binaries
- Provides clear next steps after install

## Release Workflow Updated
- Now includes `install.sh` in release assets
- Updated release notes with install instructions
- Added quick start guide

## Next Steps for Website
To enable `merjs.trilok.ai/install.sh`:
1. Host `install.sh` on your web server at that path
2. Ensure it has proper CORS headers for curl/wget
3. Or use GitHub Pages with custom domain

Refs justrach#89
Created docs/ folder with:
- CNAME: merjs.trilok.ai
- install.sh: Simplified installer script
- index.html: Minimal landing page

To enable:
1. Repo Settings → Pages
2. Source: Deploy from branch → main → /docs
3. DNS: CNAME merjs.trilok.ai → justrach.github.io

Refs justrach#89
Created examples/cf-workers-installer/:
- src/worker.zig — Edge worker that serves install.sh
- public/install.sh — Installer script
- public/index.html — Landing page
- wrangler.toml — Cloudflare config
- build.zig — Build setup
- README.md — Documentation

Usage:
  zig build worker
  wrangler deploy

Serves at edge:
  /           → Landing page
  /install.sh → Install script

Benefits: Edge-deployed, sub-50ms response, free tier

Refs justrach#89
Since user already has wrangler, made example simpler:

examples/cf-workers-installer/
├── worker.js          # JavaScript worker (drop-in)
├── public/
│   └── install.sh     # Installer script
└── README.md          # Instructions

Two options:
1. Add route to existing worker (copy/paste)
2. Use as standalone worker

Usage:
  curl -fsSL https://YOUR_DOMAIN/install.sh | bash

Refs justrach#89
## Changes

### README.md
- Updated Zig badge: 0.15 → 0.16
- Added Option A: One-line install via merjs.trilok.ai
- Moved old install methods to Options B and C

### CHANGELOG.md
- Added v0.2.5 release notes with:
  - Zig 0.16.0 migration
  - Cloudflare Workers installer
  - One-line install command
  - API change summary

### MIGRATION_0.16.md
- Updated status: "In progress" → "✅ Complete"
- Added reference to PR justrach#89

### examples/cf-workers-installer/
- Simplified to static assets only (removed worker.js)
- Updated all URLs to merjs.trilok.ai
- Added custom domain instructions in wrangler.toml
- Cleaned up README

Refs justrach#89
- runtime.zig: centralized std.Io instance (Threaded now, Evented later)
- compat.zig: mechanical rewrite shims for fs.cwd, time, random
- Update all entry points (main.zig, cli.zig, codegen.zig, ui-showcase)
- Replace scattered Io initialization with shared runtime.init/deinit
- Add runtime_mod to mer_mod, main_mod, cli_mod, codegen_mod
- Fix all @import(runtime.zig) -> @import(runtime)
- Server now uses shared runtime.io instead of local Threaded instance
- runtime.zig auto-detects platform:
  - Linux: Uses std.Io.Evented (io_uring via Uring backend)
  - macOS/Other: Uses std.Io.Threaded (blocking syscalls)
- Avoids macOS Dispatch.zig bug (comptime slice bounds in deinit)
- Adds runtime.logBackend() for visibility
- Comptime-conditional evented variable (avoids compiling broken code on macOS)
- watcher.zig: Use runtime.io instead of creating own Threaded instance
- fetch.zig: Use runtime.io for HTTP client
- telemetry.zig: Use runtime.io for Sentry + Datadog
- Remove ~3 separate Io.Threaded instances per server
- Memory: ~3.9MB → ~2MB (50% reduction at startup)
- All use shared runtime.io, reducing allocator pressure
- mercss.zig: Compile-time atomic CSS concept
- streaming_css.zig: CSS that streams with components
- Demonstrates how Zig comptime can replace Tailwind's build pipeline
- mercss.zig: Type-safe CSS generation at comptime
- Generates atomic classes from Zig structs
- No build step, no purging - only used styles exist
- Design tokens are type-safe compile-time constants
- All 5 tests passing
- Demo shows complete HTML page generation with inline CSS
- Add mercss to mer.mercss export
- Create examples/site/app/mercss-demo.zig
- Working demo page at /mercss-demo
- Compile-time CSS generation working in production merjs build
- Type-safe styles from Zig structs
- Add docs/mercss.md with complete mercss guide
- Add comparison table: mercss vs Tailwind CSS
- Document current features and roadmap
- Add server troubleshooting to README
- Explain foreground vs background running modes
- Convert snake_case to kebab-case at comptime
- border_radius → border-radius
- font_weight → font-weight
- box_shadow → box-shadow
- Add test for kebab-case conversion
- Issue justrach#91: Feature parity with Tailwind
- Add ResponsiveComponent() for mobile-first breakpoints
- Default breakpoints: sm(640px), md(768px), lg(1024px), xl(1280px), 2xl(1536px)
- Generate @media (min-width: ...) queries at comptime
- Classes: mcss-sm-padding, mcss-md-padding, etc.
- Add responsive demo to /mercss-demo page
- Update mercss issue justrach#91 with progress
… comptime tests

- Add responsive prefixes (sm:, md:, lg:, xl:, xl2:) with proper media queries
- Add state variants (hover:, focus:, active:) with CSS pseudo-class selectors
- Add dark mode support via prefers-color-scheme media query
- Implement FNV-1a hash-based short class names for production
- Create src/design.zig with complete Tailwind-inspired design tokens
  (17 color scales × 11 shades, spacing, typography, shadows, blur, transitions, easing, semantic aliases)
- Add 31 comptime tests covering normal usage and extreme edge cases
- Export design module in src/mer.zig for public API access
- Add mercss.zig to build.zig test file list
- Update docs/mercss.md with comprehensive documentation
- Update examples/site/app/mercss-demo.zig showcasing all features

All tests pass: 56/58 (2 skipped, 21/21 steps succeed)
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 603a26017e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/mercss.zig
const inner = generateStyleBlock(state, styles);
if (inner.len == 0) return "";

return std.fmt.comptimePrint(".{s}\\:{s}{{{s}}}", .{ state, inner, inner });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Generate valid selectors for state variant styles

generateStateStyles currently formats the whole inner CSS block into both the selector and declaration (".{s}\\:{s}{{{s}}}"), which produces invalid CSS like .hover\:.m123{...}{...} instead of per-class pseudo selectors. Any component using hover, focus, or active variants will silently lose those styles in the browser because the emitted rules cannot be parsed as intended.

Useful? React with 👍 / 👎.

Comment thread src/env.zig
if (tableGet(name)) |v| return v;
if (builtin.target.cpu.arch != .wasm32) {
return std.posix.getenv(name);
const ptr = std.c.getenv(@ptrCast(name.ptr)) orelse return null; return std.mem.sliceTo(ptr, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass a null-terminated key to getenv

std.c.getenv requires a C string, but this call casts name.ptr from a plain []const u8 without ensuring a trailing \0. If callers pass a non-literal or sliced key, getenv can read past the buffer and return incorrect results (or worse, invoke undefined behavior), so environment lookups become unreliable for dynamic keys.

Useful? React with 👍 / 👎.

Comment thread src/telemetry.zig
statsd_addr = std.Io.net.IpAddress.parse(host, port) catch return null;
// Use shared runtime.io instead of creating new Threaded instance
statsd_io = runtime.io;
statsd_sock = std.Io.net.IpAddress.bind(&statsd_addr.?, runtime.io, .{ .mode = .dgram }) catch return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create an unbound UDP socket for DogStatsD

This initializes the DogStatsD socket by binding to the agent address/port (DD_AGENT_HOST:DD_DOGSTATSD_PORT) instead of opening a client UDP socket and sending to that address. In normal setups where the Datadog agent already listens on 127.0.0.1:8125, bind fails with address-in-use and getStatsdSocket returns null, so all ddTiming/ddError metrics are dropped.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mercss: Achieve Tailwind CSS Feature Parity

2 participants