Philipp Spiess 47bb007eae
Download platform specific package if optionalDependencies are skipped (#17929)
Closes #15806

This PR adds a new `postinstall` script to `@tailwindcss/oxide` that
will attempt to download the platform-specific optional dependency to
avoid issues when the package manager does not do that automatically
(see #15806). The implementation for this is fairly simple: The npm
package is downloaded from the official npm servers and extracted into
the `node_modules` directory of the `@tailwidncss/oxide` installation.

## Test plan 

Since we still lack a solid repro of #15806, the way I tested this
change was to manually remove all automatically-installed optional
dependencies and then running the postinstall script manually. The
script then downloads the right version package which makes the import
to `@tailwidncss/oxide` work. An appropriate integration test was added
too.

I furthermore also validated that:

- This works across CI platforms [ci-all]
- The postinstall script bails out when running `pnpm install` in the
dev setup. This is necessary since doing the initial install won't have
any binary dependencies yet so it would download invalid versions from
npm (as the version numbers locally refer to the last released version).
We can safely bail out here though since this was never an issue with
local development.
- The postinstall script does not do anything when the
`@tailwindcss/oxide` library is added _unless_ the issue is detected.

[ci-all]

---------

Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
2025-05-09 14:55:02 +02:00

144 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* @tailwindcss/oxide postinstall script
*
* This script ensures that the correct binary for the current platform and
* architecture is downloaded and available.
*/
const fs = require('fs')
const path = require('path')
const https = require('https')
const { extract } = require('tar')
const packageJson = require('../package.json')
const detectLibc = require('detect-libc')
const version = packageJson.version
function getPlatformPackageName() {
let platform = process.platform
let arch = process.arch
let libc = ''
if (platform === 'linux') {
libc = detectLibc.isNonGlibcLinuxSync() ? 'musl' : 'gnu'
}
// Map to our package naming conventions
switch (platform) {
case 'darwin':
return arch === 'arm64' ? '@tailwindcss/oxide-darwin-arm64' : '@tailwindcss/oxide-darwin-x64'
case 'win32':
if (arch === 'arm64') return '@tailwindcss/oxide-win32-arm64-msvc'
if (arch === 'ia32') return '@tailwindcss/oxide-win32-ia32-msvc'
return '@tailwindcss/oxide-win32-x64-msvc'
case 'linux':
if (arch === 'x64') {
return libc === 'musl'
? '@tailwindcss/oxide-linux-x64-musl'
: '@tailwindcss/oxide-linux-x64-gnu'
} else if (arch === 'arm64') {
return libc === 'musl'
? '@tailwindcss/oxide-linux-arm64-musl'
: '@tailwindcss/oxide-linux-arm64-gnu'
} else if (arch === 'arm') {
return '@tailwindcss/oxide-linux-arm-gnueabihf'
}
break
case 'freebsd':
return '@tailwindcss/oxide-freebsd-x64'
case 'android':
return '@tailwindcss/oxide-android-arm64'
default:
return '@tailwindcss/oxide-wasm32-wasi'
}
}
function isPackageAvailable(packageName) {
try {
require.resolve(packageName)
return true
} catch (e) {
return false
}
}
// Extract all files from a tarball to a destination directory
async function extractTarball(tarballStream, destDir) {
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true })
}
return new Promise((resolve, reject) => {
tarballStream
.pipe(extract({ cwd: destDir, strip: 1 }))
.on('error', (err) => reject(err))
.on('end', () => resolve())
})
}
async function downloadAndExtractBinary(packageName) {
let tarballUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.replace('@tailwindcss/', '')}-${version}.tgz`
console.log(`Downloading ${tarballUrl}...`)
return new Promise((resolve) => {
https
.get(tarballUrl, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Handle redirects
https.get(response.headers.location, handleResponse).on('error', (err) => {
console.error('Download error:', err)
resolve()
})
return
}
handleResponse(response)
async function handleResponse(response) {
try {
if (response.statusCode !== 200) {
throw new Error(`Download failed with status code: ${response.statusCode}`)
}
await extractTarball(
response,
path.join(__dirname, '..', 'node_modules', ...packageName.split('/')),
)
console.log(`Successfully downloaded and installed ${packageName}`)
} catch (error) {
console.error('Error during extraction:', error)
resolve()
} finally {
resolve()
}
}
})
.on('error', (err) => {
console.error('Download error:', err)
resolve()
})
})
}
async function main() {
// Don't run this script in the package source
try {
if (fs.existsSync(path.join(__dirname, '..', 'build.rs'))) {
return
}
let packageName = getPlatformPackageName()
if (!packageName) return
if (isPackageAvailable(packageName)) return
await downloadAndExtractBinary(packageName)
} catch (error) {
console.error(error)
return
}
}
main()