tailwindcss/scripts/version-packages.mjs
Philipp Spiess 83ce4c0a30
Add experimental @tailwindcss/oxide-wasm32-wasi (#17558)
Closes #17448
Closes #13133

This PR adds an a new Oxide target for `wasm32-wasip1-threads`:
`@tailwindcss/oxide-wasm32-wasi`. The goal of this is to enable more
environments to run Oxide, including (but not limited to) StackBlitz.

We're making use of `napi-rs`'s upcoming v3 features to simplify the
setup here, meaning `napi-rs` will configure the WASM target and create
an npm package that works across Node and browser environments.

## MacOS AArch64 issues

While setting up an integration test for the new WASM target, I ran into
an issue where FS reads where not terminating on macOS. After some
research I found this to be a limitation of the Node.js container
interface right now, see: https://github.com/nodejs/node/issues/47193

### Windows issues

We also found that the Node.js wasi container does not properly support
Windows: https://github.com/nodejs/uvwasi/issues/11

For now we, it's probably best for MacOS AArch64 users and Windows users
to use the native modules instead.

## Test plan

The `@tailwindcss/oxide-wasm32-wasi` npm package can be built locally
via `pnpm build` and then run with the Oxide API. A usage example can be
taken from the newly added integration test.

Furthermore this was tested to work as a polyfill on StackBlitz:
https://stackblitz.com/edit/vitejs-vite-uks3gt5p

[ci-all]

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2025-04-11 17:19:55 +02:00

179 lines
5.7 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/wasm32-wasi',
'crates/node/npm/win32-arm64-msvc',
'crates/node/npm/win32-x64-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/*' --filter='!./integrations' --filter='!./packages/internal-example-plugin' -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.')
},
)