Skip to content

Conversation

@thecrypticace
Copy link
Contributor

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/[email protected]/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.

Need to test all platforms so: [ci-all]

@thecrypticace thecrypticace requested a review from a team as a code owner November 30, 2025 13:46
@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Walkthrough

This pull request removes a synchronous file-reading fast-path from the Tailwind CSS node and standalone packages. The globalThis.__tw_readFile function and its associated fs import are eliminated, requiring stylesheet content to always be loaded asynchronously via fsPromises. Additionally, the standalone package's build script is refactored from per-platform retry-wrapped logic into a centralized, multi-target build system that supports x64 and arm64 architectures across Linux, macOS, and Windows with musl/glibc variants. The new build process computes SHA-256 checksums for each produced binary and implements a Windows-specific workaround for environmental issues.

Pre-merge checks

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Don't inline all env vars in Standalone CLI' accurately summarizes the main change, which is replacing the practice of inlining all environment variables with selective inlining.
Description check ✅ Passed The description clearly relates to the changeset, explaining the rationale for removing inlined env vars, the bug they caused, and the specific implementation changes made across all modified files.
Linked Issues check ✅ Passed The changes address the core objectives from #19389: preventing NODE_PATH from macOS CI being embedded in the binary, making path resolution platform-aware, and removing the __tw_readFile hack that relied on global state.
Out of Scope Changes check ✅ Passed All code changes directly support the primary objectives: removing env var inlining, disabling NODE_PATH support, removing the __tw_readFile hack, and improving the Bun.build configuration to prevent platform-specific paths from being embedded.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 243615e and decb9e6.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • packages/@tailwindcss-node/src/compile.ts (0 hunks)
  • packages/@tailwindcss-standalone/package.json (1 hunks)
  • packages/@tailwindcss-standalone/scripts/build.ts (1 hunks)
  • packages/@tailwindcss-standalone/src/index.ts (0 hunks)
  • playgrounds/vite/package.json (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/@tailwindcss-node/src/compile.ts
  • packages/@tailwindcss-standalone/src/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: macOS / oxide
  • GitHub Check: macOS / postcss
  • GitHub Check: macOS / webpack
  • GitHub Check: macOS / upgrade
  • GitHub Check: macOS / vite
  • GitHub Check: Windows / webpack
  • GitHub Check: Windows / oxide
  • GitHub Check: Windows / postcss
  • GitHub Check: Windows / cli
  • GitHub Check: Windows / upgrade
  • GitHub Check: Windows / vite
  • GitHub Check: Windows
🔇 Additional comments (6)
playgrounds/vite/package.json (1)

21-21: LGTM! Consistent version alignment.

The Bun version bump to ^1.3.3 aligns with the same update in the standalone package. Assuming the version verification from the standalone package confirms validity, this change maintains consistency across the repository.

packages/@tailwindcss-standalone/scripts/build.ts (5)

14-22: LGTM! Comprehensive platform coverage.

The build target array covers all major platforms with appropriate baseline targets for x64 to ensure broader hardware compatibility. The structure is clear and maintainable.


41-54: Excellent! This fix directly addresses issue #19389.

The define section now selectively controls environment variables at build time, preventing platform-specific paths from being inlined. Key improvements:

  • NODE_PATH is explicitly set to empty string, preventing macOS-specific paths from being embedded in binaries for other platforms.
  • PLATFORM_LIBC is dynamically determined per target, ensuring correct runtime behavior.
  • NAPI_RS_* variables are controlled to reduce bundle size and prevent unnecessary WASI builds.

This approach ensures each platform binary contains only appropriate paths for its target OS, eliminating the cross-platform filesystem lookup issues described in #19389.


56-65: LGTM! Appropriate compile configuration.

Disabling autoloadDotenv and autoloadBunfig is correct for a standalone CLI, ensuring consistent behavior across different user environments. The target and outfile settings are properly configured.


67-76: LGTM! Plugin correctly excludes WASI build.

The plugin properly prevents the WASI build from being bundled by returning empty contents for tailwindcss-oxide.wasi.cjs. This, combined with the NAPI_RS_FORCE_WASI define, ensures WASI code is not included in the standalone binaries, reducing size and complexity.


8-10: The USERPROFILE workaround is properly isolated to this build script and doesn't impact other code paths. Since no other files reference this variable, setting it to an empty string is a safe, targeted fix for the Bun binary download issue on Windows CI.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/@tailwindcss-standalone/scripts/build.ts (1)

97-97: Checksum format is now correct.

The double-prefix issue flagged in the previous review has been resolved. The current code correctly uses ./${name} instead of ./tailwindcss-${name}, producing proper checksum entries like <hash> ./tailwindcss-linux-arm64.

🧹 Nitpick comments (1)
packages/@tailwindcss-standalone/scripts/build.ts (1)

81-82: Consider checking build success status.

The entry-point validation is good, but you could optionally check result.success or inspect result.logs to catch build warnings or failures earlier with more context. The current approach relies on Bun.build throwing on failure, which may be sufficient.

  let entry = result.outputs.find((output) => output.kind === 'entry-point')
- if (!entry) throw new Error(`Build failed for ${target}`)
+ if (!entry || !result.success) {
+   console.error(result.logs)
+   throw new Error(`Build failed for ${target}`)
+ }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between decb9e6 and 581867d.

📒 Files selected for processing (1)
  • packages/@tailwindcss-standalone/scripts/build.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: macOS
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Linux / cli
  • GitHub Check: macOS / oxide
  • GitHub Check: macOS / webpack
  • GitHub Check: Linux / postcss
  • GitHub Check: macOS / vite
  • GitHub Check: Linux / webpack
  • GitHub Check: macOS / upgrade
  • GitHub Check: Linux / upgrade
  • GitHub Check: Linux / vite
  • GitHub Check: Windows / oxide
  • GitHub Check: Windows / cli
  • GitHub Check: Windows / webpack
  • GitHub Check: Windows / upgrade
  • GitHub Check: Windows / postcss
  • GitHub Check: Windows / vite
🔇 Additional comments (4)
packages/@tailwindcss-standalone/scripts/build.ts (4)

8-10: LGTM—documented CI workaround.

The USERPROFILE clearing is intentional for Windows CI compatibility with Bun binary downloads. The comment clearly documents the rationale.


14-24: Build matrix covers all platforms appropriately.

The target definitions are comprehensive and well-structured. The TypeScript suppression at lines 17-18 is acceptable given the documented type/runtime mismatch.


33-42: Build configuration is appropriate for standalone binaries.

The Bun.build configuration correctly:

  • Targets Node compatibility
  • Preserves names for better debugging
  • Disables .env and bunfig loading (per PR objectives to prevent environment-specific config leakage)

Also applies to: 58-67


69-78: WASI exclusion plugin correctly prevents WASI build bundling.

The plugin filters out tailwindcss-oxide.wasi.cjs by returning empty contents, which aligns with the PR objective to exclude WASI builds from the standalone binary and works in conjunction with the NAPI_RS_FORCE_WASI define.

@thecrypticace
Copy link
Contributor Author

Apparently setting process.env.USERPROFILE = '' at runtime doesn't let Bun.build work. I'm guessing it needs to spawn a separate command with that removed. Maybe some init time setup stuff gets cached.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/@tailwindcss-standalone/scripts/build.ts (2)

13-13: Optional: Simplify redundant USERPROFILE check.

The condition process.env.USERPROFILE !== '' is redundant because the preceding check process.env.USERPROFILE already ensures the variable is truthy (non-empty and defined).

Apply this diff to simplify:

-if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') {
+if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE) {

40-101: Consider adding error handling for individual build failures.

The build loop doesn't wrap individual builds in try-catch blocks, meaning a single platform build failure will terminate the entire process without producing artifacts for subsequent platforms. While fail-fast behavior might be intentional, adding error handling could improve resilience by allowing successful builds to complete and reporting all failures at the end.

If you want to allow partial success, consider wrapping each build iteration:

let errors: Array<{ target: string; error: Error }> = []

for (let { target, name } of builds) {
  try {
    // ... existing build logic ...
  } catch (error) {
    errors.push({ target: name, error: error as Error })
    console.error(`Build failed for ${name}:`, error)
    continue
  }
}

if (errors.length > 0) {
  console.error(`\n${errors.length} build(s) failed`)
  process.exit(1)
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 581867d and 297d884.

📒 Files selected for processing (1)
  • packages/@tailwindcss-standalone/scripts/build.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: macOS / webpack
  • GitHub Check: macOS / cli
  • GitHub Check: macOS / oxide
  • GitHub Check: macOS / upgrade
  • GitHub Check: Linux / webpack
  • GitHub Check: Linux / upgrade
  • GitHub Check: Linux / oxide
  • GitHub Check: Linux / postcss
  • GitHub Check: Linux / vite
  • GitHub Check: Linux / cli
  • GitHub Check: Windows / upgrade
  • GitHub Check: Windows / postcss
  • GitHub Check: Windows / webpack
  • GitHub Check: Windows / oxide
  • GitHub Check: Windows / vite
  • GitHub Check: Windows / cli
  • GitHub Check: macOS
  • GitHub Check: Windows
  • GitHub Check: Linux
🔇 Additional comments (3)
packages/@tailwindcss-standalone/scripts/build.ts (3)

24-41: LGTM! Well-structured multi-target build configuration.

The target array clearly defines all platform variants with appropriate naming. The baseline builds for x64 ensure compatibility with older hardware, and the type mismatch is appropriately acknowledged.


107-107: LGTM! Checksum format correctly fixed.

The checksum file format now correctly uses ./${name} without adding an extra tailwindcss- prefix, as noted in the past review comment.


53-66: No action needed. The PLATFORM_LIBC environment variable is only referenced in Linux-specific code paths (guarded by process.platform === 'linux' checks in the lightningcss and @parcel/watcher patches). Setting it for all platforms with a default value of 'glibc' is safe and does not cause issues on macOS or Windows.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/@tailwindcss-standalone/scripts/build.ts (1)

79-88: Consider specifying the loader type explicitly.

The plugin returns empty contents without specifying a loader. While Bun may handle this correctly, explicitly adding loader: 'js' would make the intent clearer.

         setup(build) {
           build.onLoad({ filter: /tailwindcss-oxide\.wasi\.cjs$/ }, async (args) => {
-            return { contents: '' }
+            return { contents: '', loader: 'js' }
           })
         },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 297d884 and 9caa7b4.

📒 Files selected for processing (1)
  • packages/@tailwindcss-standalone/scripts/build.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: macOS / oxide
  • GitHub Check: macOS / webpack
  • GitHub Check: macOS / cli
  • GitHub Check: macOS / upgrade
  • GitHub Check: Linux / postcss
  • GitHub Check: Linux / vite
  • GitHub Check: Linux / cli
  • GitHub Check: Windows / webpack
  • GitHub Check: Linux / upgrade
  • GitHub Check: Windows / cli
  • GitHub Check: Windows / oxide
  • GitHub Check: Windows / postcss
  • GitHub Check: Windows / upgrade
  • GitHub Check: Windows / vite
  • GitHub Check: Windows
  • GitHub Check: Linux
  • GitHub Check: macOS
🔇 Additional comments (2)
packages/@tailwindcss-standalone/scripts/build.ts (2)

8-20: Windows workaround is appropriately documented and implemented.

The nested process spawn to clear USERPROFILE addresses a specific Bun binary download issue on Windows CI. The comment clearly explains why runtime modification doesn't work.


107-114: Checksum format and output are correct.

The checksum format correctly uses the name as-is without adding a duplicate prefix, and the summary output provides useful build information.

@thecrypticace thecrypticace marked this pull request as draft December 1, 2025 13:03
@thecrypticace
Copy link
Contributor Author

thecrypticace commented Dec 1, 2025

Some .node modules aren't actually being bundled properly. Need to investigate why.

edit: This was just a local environment problem.

@thecrypticace thecrypticace marked this pull request as ready for review December 1, 2025 18:07
This is equivalent to the old build setup but doesn’t need to use CLI commands to build the binaries
Right now `NODE_PATH` gets inlined into the build causing linux binaries to additionally search for `/Users/runner/…` directories
Async APIs became capable of reading embedded files in Bun v1.2.3
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/@tailwindcss-standalone/scripts/build.ts (1)

27-28: Remove the @ts-expect-error suppression.

This was flagged in a previous review: Bun 1.3.3 types support bun-linux-x64-baseline as a valid target in the form bun-linux-x64-${SIMD}-${Libc}, where baseline is a valid SIMD value. The suppression is no longer necessary.

Apply this diff:

-  // @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' },
🧹 Nitpick comments (1)
packages/@tailwindcss-standalone/scripts/build.ts (1)

8-20: Windows workaround looks correct.

The nested process approach with the NESTED_BUILD flag prevents infinite recursion, and clearing USERPROFILE in the spawned process aligns with the PR objectives to work around Bun.build's separate command spawning behavior.

Optional: Simplify the USERPROFILE check.

Line 13's check process.env.USERPROFILE !== '' is redundant since process.env.USERPROFILE already checks truthiness. Consider simplifying:

-if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE && process.env.USERPROFILE !== '') {
+if (process.env.NESTED_BUILD !== '1' && process.env.USERPROFILE) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9caa7b4 and 9a2048b.

📒 Files selected for processing (3)
  • packages/@tailwindcss-node/src/compile.ts (0 hunks)
  • packages/@tailwindcss-standalone/scripts/build.ts (1 hunks)
  • packages/@tailwindcss-standalone/src/index.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • packages/@tailwindcss-node/src/compile.ts
  • packages/@tailwindcss-standalone/src/index.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-01T11:27:16.037Z
Learnt from: thecrypticace
Repo: tailwindlabs/tailwindcss PR: 19391
File: packages/@tailwindcss-standalone/scripts/build.ts:43-92
Timestamp: 2025-12-01T11:27:16.037Z
Learning: Bun.build throws by default (throws: true is the default behavior). The result.success property is only false when throws: false is explicitly configured. Therefore, no need to check result.success unless throws: false is set in the build options.

Applied to files:

  • packages/@tailwindcss-standalone/scripts/build.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: macOS / cli
  • GitHub Check: Windows
  • GitHub Check: Windows / upgrade
🔇 Additional comments (4)
packages/@tailwindcss-standalone/scripts/build.ts (4)

91-92: Entry point validation is appropriate.

The check ensures the build produced the expected entry-point output. Since Bun.build throws by default (per retrieved learnings), this validation confirms the output structure without redundant success checks.


94-109: Checksum computation and file format are correct.

The SHA-256 hash calculation and the checksum file format (${sum} ./${name}) correctly produce standard checksum entries. The previous double-prefix concern was addressed.


111-114: Summary output provides clear build feedback.

The consolidated console.table with checksums and elapsed time improves developer experience compared to verbose per-target logging.


43-89: Build configuration correctly implements the PR objectives.

The configuration properly:

  • Sets PLATFORM_LIBC based on target to prevent embedding macOS paths
  • Clears NODE_PATH to disable extra path support per PR goals
  • Disables WASI bundling via both define and plugin
  • Disables dotenv/bunfig loading for the standalone binary

The WASI filename filter /tailwindcss-oxide\.wasi\.cjs$/ is correct and matches the main entry point defined in crates/node/npm/wasm32-wasi/package.json.

// 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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursed, I love it.

@thecrypticace thecrypticace merged commit cdc851d into main Dec 8, 2025
21 checks passed
@thecrypticace thecrypticace deleted the fix/issue-19389 branch December 8, 2025 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standalone CLI looking in hard-coded macOS directories

3 participants