Skip to content

Commit d9e3fd6

Browse files
philipp-spiessthecrypticaceRobinMalfait
authored
Add standalone CLI (#14270)
This PR adds a new standalone client: A single-binary file that you can use to run Tailwind v4 without having a node setup. To make this work we use Bun's single-binary build which can properly package up native modules and the bun runtime for us so we do not have to rely on any expand-into-tmp-folder-at-runtime workarounds. When running locally, `pnpm build` will now standalone artifacts inside `packages/@tailwindcss-standalone/dist`. Note that since we do not build Oxide for other environments in the local setup, you won't be able to use the standalone artifacts for other platforms in local dev mode. Unfortunately Bun does not have support for Windows ARM builds yet but we found that using the `bun-baseline` runtime for Windows x64 would make the builds work fine in ARM emulation mode: ![Screenshot windows](https://github.com/user-attachments/assets/5b39387f-ec50-4757-9469-19b98e43162d) Some Bun related issues we faced and worked around: - We found that the regular Windows x64 build of `bun` does not run on Windows ARM via emulation. Instead, we have to use the `bun-baseline` builds which emulate correctly. - When we tried to bundle artifacts with [embed directories](https://bun.sh/docs/bundler/executables#embed-directories), node binary dependencies were no longer resolved correctly even though they would still be bundled and accessible within the [`embeddedFiles` list](https://bun.sh/docs/bundler/executables#listing-embedded-files). We worked around this by using the `import * as from ... with { type: "file" };` and patching the resolver we use in our CLI. - If you have an import to a module that is used as a regular import _and_ a `with { type: "file" }`, it will either return the module in both cases _or_ the file path when we would expect only the `with { type: "file" }` import to return the path. We do read the Tailwind CSS version via the file system and `require.resolve()` in the CLI and via `import * from './package.json'` in core and had to work around this by patching the version resolution in our CLI. ```ts import packageJson from "./package.json" import packageJsonPath from "./package.json" with {type: "file"} // We do not expect these to be equal packageJson === packageJsonPath ``` - We can not customize the app icon used for Windows `.exe` builds without decompiling the binary. For now we will leave the default but one workaround is to [use tools like ResourceHacker](698d9c4) to decompile the binary first. --------- Co-authored-by: Jordan Pittman <[email protected]> Co-authored-by: Robin Malfait <[email protected]>
1 parent ac6d4a6 commit d9e3fd6

File tree

19 files changed

+816
-194
lines changed

19 files changed

+816
-194
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fail-fast: false
1515
matrix:
1616
node-version: [20]
17-
runner: [ubuntu-latest, windows-latest]
17+
runner: [ubuntu-latest, windows-latest, macos-14]
1818

1919
runs-on: ${{ matrix.runner }}
2020
timeout-minutes: 15

.github/workflows/release.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,6 @@ jobs:
188188
- name: Install dependencies
189189
run: pnpm --filter=!./playgrounds/* install --ignore-scripts
190190

191-
- name: Build Tailwind CSS
192-
run: pnpm run build
193-
194191
- name: Download artifacts
195192
uses: actions/download-artifact@v4
196193
with:
@@ -210,6 +207,9 @@ jobs:
210207
cp bindings-x86_64-unknown-linux-gnu/* ./npm/linux-x64-gnu/
211208
cp bindings-x86_64-unknown-linux-musl/* ./npm/linux-x64-musl/
212209
210+
- name: Build Tailwind CSS
211+
run: pnpm run build
212+
213213
- name: Run pre-publish optimizations scripts
214214
run: node ./scripts/pre-publish-optimizations.mjs
215215

@@ -220,3 +220,9 @@ jobs:
220220
run: pnpm --recursive publish --tag ${{ inputs.release_channel }} --no-git-checks
221221
env:
222222
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
223+
224+
- name: Upload Standalone Artifacts
225+
uses: actions/upload-artifact@v4
226+
with:
227+
name: tailwindcss-standalone
228+
path: packages/@tailwindcss-standalone/dist/

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add new standalone builds of Tailwind CSS v4 ([#14270](https://github.com/tailwindlabs/tailwindcss/pull/14270))
13+
1014
### Fixed
1115

12-
- Bring back type exports for the cjs build of `@tailwindcss/postcss`. ([#14256](https://github.com/tailwindlabs/tailwindcss/pull/14256))
16+
- Bring back type exports for the cjs build of `@tailwindcss/postcss` ([#14256](https://github.com/tailwindlabs/tailwindcss/pull/14256))
1317
- Correctly merge tuple values when using the plugin API ([#14260](https://github.com/tailwindlabs/tailwindcss/pull/14260))
1418
- Handle arrays in the CSS `theme()` function when using plugins ([#14262](https://github.com/tailwindlabs/tailwindcss/pull/14262))
1519
- Fix fallback values when using the CSS `theme()` function ([#14262](https://github.com/tailwindlabs/tailwindcss/pull/14262))

integrations/cli/index.test.ts

Lines changed: 153 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,165 @@
1+
import os from 'node:os'
12
import path from 'node:path'
3+
import { describe } from 'vitest'
24
import { candidate, css, html, js, json, test, yaml } from '../utils'
35

4-
test(
5-
'production build',
6-
{
7-
fs: {
8-
'package.json': json`{}`,
9-
'pnpm-workspace.yaml': yaml`
10-
#
11-
packages:
12-
- project-a
13-
`,
14-
'project-a/package.json': json`
15-
{
16-
"dependencies": {
17-
"tailwindcss": "workspace:^",
18-
"@tailwindcss/cli": "workspace:^"
6+
const STANDALONE_BINARY = (() => {
7+
switch (os.platform()) {
8+
case 'win32':
9+
return 'tailwindcss-windows-x64.exe'
10+
case 'darwin':
11+
return os.arch() === 'x64' ? 'tailwindcss-macos-x64' : 'tailwindcss-macos-arm64'
12+
case 'linux':
13+
return os.arch() === 'x64' ? 'tailwindcss-linux-x64' : 'tailwindcss-linux-arm64'
14+
default:
15+
throw new Error(`Unsupported platform: ${os.platform()} ${os.arch()}`)
16+
}
17+
})()
18+
19+
describe.each([
20+
['CLI', 'pnpm tailwindcss'],
21+
[
22+
'Standalone CLI',
23+
path.resolve(__dirname, `../../packages/@tailwindcss-standalone/dist/${STANDALONE_BINARY}`),
24+
],
25+
])('%s', (_, command) => {
26+
test(
27+
'production build',
28+
{
29+
fs: {
30+
'package.json': json`{}`,
31+
'pnpm-workspace.yaml': yaml`
32+
#
33+
packages:
34+
- project-a
35+
`,
36+
'project-a/package.json': json`
37+
{
38+
"dependencies": {
39+
"tailwindcss": "workspace:^",
40+
"@tailwindcss/cli": "workspace:^"
41+
}
42+
}
43+
`,
44+
'project-a/index.html': html`
45+
<div
46+
class="underline 2xl:font-bold hocus:underline inverted:flex"
47+
></div>
48+
`,
49+
'project-a/plugin.js': js`
50+
module.exports = function ({ addVariant }) {
51+
addVariant('inverted', '@media (inverted-colors: inverted)')
52+
addVariant('hocus', ['&:focus', '&:hover'])
1953
}
20-
}
21-
`,
22-
'project-a/index.html': html`
23-
<div
24-
class="underline 2xl:font-bold hocus:underline inverted:flex"
25-
></div>
26-
`,
27-
'project-a/plugin.js': js`
28-
module.exports = function ({ addVariant }) {
29-
addVariant('inverted', '@media (inverted-colors: inverted)')
30-
addVariant('hocus', ['&:focus', '&:hover'])
31-
}
32-
`,
33-
'project-a/src/index.css': css`
34-
@import 'tailwindcss/utilities';
35-
@source '../../project-b/src/**/*.js';
36-
@plugin '../plugin.js';
37-
`,
38-
'project-a/src/index.js': js`
39-
const className = "content-['project-a/src/index.js']"
40-
module.exports = { className }
41-
`,
42-
'project-b/src/index.js': js`
43-
const className = "content-['project-b/src/index.js']"
44-
module.exports = { className }
45-
`,
54+
`,
55+
'project-a/src/index.css': css`
56+
@import 'tailwindcss/utilities';
57+
@source '../../project-b/src/**/*.js';
58+
@plugin '../plugin.js';
59+
`,
60+
'project-a/src/index.js': js`
61+
const className = "content-['project-a/src/index.js']"
62+
module.exports = { className }
63+
`,
64+
'project-b/src/index.js': js`
65+
const className = "content-['project-b/src/index.js']"
66+
module.exports = { className }
67+
`,
68+
},
4669
},
47-
},
48-
async ({ root, fs, exec }) => {
49-
await exec('pnpm tailwindcss --input src/index.css --output dist/out.css', {
50-
cwd: path.join(root, 'project-a'),
51-
})
70+
async ({ root, fs, exec }) => {
71+
await exec(`${command} --input src/index.css --output dist/out.css`, {
72+
cwd: path.join(root, 'project-a'),
73+
})
5274

53-
await fs.expectFileToContain('project-a/dist/out.css', [
54-
candidate`underline`,
55-
candidate`content-['project-a/src/index.js']`,
56-
candidate`content-['project-b/src/index.js']`,
57-
candidate`inverted:flex`,
58-
candidate`hocus:underline`,
59-
])
60-
},
61-
)
75+
await fs.expectFileToContain('project-a/dist/out.css', [
76+
candidate`underline`,
77+
candidate`content-['project-a/src/index.js']`,
78+
candidate`content-['project-b/src/index.js']`,
79+
candidate`inverted:flex`,
80+
candidate`hocus:underline`,
81+
])
82+
},
83+
)
6284

