From f2f17e84d70aa22d17dd3559ba956a6600278110 Mon Sep 17 00:00:00 2001 From: Daniel Whitney Date: Wed, 18 Feb 2026 14:16:38 -0500 Subject: [PATCH 1/5] feat(vite): scan Vite pipeline modules for candidate classes Augment the existing filesystem scanner with a transform-based scanner that intercepts all modules flowing through Vite (including virtual ones), extracts candidates via scan_files(), and triggers incremental CSS HMR updates when new candidates are discovered. Co-Authored-By: Claude Opus 4.6 --- packages/@tailwindcss-vite/src/index.ts | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 3ab7c3a0f2f8..8c7e6f252e12 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -26,6 +26,14 @@ const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/ const COMMON_JS_PROXY_RE = /\?commonjs-proxy/ const INLINE_STYLE_ID_RE = /[?&]index\=\d+\.css$/ +const IGNORED_EXTENSIONS = new Set([ + 'css', 'less', 'sass', 'scss', 'styl', + 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'avif', + 'woff', 'woff2', 'ttf', 'eot', 'otf', + 'mp3', 'mp4', 'webm', 'ogg', 'wav', + 'json', 'map', 'lock', +]) + export type PluginOptions = { /** * Optimize and minify the output CSS. @@ -120,6 +128,48 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { ) } + let updateTimers = new Map>() + + function scheduleUpdate(envName: string) { + // Debounce: multiple modules may transform in quick succession + if (updateTimers.has(envName)) return + updateTimers.set( + envName, + setTimeout(() => { + updateTimers.delete(envName) + triggerCssUpdate(envName) + }, 16), + ) + } + + function triggerCssUpdate(envName: string) { + let roots = rootsByEnv.get(envName) + + for (let server of servers) { + for (let [id] of roots) { + let moduleGraph = + server.environments?.[envName]?.moduleGraph ?? server.moduleGraph + let mod = moduleGraph.getModuleById(id) + if (!mod) continue + + moduleGraph.invalidateModule(mod) + + let hot = server.environments?.[envName]?.hot ?? server.hot ?? server.ws + hot.send({ + type: 'update', + updates: [ + { + type: 'js-update', + timestamp: Date.now(), + path: mod.url, + acceptedPath: mod.url, + }, + ], + }) + } + } + } + return [ { // Step 1: Scan source files for candidates @@ -151,6 +201,40 @@ export default function tailwindcss(opts: PluginOptions = {}): Plugin[] { }, }, + { + name: '@tailwindcss/vite:scan-content', + enforce: 'pre', + + transform(src, id) { + let extension = getExtension(id) + if (extension === 'css') return + if (id.includes('/.vite/')) return + if (id.includes('/node_modules/')) return + if (SPECIAL_QUERY_RE.test(id)) return + if (COMMON_JS_PROXY_RE.test(id)) return + + if (id.startsWith('\0')) { + extension = getExtension(id.slice(1)) + } + + if (!extension || IGNORED_EXTENSIONS.has(extension)) return + + let envName = this.environment?.name ?? 'default' + let roots = rootsByEnv.get(envName) + let hasNewCandidates = false + + for (let root of roots.values()) { + if (root.scanContent(src, extension)) { + hasNewCandidates = true + } + } + + if (hasNewCandidates) { + scheduleUpdate(envName) + } + }, + }, + { // Step 2 (serve mode): Generate CSS name: '@tailwindcss/vite:generate:serve', @@ -368,6 +452,16 @@ class Root { return this.scanner?.files ?? [] } + public scanContent(content: string, extension: string): boolean { + if (!this.scanner) return false + + let newCandidates = this.scanner.scan_files([{ content, extension }]) + for (let candidate of newCandidates) { + this.candidates.add(candidate) + } + return newCandidates.length > 0 + } + // Generate the CSS for the root file. This can return false if the file is // not considered a Tailwind root. When this happened, the root can be GCed. public async generate( From c9fb3078fa26e954ed6db9fe459ad295a4f07e15 Mon Sep 17 00:00:00 2001 From: Daniel Whitney Date: Wed, 18 Feb 2026 14:47:32 -0500 Subject: [PATCH 2/5] ci: add GitHub Packages release workflow for @tiedye/tailwindcss-vite Publishes the Vite plugin to GitHub Packages under the @tiedye scope. Builds only for x86_64-unknown-linux-gnu instead of the full target matrix. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-tiedye.yml | 181 +++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 .github/workflows/release-tiedye.yml diff --git a/.github/workflows/release-tiedye.yml b/.github/workflows/release-tiedye.yml new file mode 100644 index 000000000000..ca87fab411a7 --- /dev/null +++ b/.github/workflows/release-tiedye.yml @@ -0,0 +1,181 @@ +name: Release Tiedye + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + APP_NAME: tailwindcss-oxide + NODE_VERSION: 24 + OXIDE_LOCATION: ./crates/node + +jobs: + build: + name: Build x86_64-unknown-linux-gnu (oxide) + runs-on: ubuntu-latest + container: + image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-x86_64-unknown-linux-gnu-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v5 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-x86_64-unknown-linux-gnu-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Setup rust target + run: rustup target add x86_64-unknown-linux-gnu + + - name: Install dependencies + run: pnpm install --ignore-scripts --filter=!./playgrounds/* + + - name: Build release + run: pnpm run --filter ${{ env.OXIDE_LOCATION }} build:platform --target=x86_64-unknown-linux-gnu + env: + RUST_TARGET: x86_64-unknown-linux-gnu + + - name: Strip debug symbols # https://github.com/rust-lang/rust/issues/46034 + run: strip ${{ env.OXIDE_LOCATION }}/*.node + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: bindings-x86_64-unknown-linux-gnu + path: ${{ env.OXIDE_LOCATION }}/*.node + + release: + runs-on: ubuntu-latest + timeout-minutes: 15 + name: Build and release @tiedye/tailwindcss-vite + + permissions: + contents: read + packages: write + id-token: write + + needs: + - build + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 20 + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + registry-url: 'https://npm.pkg.github.com' + + # Cargo already skips downloading dependencies if they already exist + - name: Cache cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-x86_64-unknown-linux-gnu-cargo-${{ hashFiles('**/Cargo.lock') }} + + # Cache the `oxide` Rust build + - name: Cache oxide build + uses: actions/cache@v5 + with: + path: | + ./crates/node/*.node + ./crates/node/*.wasm + ./crates/node/index.d.ts + ./crates/node/index.js + ./crates/node/browser.js + ./crates/node/tailwindcss-oxide.wasi-browser.js + ./crates/node/tailwindcss-oxide.wasi.cjs + ./crates/node/wasi-worker-browser.mjs + ./crates/node/wasi-worker.mjs + key: ${{ runner.os }}-x86_64-unknown-linux-gnu-oxide-${{ hashFiles('./crates/**/*') }} + + - name: Setup WASM target + run: rustup target add wasm32-wasip1-threads + + - name: Install dependencies + run: pnpm --filter=!./playgrounds/* install + + - name: Download artifacts + uses: actions/download-artifact@v7 + with: + path: ${{ env.OXIDE_LOCATION }} + + - name: Move artifacts + run: | + cd ${{ env.OXIDE_LOCATION }} + cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/ + + - name: Build Tailwind CSS + run: pnpm run build + env: + FEATURES_ENV: stable + + - name: Run pre-publish optimizations scripts + run: node ./scripts/pre-publish-optimizations.mjs + + - name: Lock pre-release versions + run: node ./scripts/lock-pre-release-versions.mjs + + - name: Rename package for Tiedye scope + run: | + node -e " + const fs = require('fs'); + const path = 'packages/@tailwindcss-vite/package.json'; + const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); + pkg.name = '@tiedye/tailwindcss-vite'; + delete pkg.publishConfig.provenance; + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Calculate environment variables + run: | + echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV + + - name: Publish @tiedye/tailwindcss-vite to GitHub Packages + run: | + cd packages/@tailwindcss-vite + pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 8fd7aa77a5c4e7ea75dd3fe170ef38a451618d6f Mon Sep 17 00:00:00 2001 From: Daniel Whitney Date: Wed, 18 Feb 2026 15:07:59 -0500 Subject: [PATCH 3/5] fix(ci): download artifact directly to target path The artifact was not found after cd into crates/node. Download directly into npm/linux-x64-gnu/ instead of copying. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-tiedye.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-tiedye.yml b/.github/workflows/release-tiedye.yml index ca87fab411a7..60a86f3fe5ad 100644 --- a/.github/workflows/release-tiedye.yml +++ b/.github/workflows/release-tiedye.yml @@ -140,12 +140,8 @@ jobs: - name: Download artifacts uses: actions/download-artifact@v7 with: - path: ${{ env.OXIDE_LOCATION }} - - - name: Move artifacts - run: | - cd ${{ env.OXIDE_LOCATION }} - cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/ + name: bindings-x86_64-unknown-linux-gnu + path: ${{ env.OXIDE_LOCATION }}/npm/linux-x64-gnu/ - name: Build Tailwind CSS run: pnpm run build From 035b247bbdfaebf6d4996d7650ae04d704844894 Mon Sep 17 00:00:00 2001 From: Daniel Whitney Date: Wed, 18 Feb 2026 15:16:44 -0500 Subject: [PATCH 4/5] fix(vite): use scanFiles instead of scan_files The Scanner type uses camelCase (scanFiles), not snake_case (scan_files). Co-Authored-By: Claude Opus 4.6 --- packages/@tailwindcss-vite/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 8c7e6f252e12..756344c1071b 100644 --- a/packages/@tailwindcss-vite/src/index.ts +++ b/packages/@tailwindcss-vite/src/index.ts @@ -455,7 +455,7 @@ class Root { public scanContent(content: string, extension: string): boolean { if (!this.scanner) return false - let newCandidates = this.scanner.scan_files([{ content, extension }]) + let newCandidates = this.scanner.scanFiles([{ content, extension }]) for (let candidate of newCandidates) { this.candidates.add(candidate) } From d67b55c692bbaa2fff486afa8cc37eb62edd1259 Mon Sep 17 00:00:00 2001 From: Daniel Whitney Date: Thu, 19 Feb 2026 13:06:38 -0500 Subject: [PATCH 5/5] ci: switch tiedye release to GitHub Release tarball Replaces GitHub Packages publish with pnpm pack + GitHub Release. Consumers can install directly from the tarball URL without needing to configure a custom registry. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release-tiedye.yml | 34 +++++++++++----------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release-tiedye.yml b/.github/workflows/release-tiedye.yml index 60a86f3fe5ad..1dc5dd48cfe8 100644 --- a/.github/workflows/release-tiedye.yml +++ b/.github/workflows/release-tiedye.yml @@ -82,9 +82,7 @@ jobs: name: Build and release @tiedye/tailwindcss-vite permissions: - contents: read - packages: write - id-token: write + contents: write needs: - build @@ -101,7 +99,6 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - registry-url: 'https://npm.pkg.github.com' # Cargo already skips downloading dependencies if they already exist - name: Cache cargo @@ -154,24 +151,19 @@ jobs: - name: Lock pre-release versions run: node ./scripts/lock-pre-release-versions.mjs - - name: Rename package for Tiedye scope + - name: Calculate version run: | - node -e " - const fs = require('fs'); - const path = 'packages/@tailwindcss-vite/package.json'; - const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); - pkg.name = '@tiedye/tailwindcss-vite'; - delete pkg.publishConfig.provenance; - fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); - " - - - name: Calculate environment variables - run: | - echo "RELEASE_CHANNEL=$(node ./scripts/release-channel.js)" >> $GITHUB_ENV + echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV - - name: Publish @tiedye/tailwindcss-vite to GitHub Packages + - name: Pack @tailwindcss/vite tarball run: | cd packages/@tailwindcss-vite - pnpm publish --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + pnpm pack --pack-gzip-level 9 + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: vite-v${{ env.TAILWINDCSS_VERSION }} + name: "@tailwindcss/vite v${{ env.TAILWINDCSS_VERSION }}" + files: packages/@tailwindcss-vite/*.tgz + fail_on_unmatched_files: true