Skip to content

Commit 45ee6a1

Browse files
Download platform specific package if optionalDependencies are skipped
1 parent 179e5dd commit 45ee6a1

File tree

5 files changed

+259
-23
lines changed

5 files changed

+259
-23
lines changed

crates/node/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
}
3333
},
3434
"license": "MIT",
35+
"dependencies": {
36+
"tar": "^7.4.3",
37+
"detect-libc": "^2.0.4"
38+
},
3539
"devDependencies": {
3640
"@napi-rs/cli": "^3.0.0-alpha.78",
3741
"@napi-rs/wasm-runtime": "^0.2.9",
@@ -42,7 +46,8 @@
4246
},
4347
"files": [
4448
"index.js",
45-
"index.d.ts"
49+
"index.d.ts",
50+
"scripts/install.js"
4651
],
4752
"publishConfig": {
4853
"provenance": true,
@@ -57,7 +62,8 @@
5762
"postbuild:wasm": "node ./scripts/move-artifacts.mjs",
5863
"dev": "cargo watch --quiet --shell 'npm run build'",
5964
"build:debug": "napi build --platform --no-const-enum",
60-
"version": "napi version"
65+
"version": "napi version",
66+
"postinstall": "node ./scripts/install.js"
6167
},
6268
"optionalDependencies": {
6369
"@tailwindcss/oxide-android-arm64": "workspace:*",

crates/node/scripts/install.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* @tailwindcss/oxide postinstall script
5+
*
6+
* This script ensures that the correct binary for the current platform and
7+
* architecture is downloaded and available.
8+
*/
9+
10+
const fs = require('fs')
11+
const path = require('path')
12+
const https = require('https')
13+
const { extract } = require('tar')
14+
const packageJson = require('../package.json')
15+
const detectLibc = require('detect-libc')
16+
17+
const version = packageJson.version
18+
19+
function getPlatformPackageName() {
20+
const platform = process.platform
21+
const arch = process.arch
22+
23+
let libc = ''
24+
if (platform === 'linux') {
25+
libc = detectLibc.isNonGlibcLinuxSync() ? 'musl' : 'gnu'
26+
}
27+
28+
// Map to our package naming conventions
29+
switch (platform) {
30+
case 'darwin':
31+
return arch === 'arm64' ? '@tailwindcss/oxide-darwin-arm64' : '@tailwindcss/oxide-darwin-x64'
32+
case 'win32':
33+
if (arch === 'arm64') return '@tailwindcss/oxide-win32-arm64-msvc'
34+
if (arch === 'ia32') return '@tailwindcss/oxide-win32-ia32-msvc'
35+
return '@tailwindcss/oxide-win32-x64-msvc'
36+
case 'linux':
37+
if (arch === 'x64') {
38+
return libc === 'musl'
39+
? '@tailwindcss/oxide-linux-x64-musl'
40+
: '@tailwindcss/oxide-linux-x64-gnu'
41+
} else if (arch === 'arm64') {
42+
return libc === 'musl'
43+
? '@tailwindcss/oxide-linux-arm64-musl'
44+
: '@tailwindcss/oxide-linux-arm64-gnu'
45+
} else if (arch === 'arm') {
46+
return '@tailwindcss/oxide-linux-arm-gnueabihf'
47+
}
48+
break
49+
case 'freebsd':
50+
return '@tailwindcss/oxide-freebsd-x64'
51+
52+
case 'android':
53+
return '@tailwindcss/oxide-android-arm64'
54+
default:
55+
return '@tailwindcss/oxide-wasm32-wasi'
56+
}
57+
}
58+
59+
function isPackageAvailable(packageName) {
60+
try {
61+
require.resolve(packageName)
62+
return true
63+
} catch (e) {
64+
return false
65+
}
66+
}
67+
68+
// Extract all files from a tarball to a destination directory
69+
async function extractTarball(tarballStream, destDir) {
70+
if (!fs.existsSync(destDir)) {
71+
fs.mkdirSync(destDir, { recursive: true })
72+
}
73+
74+
return new Promise((resolve, reject) => {
75+
tarballStream
76+
.pipe(extract({ cwd: destDir, strip: 1 }))
77+
.on('error', (err) => reject(err))
78+
.on('end', () => resolve())
79+
})
80+
}
81+
82+
async function downloadAndExtractBinary(packageName) {
83+
let tarballUrl = `https://registry.npmjs.org/${packageName}/-/${packageName.replace('@tailwindcss/', '')}-${version}.tgz`
84+
console.log(`Downloading ${tarballUrl}...`)
85+
86+
return new Promise((resolve) => {
87+
https
88+
.get(tarballUrl, (response) => {
89+
if (response.statusCode === 302 || response.statusCode === 301) {
90+
// Handle redirects
91+
https.get(response.headers.location, handleResponse).on('error', (err) => {
92+
console.error('Download error:', err)
93+
resolve()
94+
})
95+
return
96+
}
97+
98+
handleResponse(response)
99+
100+
async function handleResponse(response) {
101+
try {
102+
if (response.statusCode !== 200) {
103+
throw new Error(`Download failed with status code: ${response.statusCode}`)
104+
}
105+
106+
await extractTarball(
107+
response,
108+
path.join(__dirname, '..', 'node_modules', ...packageName.split('/')),
109+
)
110+
console.log(`Successfully downloaded and installed ${packageName}`)
111+
} catch (error) {
112+
console.error('Error during extraction:', error)
113+
resolve()
114+
} finally {
115+
resolve()
116+
}
117+
}
118+
})
119+
.on('error', (err) => {
120+
console.error('Download error:', err)
121+
resolve()
122+
})
123+
})
124+
}
125+
126+
async function main() {
127+
const packageName = getPlatformPackageName()
128+
if (!packageName) return
129+
if (isPackageAvailable(packageName)) return
130+
131+
await downloadAndExtractBinary(packageName)
132+
}
133+
134+
main()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { js, json, test } from '../utils'
2+
3+
test(
4+
'@tailwindcss/oxide will fail when architecture-specific packages are missing',
5+
{
6+
fs: {
7+
'package.json': json`
8+
{
9+
"dependencies": {
10+
"@tailwindcss/oxide": "workspace:^"
11+
}
12+
}
13+
`,
14+
'test.js': js`
15+
try {
16+
let Scanner = require('@tailwindcss/oxide')
17+
console.log('SUCCESS: @tailwindcss/oxide loaded successfully', Scanner)
18+
} catch (error) {
19+
console.log('FAILURE: Failed to load @tailwindcss/oxide:', error.message)
20+
}
21+
`,
22+
},
23+
},
24+
async ({ exec, root, expect, fs }) => {
25+
// Remove the automatically added optional dependencies
26+
await exec('find node_modules -type l -name "oxide-*" -exec rm {} +')
27+
await exec('find node_modules -type f -name "oxide-*" -exec rm -rf {} +')
28+
29+
// Get last published version
30+
let version = (await exec('npm show @tailwindcss/oxide version')).trim()
31+
// Ensure that we don't depend on a specific version number in the download
32+
// script in case we bump the version number in the repository and CI is run
33+
// before a release
34+
let packageJson = JSON.parse(await fs.read('node_modules/@tailwindcss/oxide/package.json'))
35+
packageJson.version = version
36+
await fs.write(
37+
'node_modules/@tailwindcss/oxide/package.json',
38+
JSON.stringify(packageJson, null, 2),
39+
)
40+
41+
let opts = {
42+
// Ensure that we don't include any node paths from the test runner
43+
env: { NODE_PATH: '' },
44+
}
45+
46+
expect(await exec('node test.js', opts)).toMatch(/FAILURE/)
47+
48+
// Now run the post-install script
49+
await exec('node node_modules/@tailwindcss/oxide/scripts/install.js', opts)
50+
51+
expect(await exec('node test.js', opts)).toMatch(/SUCCESS/)
52+
},
53+
)

integrations/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ export function test(
122122
{
123123
cwd,
124124
...childProcessOptions,
125-
env: childProcessOptions.env,
125+
env: {
126+
...process.env,
127+
...childProcessOptions.env,
128+
},
126129
},
127130
(error, stdout, stderr) => {
128131
if (error) {

0 commit comments

Comments
 (0)