fix(aphrody): restore turbo build, drop unverified cargo/postcss changes, fix compileStream

- build is turbo again (oxide, all packages, .d.ts); fast-build.ts stays as
  build:fast, documented as a JS-only dev build, with the browser intellisense stub
- remove .cargo/config.toml (cargo has no linker-flavor key, rust-lld on macOS)
  and restore the upstream release profile (lto = true)
- revert the @tailwindcss/postcss candidate-hash cache and its duplicate
  compileStream/hashCandidates exports
- @tailwindcss/node compileStream: optional base/onDependency, no circular
  re-export from compile.ts, exported from index.cts, meaningful tests;
  drop the unused hashCandidates
- restore the v3 playground upgrade script (package.json was invalid JSON)
- test preload warns instead of silently skipping the snapshot serializer
This commit is contained in:
aphrody-dev committed 2026-10-09 03:25:42 +02:00
1 parent e0bf5e2316
commit f8a1923171
14 files changed
+118 -235

No files matched your search

-32
View File
@@ -1,32 +0,0 @@
[target.'cfg(all(windows, target_env = "msvc"))']
linker = "rust-lld"
rustflags = ["-Ctarget-feature=+crt-static"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
rustflags = ["-Ctarget-feature=+crt-static"]
[target.aarch64-pc-windows-msvc]
linker = "rust-lld"
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_os = "macos")']
linker = "rust-lld"
[target.x86_64-apple-darwin]
linker = "rust-lld"
[target.aarch64-apple-darwin]
linker = "rust-lld"
[target.x86_64-unknown-linux-gnu]
linker-flavor = "gnu-lld-cc"
[target.aarch64-unknown-linux-gnu]
linker-flavor = "gnu-lld-cc"
[target.x86_64-unknown-linux-musl]
linker-flavor = "gnu-lld-cc"
[target.aarch64-unknown-linux-musl]
linker-flavor = "gnu-lld-cc"
+2
View File
@@ -20,6 +20,8 @@ through npm aliases (`"tailwindcss": "npm:@aphrody/tailwindcss@<v>"`), so `@impo
- `bun install` (lockfile `bun.lock`, isolated linker). Workspaces, catalog, patched dependencies and the build allow
list live in the root `package.json`; `pnpm-workspace.yaml` and `pnpm-lock.yaml` are removed.
- `bun run build`, `bun run test` (`cargo test && bun test`), `bun run test:integrations`.
- `bun run build:fast` (`scripts/aphrody/fast-build.ts`): JS-only dev build of tailwindcss, node, postcss, cli and
browser with `Bun.build`; no oxide binding, no `.d.ts`, not used by CI or releases.
- Script rewrite (pnpm/npm/node/vitest to bun): `scripts/aphrody/bunify.ts` (`--check`, `--write`).
- Workspace helpers (replace `pnpm -r`): `scripts/aphrody/workspaces.ts`.
- Integration test projects are still installed with pnpm, like a user's project.
+1 -8
View File
@@ -3,11 +3,4 @@ resolver = "2"
members = ["crates/*"]
[profile.release]
lto = "thin"
codegen-units = 16
opt-level = 3
[profile.dev]
codegen-units = 256
incremental = true
debug = 1
lto = true
+1 -1
View File
@@ -34,7 +34,7 @@
"scripts": {
"format": "prettier --write .",
"lint": "prettier --check . && turbo lint",
"build": "bun ./scripts/aphrody/fast-build.ts",
"build": "turbo build --filter=!./playgrounds/*",
"build:fast": "bun ./scripts/aphrody/fast-build.ts",
"postbuild": "bun ./scripts/pack-packages.mjs",
"dev": "turbo dev --filter=!./playgrounds/*",
@@ -279,5 +279,3 @@ function runResolver(
}),
)
}
export * from './stream'
+1
View File
@@ -7,6 +7,7 @@ export * from './instrumentation'
export * from './normalize-path'
export * from './optimize'
export * from './source-maps'
export * from './stream'
export { env }
// In Bun, ESM modules will also populate `require.cache`, so the module hook is
+33 -29
View File
@@ -1,37 +1,41 @@
import { describe, expect, it } from 'bun:test'
import { compileStream, hashCandidates } from './index'
import { compileStream } from './index'
describe('@tailwindcss/node web features', () => {
it('hashes candidate lists consistently using crypto.subtle', async () => {
const list = ['flex', 'hidden', 'p-4', 'bg-red-500']
const hashA = await hashCandidates(list)
const hashB = await hashCandidates([...list])
expect(hashA).toBe(hashB)
expect(typeof hashA).toBe('string')
expect(hashA.length).toBe(64) // SHA-256 hex string
function streamOf(...chunks: string[]) {
let encoder = new TextEncoder()
return new ReadableStream<Uint8Array>({
start(controller) {
for (let chunk of chunks) controller.enqueue(encoder.encode(chunk))
controller.close()
},
})
}
const hashDifferent = await hashCandidates(['flex', 'hidden'])
expect(hashA).not.toBe(hashDifferent)
async function readAll(stream: ReadableStream<Uint8Array>) {
let decoder = new TextDecoder()
let output = ''
for await (let chunk of stream) output += decoder.decode(chunk, { stream: true })
return output + decoder.decode()
}
describe('compileStream', () => {
it('compiles a chunked CSS stream with the given candidates', async () => {
let output = await readAll(
compileStream(streamOf('@tailwind utilities;\n@utility foo-', 'bar { color: blue; }'), {
candidates: ['foo-bar', 'baz'],
}),
)
expect(output).toContain('.foo-bar')
expect(output).toContain('color: blue')
expect(output).not.toContain('baz')
})
it('compiles CSS via native Web Streams (ReadableStream)', async () => {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const css = '@utility foo-bar { color: blue; }'
it('emits no utilities without candidates', async () => {
let output = await readAll(
compileStream(streamOf('@tailwind utilities;\n@utility foo-bar { color: blue; }')),
)
const inStream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(css))
controller.close()
},
})
const outStream = compileStream(inStream)
let output = ''
for await (const chunk of outStream) {
output += decoder.decode(chunk)
}
expect(output).toContain('foo-bar')
expect(output).not.toContain('.foo-bar')
})
})
+18 -21
View File
@@ -1,36 +1,33 @@
import { compile, type CompileOptions } from './compile'
export async function hashCandidates(candidates: string[] | Iterable<string>): Promise<string> {
const encoder = new TextEncoder()
const content = Array.isArray(candidates) ? candidates.join('\0') : Array.from(candidates).join('\0')
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(content))
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
/**
* Compiles a CSS stream with `compile()` and emits the result of
* `compiler.build(candidates)`. The input is buffered until the stream ends.
* No source scanning happens here: pass the candidates explicitly.
*/
export function compileStream(
stream: ReadableStream<Uint8Array>,
options?: CompileOptions & { candidates?: string[] },
options: Partial<CompileOptions> & { candidates?: string[] } = {},
): ReadableStream<Uint8Array> {
const textDecoder = new TextDecoderStream()
const textEncoder = new TextEncoderStream()
let { candidates = [], ...compileOptions } = options
let buffer = ''
const transform = new TransformStream<string, string>({
let transform = new TransformStream<string, string>({
transform(chunk) {
buffer += chunk
},
async flush(controller) {
const compileOptions: CompileOptions = options ?? {
base: process.cwd(),
onDependency: () => {},
}
const compiler = await compile(buffer, compileOptions)
const compiled = compiler.build(options?.candidates ?? [])
controller.enqueue(compiled)
let compiler = await compile(buffer, {
...compileOptions,
base: compileOptions.base ?? process.cwd(),
onDependency: compileOptions.onDependency ?? (() => {}),
})
controller.enqueue(compiler.build(candidates))
},
})
return stream.pipeThrough(textDecoder).pipeThrough(transform).pipeThrough(textEncoder)
return stream
.pipeThrough(new TextDecoderStream())
.pipeThrough(transform)
.pipeThrough(new TextEncoderStream())
}
+1 -7
View File
@@ -1,13 +1,7 @@
import tailwindcss, * as allExports from './index.ts'
import tailwindcss from './index.ts'
// This is used instead of `export default` to work around a bug in
// `postcss-load-config`
for (let key in allExports) {
if (key === 'default') continue
// @ts-ignore
tailwindcss[key] = allExports[key]
}
// @ts-ignore
export = tailwindcss
+25 -88
View File
@@ -1,6 +1,5 @@
import QuickLRU from '@alloc/quick-lru'
import {
compile,
compileAst,
env,
Features,
@@ -28,7 +27,6 @@ interface CacheEntry {
cachedPostCssAst: Root
optimizedPostCssAst: Root
fullRebuildPaths: string[]
candidateHash: string
}
const cache = new QuickLRU<string, CacheEntry>({ maxSize: 50 })
@@ -46,7 +44,6 @@ function getContextFromCache(postcss: Postcss, inputFile: string, opts: PluginOp
optimizedPostCssAst: postcss.root(),
fullRebuildPaths: [] as string[],
candidateHash: '',
}
cache.set(key, entry)
return entry
@@ -249,12 +246,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
let candidates = compiler.features & Features.Utilities ? context.scanner.scan() : []
DEBUG && I.end('Scan for candidates')
DEBUG && I.start('Hash candidates')
let candidateHash = await hashCandidates(candidates)
let candidatesChanged = candidateHash !== context.candidateHash
context.candidateHash = candidateHash
DEBUG && I.end('Hash candidates')
if (compiler.features & Features.Utilities) {
DEBUG && I.start('Register dependency messages')
// Add all found files as direct dependencies
@@ -305,41 +296,39 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
DEBUG && I.end('Register dependency messages')
}
if (candidatesChanged || context.tailwindCssAst.length === 0 || rebuildStrategy === 'full') {
DEBUG && I.start('Build utilities')
let tailwindCssAst = compiler.build(candidates)
DEBUG && I.end('Build utilities')
DEBUG && I.start('Build utilities')
let tailwindCssAst = compiler.build(candidates)
DEBUG && I.end('Build utilities')
if (context.tailwindCssAst !== tailwindCssAst) {
if (optimize) {
DEBUG && I.start('Optimization')
if (context.tailwindCssAst !== tailwindCssAst) {
if (optimize) {
DEBUG && I.start('Optimization')
DEBUG && I.start('AST -> CSS')
let css = toCss(tailwindCssAst)
DEBUG && I.end('AST -> CSS')
DEBUG && I.start('AST -> CSS')
let css = toCss(tailwindCssAst)
DEBUG && I.end('AST -> CSS')
DEBUG && I.start('Lightning CSS')
let optimized = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : true,
})
DEBUG && I.end('Lightning CSS')
DEBUG && I.start('Lightning CSS')
let optimized = optimizeCss(css, {
minify: typeof optimize === 'object' ? optimize.minify : true,
})
DEBUG && I.end('Lightning CSS')
DEBUG && I.start('CSS -> PostCSS AST')
context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts)
DEBUG && I.end('CSS -> PostCSS AST')
DEBUG && I.start('CSS -> PostCSS AST')
context.optimizedPostCssAst = postcss.parse(optimized.code, result.opts)
DEBUG && I.end('CSS -> PostCSS AST')
DEBUG && I.end('Optimization')
} else {
// Convert our AST to a PostCSS AST
DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source)
DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
}
DEBUG && I.end('Optimization')
} else {
// Convert our AST to a PostCSS AST
DEBUG && I.start('Transform Tailwind CSS AST into PostCSS AST')
context.cachedPostCssAst = cssAstToPostCssAst(postcss, tailwindCssAst, root.source)
DEBUG && I.end('Transform Tailwind CSS AST into PostCSS AST')
}
context.tailwindCssAst = tailwindCssAst
}
context.tailwindCssAst = tailwindCssAst
DEBUG && I.start('Update PostCSS AST')
root.removeAll()
root.append(
@@ -386,55 +375,3 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
}
export default Object.assign(tailwindcss, { postcss: true }) as PluginCreator<PluginOptions>
export async function hashCandidates(candidates: string[] | Iterable<string>): Promise<string> {
const encoder = new TextEncoder()
const content = Array.isArray(candidates) ? candidates.join('\0') : Array.from(candidates).join('\0')
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoder.encode(content))
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
}
export function compileStream(
stream: ReadableStream<Uint8Array>,
opts: PluginOptions = {},
): ReadableStream<Uint8Array> {
const textDecoder = new TextDecoderStream()
const textEncoder = new TextEncoderStream()
let buffer = ''
const transform = new TransformStream<string, string>({
transform(chunk) {
buffer += chunk
},
async flush(controller) {
const base = opts.base ?? process.cwd()
const compiler = await compile(buffer, {
base,
onDependency: () => {},
shouldRewriteUrls: opts.transformAssetUrls ?? true,
})
const candidates: string[] = []
if (compiler.features & Features.Utilities) {
const sources = (() => {
if (compiler.root === 'none') return []
if (compiler.root === null) return [{ base, pattern: '**/*', negated: false }]
return [{ ...compiler.root, negated: false }]
})().concat(compiler.sources)
const scanner = new Scanner({ sources })
candidates.push(...scanner.scan())
}
let output = compiler.build(candidates)
const optimize = opts.optimize ?? process.env.NODE_ENV === 'production'
if (optimize) {
output = optimizeCss(output, {
minify: typeof opts.optimize === 'object' ? opts.optimize.minify : true,
}).code
}
controller.enqueue(output)
},
})
return stream.pipeThrough(textDecoder).pipeThrough(transform).pipeThrough(textEncoder)
}
@@ -1,37 +0,0 @@
import { describe, expect, it } from 'bun:test'
import { compileStream, hashCandidates } from './index'
describe('@tailwindcss/postcss web features', () => {
it('hashes candidate lists consistently using crypto.subtle', async () => {
const list = ['font-bold', 'text-center', 'm-2']
const hashA = await hashCandidates(list)
const hashB = await hashCandidates([...list])
expect(hashA).toBe(hashB)
expect(typeof hashA).toBe('string')
expect(hashA.length).toBe(64) // SHA-256 hex string
const hashDifferent = await hashCandidates(['font-bold', 'text-left'])
expect(hashA).not.toBe(hashDifferent)
})
it('compiles CSS via native Web Streams (ReadableStream)', async () => {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const css = '@utility test-stream { display: flex; }'
const inStream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(css))
controller.close()
},
})
const outStream = compileStream(inStream)
let output = ''
for await (const chunk of outStream) {
output += decoder.decode(chunk)
}
expect(output).toContain('test-stream')
})
})
+1 -1
View File
@@ -6,7 +6,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"upgrade": "bun scripts/upgrade.mjs"
},
"dependencies": {
"next": "16.2.7",
+24 -4
View File
@@ -1,5 +1,9 @@
// Fast parallel build pipeline using Bun.build for Tailwind CSS monorepo packages.
// Compiles packages/tailwindcss, @tailwindcss-postcss, @tailwindcss-node, @tailwindcss-cli, @tailwindcss-browser.
// `bun run build:fast`: JS-only development build with Bun.build, in parallel, of
// tailwindcss, @tailwindcss/postcss, @tailwindcss/node, @tailwindcss/cli and
// @tailwindcss/browser. It does not build the oxide binding (Rust), the other
// packages (vite, webpack, turbopack, upgrade, standalone) nor the .d.ts files,
// and tailwindcss/dist also gets index.* next to lib.*. Releases and CI use
// `bun run build` (turbo + tsup).
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
import { join, resolve } from 'node:path'
@@ -7,7 +11,7 @@ import { join, resolve } from 'node:path'
const ROOT = resolve(import.meta.dir, '..', '..')
const startTime = performance.now()
console.log('⚡ Starting Bun fast-build pipeline...')
console.log('fast-build: Bun.build of 5 packages')
async function buildTailwindCss() {
const pkgDir = join(ROOT, 'packages', 'tailwindcss')
@@ -228,6 +232,22 @@ async function buildBrowser() {
loader: {
'.css': 'text',
},
// Same stub as packages/@tailwindcss-browser/tsup.config.ts.
plugins: [
{
name: 'patch-intellisense-apis',
setup(build) {
build.onLoad({ filter: /intellisense\.ts$/ }, () => ({
loader: 'ts',
contents: `
export function getClassList() { return [] }
export function getVariants() { return [] }
export function canonicalizeCandidates() { return [] }
`,
}))
},
},
],
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.FEATURES_ENV': JSON.stringify('stable'),
@@ -252,7 +272,7 @@ const built = await Promise.all([
])
const durationMs = (performance.now() - startTime).toFixed(1)
console.log(`✅ Fast build completed in ${durationMs}ms:`)
console.log(`fast-build: done in ${durationMs}ms:`)
for (const name of built) {
console.log(` - ${name}`)
}
+11 -5
View File
@@ -1,9 +1,15 @@
// bun test preload: what packages/tailwindcss/vitest.config.mts sets for vitest.
// Needs `expect.addSnapshotSerializer` from the Aphrody Bun fork (984c0a12081).
// Needs `expect.addSnapshotSerializer` from the Aphrody Bun fork (984c0a12081);
// on another Bun the oklab snapshots of packages/tailwindcss will not match.
import { expect } from 'bun:test'
import serializer from '../../packages/tailwindcss/src/test-utils/custom-serializer.ts'
try {
;(expect as any).addSnapshotSerializer?.(serializer as any)
} catch {}
let addSnapshotSerializer = (expect as any).addSnapshotSerializer
if (typeof addSnapshotSerializer === 'function') {
addSnapshotSerializer(serializer)
} else {
console.warn(
`test-preload: expect.addSnapshotSerializer is missing in Bun ${Bun.version}; ` +
'use the Aphrody Bun fork (package.json packageManager) for snapshot parity.',
)
}