tailwindcss/scripts/version-packages.mjs
Robin Malfait 515a9bdc5f
Setup v4 releases (#15961)
Now that Tailwind CSS v4 is released, we can setup a proper release
workflow again. This setup mimics the workflow of how we did it in v3,
but adjusted to make it work on the v4 codebase.

Whenever a PR is merged into the `next` branch, we will publish an
insiders release to npm using the following version number:
`0.0.0-insiders.<commit-hash>`. Note: insiders releases will not have a
GitHub release associated with them, therefore the `standalone-cli`
won't be available as an insiders release.

For the normal release procedure, the following steps are required:

1. Manually version the packages (e.g.: `pnpm run version-packages`)
2. Create a git tag for the version you want to release
3. Push the updated package.json files and the git tag to the repository

Next, a GitHub action will kick in once a `tag` is being pushed.

The GitHub action will run a build, and create a draft release on GitHub
that will contain:

1. The CHANGELOG.md contents for the last version
2. The `standalone-cli` artifacts attached to the drafted release

Once you are happy with the draft, you can publish the draft on GitHub.

This in turn will trigger another GitHub action that will publish the
packages to npm.

Whenever an insiders release or a normal release completes, we will also
trigger Tailwind Play, to update its dependencies to the latest version
of Tailwind CSS.

---

Note: some of the GitHub Action workflows still refer to the `next`
branch instead of the `main` branch. If we later today want to get a new
release out, we can merge `next` into `main` and update the workflows
accordingly.


---

This is hard to test, but I started from the existing release.yml file
and adjusted things accordingly. The biggest change is related to the
insiders version. If you look at this temporary
[commit](572dddfc33),
you can see that the publishing (dry-run) seems to work as expected:
<img width="1508" alt="image"
src="https://github.com/user-attachments/assets/c075e788-dcbc-4200-aa32-2b9a3c54d629"
/>
2025-01-28 12:51:34 +01:00

175 lines
5.4 KiB
JavaScript

import { exec, spawnSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import fs from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import url from 'node:url'
import prettier from 'prettier'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, '..')
const version = process.argv[2] || null
// The known workspace is: @tailwindcss/oxide
// All the workspaces in `crates/node/npm/*` should always be in sync with
// `@tailwindcss/oxide`. You can think of them as one big package, but they are
// split into multiple packages because they are OS specific.
const syncedWorkspaces = new Map([
[
'@tailwindcss/oxide',
[
'crates/node/npm/android-arm-eabi',
'crates/node/npm/android-arm64',
'crates/node/npm/darwin-arm64',
'crates/node/npm/darwin-x64',
'crates/node/npm/freebsd-x64',
'crates/node/npm/linux-arm-gnueabihf',
'crates/node/npm/linux-arm64-gnu',
'crates/node/npm/linux-arm64-musl',
'crates/node/npm/linux-x64-gnu',
'crates/node/npm/linux-x64-musl',
'crates/node/npm/win32-x64-msvc',
'crates/node/npm/win32-arm64-msvc',
],
],
['@tailwindcss/cli', ['packages/@tailwindcss-standalone']],
])
const inverseSyncedWorkspaces = new Map()
for (let [name, paths] of syncedWorkspaces) {
for (let [idx, filePath] of paths.entries()) {
// Make sure all the paths are absolute paths
paths[idx] = path.resolve(root, filePath, 'package.json')
// Make sure inverse lookup table exists
inverseSyncedWorkspaces.set(paths[idx], name)
}
}
exec('pnpm --silent --filter=!./playgrounds/* -r exec pwd', async (err, stdout) => {
if (err) {
console.error(err)
process.exit(1)
}
if (version !== null) {
for (let pkgPath of stdout
.trim()
.split('\n')
.map((x) => path.resolve(x, 'package.json'))) {
let pkg = await fs.readFile(pkgPath, 'utf8').then(JSON.parse)
let name = pkg.name
if (version !== '') {
// Ensure the version is set after the name and before everything else
delete pkg.name
delete pkg.version
// This allows us to keep the order of the keys in the package.json
pkg = { name, version, ...pkg }
}
await fs.writeFile(
pkgPath,
await prettier
.format(JSON.stringify(pkg, null, 2), { filepath: pkgPath })
.then((x) => `${x.trim()}\n`),
)
}
console.log('Done.')
return
}
let paths = stdout
.trim()
.split('\n')
.map((x) => path.resolve(x, 'package.json'))
// Workspaces that are in sync with another workspace should not be updated
// manually, they should be updated by updating the main workspace.
.filter((x) => !inverseSyncedWorkspaces.has(x))
let workspaces = new Map()
// Track all the workspaces
for (let path of paths) {
let pkg = await fs.readFile(path, 'utf8').then(JSON.parse)
if (pkg.private) continue
workspaces.set(pkg.name, { version: pkg.version ?? '', path })
}
// Build the editable output
let lines = ['# Update the versions of the packages you want to change', '']
for (let [name, info] of workspaces) {
lines.push(`${name}: ${info.version}`)
}
let output = lines.join('\n')
// Edit the file
{
// Figure out which editor to use.
//
// In this case we still split on whitespace, because it can happen that the
// EDITOR env variable is configured as `code --wait`. This means that we
// want `code` as the editor, but `--wait` is one of the arguments.
let args = process.env.EDITOR.split(' ')
let editor = args.shift()
// Create a temporary file which will be edited
let filepath = path.resolve(tmpdir(), `version-${randomUUID()}.txt`)
await fs.writeFile(filepath, output)
// Edit the file, once the editor is closed, the file will be saved and we
// can read the changes
spawnSync(editor, [...args, filepath], {
stdio: 'inherit',
})
let newOutput = await fs.readFile(filepath, 'utf8').then((x) => x.trim().split('\n'))
// Cleanup temporary file
await fs.unlink(filepath)
// Update the package.json files
for (let line of newOutput) {
if (line[0] === '#') continue // Skip comment lines
if (line.trim() === '') continue // Skip empty lines
let [name, version = ''] = line.split(':').map((x) => x.trim())
// Figure out all the paths to the package.json files that need to be
// updated with the new version
let paths = [
// The package.json file of the main workspace
workspaces.get(name).path,
// The package.json files of the workspaces that are in sync with the
// main workspace
...(syncedWorkspaces.get(name) ?? []),
]
for (let pkgPath of paths) {
let pkg = await fs.readFile(pkgPath, 'utf8').then(JSON.parse)
let name = pkg.name
if (version !== '') {
// Ensure the version is set after the name and before everything else
delete pkg.name
delete pkg.version
// This allows us to keep the order of the keys in the package.json
pkg = { name, version, ...pkg }
}
await fs.writeFile(
pkgPath,
await prettier
.format(JSON.stringify(pkg, null, 2), { filepath: pkgPath })
.then((x) => `${x.trim()}\n`),
)
}
}
}
console.log('Done.')
})