Skip to content

Commit 0c5f8bf

Browse files
authored
Ignore the standalone CLI (#20139)
This PR fixes an issue where if you use the standalone CLI, and you move the standalone CLI into the current project, then we would scan that standalone CLI as-if it contains Tailwind CSS classes. Since the CLI contains actual Tailwind CSS classes, and is in fact readable text, this binary would've been used as a source. There are a few ways of fixing this, we could hardcode all the known names, but that would result in an issue if you rename the CLI. We could check whether it's a binary format and look for magic numbers at the top. We could also check for a shebang at the top of the file and skip it that way. While some of these solutions might still be useful for the future. For now I fixed it by essentially always ignoring `process.execPath`. That way we never ever scan the actual executable regardless of whether you renamed it or not. Fixes: #20134 ## Test plan - Added an integration tests - Works on every OS [ci-all]
1 parent 44818a6 commit 0c5f8bf

3 files changed

Lines changed: 48 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Canonicalization: don't suggest invalid `calc(…)` expressions (e.g. `px-[calc(1rem+0px)]``px-[calc(1rem+0)]`) ([#20127](https://github.com/tailwindlabs/tailwindcss/pull/20127))
2424
- Canonicalization: avoid suggesting large spacing-scale values for arbitrary lengths (e.g. `left-[99999px]``left-[99999px]`, not `left-24999.75`) ([#20130](https://github.com/tailwindlabs/tailwindcss/pull/20130))
2525
- Ensure `@tailwindcss/cli` in `--watch` mode recovers when a tracked dependency is deleted and restored ([#20137](https://github.com/tailwindlabs/tailwindcss/pull/20137))
26+
- Ensure standalone `@tailwindcss/cli` binaries are ignored when scanning for class candidates ([#20139](https://github.com/tailwindlabs/tailwindcss/pull/20139))
2627

2728
## [4.3.0] - 2026-05-08
2829

integrations/cli/standalone.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import nodeFs from 'node:fs/promises'
12
import os from 'node:os'
23
import path from 'node:path'
3-
import { candidate, css, html, js, json, test, ts } from '../utils'
4+
import { candidate, css, html, IS_WINDOWS, js, json, test, ts } from '../utils'
45

56
const STANDALONE_BINARY = (() => {
67
switch (os.platform()) {
@@ -15,6 +16,41 @@ const STANDALONE_BINARY = (() => {
1516
}
1617
})()
1718

19+
test(
20+
'does not scan itself for candidates',
21+
{
22+
fs: {
23+
'package.json': json`
24+
{
25+
"dependencies": {}
26+
}
27+
`,
28+
'src/index.css': css` @import 'tailwindcss'; `,
29+
},
30+
},
31+
async ({ root, fs, exec }) => {
32+
let sourceBinary = path.resolve(
33+
__dirname,
34+
`../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`,
35+
)
36+
let binary = IS_WINDOWS ? 'tailwindcss.exe' : 'tailwindcss'
37+
let localBinary = path.join(root, binary)
38+
await nodeFs.copyFile(sourceBinary, localBinary)
39+
40+
if (!IS_WINDOWS) {
41+
await nodeFs.chmod(localBinary, 0o755)
42+
}
43+
44+
await exec(`${IS_WINDOWS ? binary : `./${binary}`} --input src/index.css --output dist/out.css`)
45+
46+
await fs.expectFileNotToContain('dist/out.css', [
47+
candidate`flex`,
48+
candidate`grid`,
49+
candidate`underline`,
50+
])
51+
},
52+
)
53+
1854
test(
1955
'includes first-party plugins',
2056
{

packages/@tailwindcss-cli/src/commands/build/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ export async function handle(args: Result<ReturnType<typeof options>>) {
250250
return [{ ...compiler.root, negated: false }]
251251
})().concat(compiler.sources)
252252

253+
// Do not scan the current executable. Otherwise when using the standalone
254+
// CLI, if the CLI lives in the current repo we would be scanning that file.
255+
//
256+
// This is also immune against renames of the executable file.
257+
sources.push({
258+
base: path.dirname(process.execPath),
259+
pattern: path.basename(process.execPath),
260+
negated: true,
261+
})
262+
253263
let scanner = new Scanner({ sources })
254264
DEBUG && I.end('Setup compiler')
255265

0 commit comments

Comments
 (0)