diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index bad1dfe1..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -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" diff --git a/APHRODY.md b/APHRODY.md index 38f9b803..fff86f8b 100644 --- a/APHRODY.md +++ b/APHRODY.md @@ -20,6 +20,8 @@ through npm aliases (`"tailwindcss": "npm:@aphrody/tailwindcss@"`), 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. diff --git a/Cargo.toml b/Cargo.toml index 6f43f218..6d2d5a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/package.json b/package.json index b4537ef1..7348a6b9 100644 --- a/package.json +++ b/package.json @@ -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/*", diff --git a/packages/@tailwindcss-node/src/compile.ts b/packages/@tailwindcss-node/src/compile.ts index 1d291c72..e5f8782c 100644 --- a/packages/@tailwindcss-node/src/compile.ts +++ b/packages/@tailwindcss-node/src/compile.ts @@ -279,5 +279,3 @@ function runResolver( }), ) } - -export * from './stream' diff --git a/packages/@tailwindcss-node/src/index.cts b/packages/@tailwindcss-node/src/index.cts index 9f278893..fa6cc87d 100644 --- a/packages/@tailwindcss-node/src/index.cts +++ b/packages/@tailwindcss-node/src/index.cts @@ -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 diff --git a/packages/@tailwindcss-node/src/stream.test.ts b/packages/@tailwindcss-node/src/stream.test.ts index c77d2edb..49cf5f0d 100644 --- a/packages/@tailwindcss-node/src/stream.test.ts +++ b/packages/@tailwindcss-node/src/stream.test.ts @@ -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({ + 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) { + 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') }) }) diff --git a/packages/@tailwindcss-node/src/stream.ts b/packages/@tailwindcss-node/src/stream.ts index 54a762a9..ef7d9733 100644 --- a/packages/@tailwindcss-node/src/stream.ts +++ b/packages/@tailwindcss-node/src/stream.ts @@ -1,36 +1,33 @@ import { compile, type CompileOptions } from './compile' -export async function hashCandidates(candidates: string[] | Iterable): Promise { - 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, - options?: CompileOptions & { candidates?: string[] }, + options: Partial & { candidates?: string[] } = {}, ): ReadableStream { - const textDecoder = new TextDecoderStream() - const textEncoder = new TextEncoderStream() + let { candidates = [], ...compileOptions } = options let buffer = '' - const transform = new TransformStream({ + let transform = new TransformStream({ 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()) } diff --git a/packages/@tailwindcss-postcss/src/index.cts b/packages/@tailwindcss-postcss/src/index.cts index 05410533..abc7c2e2 100644 --- a/packages/@tailwindcss-postcss/src/index.cts +++ b/packages/@tailwindcss-postcss/src/index.cts @@ -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 diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 2cf899f8..a4a8d932 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -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({ 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 - -export async function hashCandidates(candidates: string[] | Iterable): Promise { - 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, - opts: PluginOptions = {}, -): ReadableStream { - const textDecoder = new TextDecoderStream() - const textEncoder = new TextEncoderStream() - let buffer = '' - - const transform = new TransformStream({ - 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) -} diff --git a/packages/@tailwindcss-postcss/src/stream.test.ts b/packages/@tailwindcss-postcss/src/stream.test.ts deleted file mode 100644 index c944a15a..00000000 --- a/packages/@tailwindcss-postcss/src/stream.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/playgrounds/v3/package.json b/playgrounds/v3/package.json index 43875687..46d0232a 100644 --- a/playgrounds/v3/package.json +++ b/playgrounds/v3/package.json @@ -6,7 +6,7 @@ "build": "next build", "start": "next start", "lint": "next lint", - + "upgrade": "bun scripts/upgrade.mjs" }, "dependencies": { "next": "16.2.7", diff --git a/scripts/aphrody/fast-build.ts b/scripts/aphrody/fast-build.ts index 125a296a..28b7cfec 100644 --- a/scripts/aphrody/fast-build.ts +++ b/scripts/aphrody/fast-build.ts @@ -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}`) } diff --git a/scripts/aphrody/test-preload.ts b/scripts/aphrody/test-preload.ts index ff470a98..fcb008d0 100644 --- a/scripts/aphrody/test-preload.ts +++ b/scripts/aphrody/test-preload.ts @@ -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.', + ) +}