Don't inline all env vars in Standalone CLI (#19391)

Fixes #19389

We inlined env vars in the Standalone CLI because we use some custom
patches + env vars to ensure that only the appropriate `glibc` / `musl`
binaries are included for Lightning CSS and Parcel Watcher for Linux
builds.

The build happens to run on a macOS Github CI machine though so
`NODE_PATH` was getting inlined as the string:
```
/Users/runner/work/tailwindcss/tailwindcss/node_modules/.pnpm/bun@1.3.0/node_modules/bun/bin/node_modules
```

I don't think there's a reason for `NODE_PATH` to work on the Standalone
CLI (and it didn't work because of the above bug *anyway*) so I've done
a few things here:
1. The build setup now uses `Bun.build(…)` which now supports compiling
binaries. This speeds up the build process a bit.
2. We're no longer inlining all env vars. We selectively inline only a
few using `define`.
3. I've explicitly disabled the extra `NODE_PATH` support in
`@tailwindcss/node` when building with the Standalone CLI.
4. The `__tw_readFile` hack is now gone. Async FS APIs were not
originally able to read embedded files but that changed in Bun v1.2.3
making the hack unnecessary.
5. A few more env vars are now inlined + a plugin to simplify the Oxide
loading code when bundled.
6. A plugin + env vars prevents bundling WASI build as it's not
necessary for the Standalone CLI.

I want to find a way to get rid of `__tw_resolve` and `__tw_load` but
don't want to change too much in this PR so I haven't looked into it
yet.
This commit is contained in:
Jordan Pittman 2025-12-08 11:23:20 -05:00 committed by GitHub
parent 820d90797c
commit cdc851d3f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 82 deletions

View File

@ -166,17 +166,6 @@ async function loadStylesheet(
onDependency(resolvedPath)
if (typeof globalThis.__tw_readFile === 'function') {
let file = await globalThis.__tw_readFile(resolvedPath, 'utf-8')
if (file) {
return {
path: resolvedPath,
base: path.dirname(resolvedPath),
content: file,
}
}
}
let file = await fsPromises.readFile(resolvedPath, 'utf-8')
return {
path: resolvedPath,

View File

@ -1,4 +1,3 @@
import { $ } from 'bun'
import { createHash } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import * as path from 'node:path'
@ -6,80 +5,110 @@ import { fileURLToPath } from 'node:url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
async function buildForPlatform(triple: string, outfile: string) {
// We wrap this in a retry because occasionally the atomic rename fails for some reason
for (let i = 0; i < 5; ++i) {
try {
let cmd = $`bun build --compile --target=${triple} ./src/index.ts --outfile=${outfile} --env inline`
// Workaround for Bun binary downloads failing on Windows CI when
// USERPROFILE is passed through by Turborepo.
//
// Unfortunately, setting this at runtime doesn't appear to work so we have to
// spawn a new process without the env var.
if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') {
let result = await Bun.$`bun ${fileURLToPath(import.meta.url)}`.env({
USERPROFILE: '',
NESTED_BUILD: '1',
})
// This env var is used by our patched versions of Lightning CSS and Parcel Watcher to
// statically bundle the proper binaries for musl vs glibc
cmd = cmd.env({
PLATFORM_LIBC: triple.includes('-musl') ? 'musl' : 'glibc',
// Workaround for Bun binary downloads failing on Windows CI when
// USERPROFILE is passed through by Turborepo.
USERPROFILE: '',
})
return await cmd
} catch (err) {
if (i < 5) continue
throw new Error(`Failed to build for platform ${triple}`, { cause: err })
}
}
process.exit(result.exitCode)
}
async function build(triple: string, file: string) {
let start = process.hrtime.bigint()
// We use baseline builds for all x64 platforms to ensure compatibility with
// older hardware.
let builds: { target: Bun.Build.Target; name: string }[] = [
{ name: 'tailwindcss-linux-arm64', target: 'bun-linux-arm64' },
{ name: 'tailwindcss-linux-arm64-musl', target: 'bun-linux-arm64-musl' },
// @ts-expect-error: Either the types are wrong or the runtime needs to be updated
// to accept a `-glibc` at the end like the types suggest.
{ name: 'tailwindcss-linux-x64', target: 'bun-linux-x64-baseline' },
{ name: 'tailwindcss-linux-x64-musl', target: 'bun-linux-x64-baseline-musl' },
{ name: 'tailwindcss-macos-arm64', target: 'bun-darwin-arm64' },
{ name: 'tailwindcss-macos-x64', target: 'bun-darwin-x64-baseline' },
{ name: 'tailwindcss-windows-x64.exe', target: 'bun-windows-x64-baseline' },
]
let outfile = path.resolve(__dirname, `../dist/${file}`)
let summary: { target: Bun.Build.Target; name: string; sum: string }[] = []
await buildForPlatform(triple, outfile)
// Build platform binaries and checksum them.
let start = process.hrtime.bigint()
for (let { target, name } of builds) {
let outfile = path.resolve(__dirname, `../dist/${name}`)
await new Promise((resolve) => setTimeout(resolve, 100))
let result = await Bun.build({
entrypoints: ['./src/index.ts'],
target: 'node',
minify: {
whitespace: false,
syntax: true,
identifiers: false,
keepNames: true,
},
define: {
// This ensures only necessary binaries are bundled for linux targets
// It reduces binary size since no runtime selection is necessary
'process.env.PLATFORM_LIBC': JSON.stringify(target.includes('-musl') ? 'musl' : 'glibc'),
// This prevents the WASI build from being bundled with the binary
'process.env.NAPI_RS_FORCE_WASI': JSON.stringify(''),
// This simplifies the Oxide loading code a small amount
'process.env.NAPI_RS_NATIVE_LIBRARY_PATH': JSON.stringify(''),
// No need to support additional NODE_PATHs in the standalone build
'process.env.NODE_PATH': JSON.stringify(''),
},
compile: {
target,
outfile,
// Disable .env loading
autoloadDotenv: false,
// Disable bunfig.toml loading
autoloadBunfig: false,
},
plugins: [
{
name: 'tailwindcss-plugin',
setup(build) {
build.onLoad({ filter: /tailwindcss-oxide\.wasi\.cjs$/ }, async (args) => {
return { contents: '' }
})
},
},
],
})
let entry = result.outputs.find((output) => output.kind === 'entry-point')
if (!entry) throw new Error(`Build failed for ${target}`)
let content = await readFile(outfile)
let sum = createHash('sha256').update(content).digest('hex')
let elapsed = process.hrtime.bigint() - start
return {
triple,
file,
sum,
elapsed,
}
summary.push({
target,
name,
sum: createHash('sha256').update(content).digest('hex'),
})
}
await mkdir(path.resolve(__dirname, '../dist'), { recursive: true })
// Build platform binaries and checksum them. We use baseline builds for all x64 platforms to ensure
// compatibility with older hardware.
let results = await Promise.all([
build('bun-linux-arm64', './tailwindcss-linux-arm64'),
build('bun-linux-arm64-musl', './tailwindcss-linux-arm64-musl'),
build('bun-linux-x64-baseline', './tailwindcss-linux-x64'),
build('bun-linux-x64-musl-baseline', './tailwindcss-linux-x64-musl'),
build('bun-darwin-arm64', './tailwindcss-macos-arm64'),
build('bun-darwin-x64-baseline', './tailwindcss-macos-x64'),
build('bun-windows-x64-baseline', './tailwindcss-windows-x64.exe'),
])
// Write the checksums to a file
let sumsFile = path.resolve(__dirname, '../dist/sha256sums.txt')
let sums = results.map(({ file, sum }) => `${sum} ${file}`)
console.table(
results.map(({ triple, sum, elapsed }) => ({
triple,
sum,
elapsed: `${(Number(elapsed) / 1e6).toFixed(0)}ms`,
})),
)
let sums = summary.map(({ name, sum }) => `${sum} ./${name}`)
await writeFile(sumsFile, sums.join('\n') + '\n')
console.table(summary.map(({ target, sum }) => ({ target, sum })))
let elapsed = process.hrtime.bigint() - start
console.log(`Build completed in ${(Number(elapsed) / 1e6).toFixed(0)}ms`)

View File

@ -1,4 +1,3 @@
import fs from 'node:fs'
import { createRequire } from 'node:module'
import packageJson from 'tailwindcss/package.json'
@ -61,15 +60,6 @@ globalThis.__tw_load = async (id) => {
}
}
globalThis.__tw_version = packageJson.version
globalThis.__tw_readFile = async (path, encoding) => {
// When reading a file from the `$bunfs`, we need to use the synchronous
// `readFileSync` API
let isEmbeddedFileBase = path.includes('/$bunfs/root') || path.includes(':/~BUN/root')
if (!isEmbeddedFileBase) {
return
}
return fs.readFileSync(path, encoding)
}
// We use a plugin to make sure that the JS APIs are bundled with the standalone
// CLI and can be imported inside configs and plugins