diff --git a/.github/workflows/release-tiedye.yml b/.github/workflows/release-tiedye.yml new file mode 100644 index 000000000000..1dc5dd48cfe8 --- /dev/null +++ b/.github/workflows/release-tiedye.yml @@ -0,0 +1,169 @@ +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: 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' + + # 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: + name: bindings-x86_64-unknown-linux-gnu + path: ${{ env.OXIDE_LOCATION }}/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: Calculate version + run: | + echo "TAILWINDCSS_VERSION=$(node -e 'console.log(require(`./packages/tailwindcss/package.json`).version);')" >> $GITHUB_ENV + + - name: Pack @tailwindcss/vite tarball + run: | + cd packages/@tailwindcss-vite + 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 diff --git a/packages/@tailwindcss-vite/src/index.ts b/packages/@tailwindcss-vite/src/index.ts index 3ab7c3a0f2f8..756344c1071b 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.scanFiles([{ 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(