63-
test(
64-
'watch mode',
65-
{
66-
fs: {
67-
'package.json': json`{}`,
68-
'pnpm-workspace.yaml': yaml`
69-
#
70-
packages:
71-
- project-a
72-
`,
73-
'project-a/package.json': json`
74-
{
75-
"dependencies": {
76-
"tailwindcss": "workspace:^",
77-
"@tailwindcss/cli": "workspace:^"
85+
test(
86+
'watch mode',
87+
{
88+
fs: {
89+
'package.json': json`{}`,
90+
'pnpm-workspace.yaml': yaml`
91+
#
92+
packages:
93+
- project-a
94+
`,
95+
'project-a/package.json': json`
96+
{
97+
"dependencies": {
98+
"tailwindcss": "workspace:^",
99+
"@tailwindcss/cli": "workspace:^"
100+
}
78101
}
79-
}
80-
`,
81-
'project-a/index.html': html`
82-
<div
83-
class="underline 2xl:font-bold hocus:underline inverted:flex"
84-
></div>
85-
`,
86-
'project-a/plugin.js': js`
87-
module.exports = function ({ addVariant }) {
88-
addVariant('inverted', '@media (inverted-colors: inverted)')
89-
addVariant('hocus', ['&:focus', '&:hover'])
90-
}
91-
`,
92-
'project-a/src/index.css': css`
93-
@import 'tailwindcss/utilities';
94-
@source '../../project-b/src/**/*.js';
95-
@plugin '../plugin.js';
96-
`,
97-
'project-a/src/index.js': js`
98-
const className = "content-['project-a/src/index.js']"
99-
module.exports = { className }
100-
`,
101-
'project-b/src/index.js': js`
102-
const className = "content-['project-b/src/index.js']"
103-
module.exports = { className }
104-
`,
102+
`,
103+
'project-a/index.html': html`
104+
<div
105+
class="underline 2xl:font-bold hocus:underline inverted:flex"
106+
></div>
107+
`,
108+
'project-a/plugin.js': js`
109+
module.exports = function ({ addVariant }) {
110+
addVariant('inverted', '@media (inverted-colors: inverted)')
111+
addVariant('hocus', ['&:focus', '&:hover'])
112+
}
113+
`,
114+
'project-a/src/index.css': css`
115+
@import 'tailwindcss/utilities';
116+
@source '../../project-b/src/**/*.js';
117+
@plugin '../plugin.js';
118+
`,
119+
'project-a/src/index.js': js`
120+
const className = "content-['project-a/src/index.js']"
121+
module.exports = { className }
122+
`,
123+
'project-b/src/index.js': js`
124+
const className = "content-['project-b/src/index.js']"
125+
module.exports = { className }
126+
`,
127+
},
105128
},
106-
},
107-
async ({ root, fs, spawn }) => {
108-
await spawn('pnpm tailwindcss --input src/index.css --output dist/out.css --watch', {
109-
cwd: path.join(root, 'project-a'),
110-
})
129+
async ({ root, fs, spawn }) => {
130+
await spawn(`${command} --input src/index.css --output dist/out.css --watch`, {
131+
cwd: path.join(root, 'project-a'),
132+
})
111133

112-
await fs.expectFileToContain('project-a/dist/out.css', [
113-
candidate`underline`,
114-
candidate`content-['project-a/src/index.js']`,
115-
candidate`content-['project-b/src/index.js']`,
116-
candidate`inverted:flex`,
117-
candidate`hocus:underline`,
118-
])
134+
await fs.expectFileToContain('project-a/dist/out.css', [
135+
candidate`underline`,
136+
candidate`content-['project-a/src/index.js']`,
137+
candidate`content-['project-b/src/index.js']`,
138+
candidate`inverted:flex`,
139+
candidate`hocus:underline`,
140+
])
119141

120-
await fs.write(
121-
'project-a/src/index.js',
122-
js`
123-
const className = "[.changed_&]:content-['project-a/src/index.js']"
124-
module.exports = { className }
125-
`,
126-
)
127-
await fs.expectFileToContain('project-a/dist/out.css', [
128-
candidate`[.changed_&]:content-['project-a/src/index.js']`,
129-
])
142+
await fs.write(
143+
'project-a/src/index.js',
144+
js`
145+
const className = "[.changed_&]:content-['project-a/src/index.js']"
146+
module.exports = { className }
147+
`,
148+
)
149+
await fs.expectFileToContain('project-a/dist/out.css', [
150+
candidate`[.changed_&]:content-['project-a/src/index.js']`,
151+
])
130152

131-
await fs.write(
132-
'project-b/src/index.js',
133-
js`
134-
const className = "[.changed_&]:content-['project-b/src/index.js']"
135-
module.exports = { className }
136-
`,
137-
)
138-
await fs.expectFileToContain('project-a/dist/out.css', [
139-
candidate`[.changed_&]:content-['project-b/src/index.js']`,
140-
])
141-
},
142-
)
153+
await fs.write(
154+
'project-b/src/index.js',
155+
js`
156+
const className = "[.changed_&]:content-['project-b/src/index.js']"
157+
module.exports = { className }
158+
`,
159+
)
160+
await fs.expectFileToContain('project-a/dist/out.css', [
161+
candidate`[.changed_&]:content-['project-b/src/index.js']`,
162+
])
163+
},
164+
)
165+
})

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,11 @@
6060
"typescript": "^5.5.4",
6161
"vitest": "^2.0.5"
6262
},
63-
"packageManager": "[email protected]"
63+
"packageManager": "[email protected]",
64+
"pnpm": {
65+
"patchedDependencies": {
66+
"@parcel/[email protected]": "patches/@[email protected]",
67+
68+
}
69+
}
6470
}

packages/@tailwindcss-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dependencies": {
3232
"@parcel/watcher": "^2.4.1",
3333
"@tailwindcss/oxide": "workspace:^",
34+
"enhanced-resolve": "^5.17.1",
3435
"lightningcss": "catalog:",
3536
"mri": "^1.2.0",
3637
"picocolors": "^1.0.1",

0 commit comments

Comments
 (0)