Skip to content

Download platform specific package if optionalDependencies are skipped #17929

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions crates/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
}
},
"license": "MIT",
"dependencies": {
"tar": "^7.4.3",
"detect-libc": "^2.0.4"
},
"devDependencies": {
"@napi-rs/cli": "^3.0.0-alpha.78",
"@napi-rs/wasm-runtime": "^0.2.9",
Expand All @@ -42,7 +46,8 @@
},
"files": [
"index.js",
"index.d.ts"
"index.d.ts",
"scripts/install.js"
],
"publishConfig": {
"provenance": true,
Expand All @@ -57,7 +62,8 @@
"postbuild:wasm": "node ./scripts/move-artifacts.mjs",
"dev": "cargo watch --quiet --shell 'npm run build'",
"build:debug": "napi build --platform --no-const-enum",
"version": "napi version"
"version": "napi version",
"postinstall": "node ./scripts/install.js"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "workspace:*",
Expand Down
143 changes: 143 additions & 0 deletions crates/node/scripts/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can somehow re-use this logic from what NAPI generated for us. Since that is generated and using require already it might not be as easy.

Just have to make sure that if we make changes, that we keep this in sync

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was looking at the napi stuff and it seems to be much more (it generates names for packages that we don't even have etc). I agree that ideally we can reuse it but the function for that is not exported so I'm not sure how. MAYBE we can just try and load the index.js file tbh but then we still don't need which file to load haha

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()
73 changes: 73 additions & 0 deletions integrations/oxide/postinstall.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { js, json, test } from '../utils'

test(
'@tailwindcss/oxide will fail when architecture-specific packages are missing',
{
fs: {
'package.json': json`
{
"dependencies": {
"@tailwindcss/oxide": "workspace:^"
}
}
`,
'test.js': js`
try {
let Scanner = require('@tailwindcss/oxide')
console.log('SUCCESS: @tailwindcss/oxide loaded successfully', Scanner)
} catch (error) {
console.log('FAILURE: Failed to load @tailwindcss/oxide:', error.message)
}
`,
},
},
async ({ exec, root, expect, fs }) => {
await removePlatformSpecificExtensions(path.join(root, 'node_modules'))

// Get last published version
let version = (await exec('npm show @tailwindcss/oxide version')).trim()
// Ensure that we don't depend on a specific version number in the download
// script in case we bump the version number in the repository and CI is run
// before a release
let packageJson = JSON.parse(await fs.read('node_modules/@tailwindcss/oxide/package.json'))
packageJson.version = version
await fs.write(
'node_modules/@tailwindcss/oxide/package.json',
JSON.stringify(packageJson, null, 2),
)

let opts = {
// Ensure that we don't include any node paths from the test runner
env: { NODE_PATH: '' },
}

expect(await exec('node test.js', opts)).toMatch(/FAILURE/)

// Now run the post-install script
await exec('node node_modules/@tailwindcss/oxide/scripts/install.js', opts)

expect(await exec('node test.js', opts)).toMatch(/SUCCESS/)
},
)

async function removePlatformSpecificExtensions(directory: string) {
let entries = await fs.readdir(directory, { withFileTypes: true })

for (let entry of entries) {
let fullPath = path.join(directory, entry.name)

if (entry.name.startsWith('oxide-')) {
if (entry.isSymbolicLink()) {
await fs.unlink(fullPath)
} else if (entry.isFile()) {
await fs.unlink(fullPath)
} else if (entry.isDirectory()) {
await fs.rm(fullPath, { recursive: true, force: true })
}
} else if (entry.isDirectory()) {
await removePlatformSpecificExtensions(fullPath)
}
}
}
5 changes: 4 additions & 1 deletion integrations/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ export function test(
{
cwd,
...childProcessOptions,
env: childProcessOptions.env,
env: {
...process.env,
...childProcessOptions.env,
},
},
(error, stdout, stderr) => {
if (error) {
Expand Down
Loading