@napi-rs/cli
Version:
Cli tools for napi-rs
264 lines (224 loc) • 7.71 kB
text/typescript
import { createRequire } from 'node:module'
import { join, resolve } from 'node:path'
import { Comparator, Range, minVersion, subset } from 'semver'
const require = createRequire(import.meta.url)
const minimumWasiNodeVersion = '>=14.0.0'
import {
applyDefaultCreateNpmDirsOptions,
type CreateNpmDirsOptions,
} from '../def/create-npm-dirs.js'
import {
debugFactory,
readNapiConfig,
mkdirAsync as rawMkdirAsync,
pick,
writeFileAsync as rawWriteFileAsync,
type Target,
type CommonPackageJsonFields,
} from '../utils/index.js'
const debug = debugFactory('create-npm-dirs')
export interface PackageMeta {
'dist-tags': { [index: string]: string }
}
const WASM_RUNTIME_PACKAGE_NAME = '@napi-rs/wasm-runtime'
async function getLatestWasmRuntimeVersion() {
const npmRegistryBase =
process.env.npm_config_registry?.replace(/\/?$/, '/') ??
'https://registry.npmjs.org/'
const packageMetadataUrl = `${npmRegistryBase}${WASM_RUNTIME_PACKAGE_NAME}`
let response: Response
try {
response = await fetch(packageMetadataUrl)
} catch (error) {
throw new Error(
`Failed to fetch ${packageMetadataUrl} while resolving ${WASM_RUNTIME_PACKAGE_NAME}. Check your network connection and npm registry availability.`,
{ cause: error },
)
}
if (!response.ok) {
throw new Error(
`Failed to fetch ${packageMetadataUrl} while resolving ${WASM_RUNTIME_PACKAGE_NAME}: npm registry responded with ${response.status} ${response.statusText || 'Unknown Status'}`,
)
}
let packageMeta: PackageMeta
try {
packageMeta = (await response.json()) as PackageMeta
} catch (error) {
throw new Error(
`Failed to parse npm registry metadata for ${WASM_RUNTIME_PACKAGE_NAME} from ${packageMetadataUrl}`,
{ cause: error },
)
}
const latestVersion = packageMeta['dist-tags']?.latest
if (typeof latestVersion !== 'string' || latestVersion.trim().length === 0) {
throw new Error(
`npm registry metadata for ${WASM_RUNTIME_PACKAGE_NAME} from ${packageMetadataUrl} did not include a latest dist-tag`,
)
}
return latestVersion.trim()
}
export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
const options = applyDefaultCreateNpmDirsOptions(userOptions)
async function mkdirAsync(dir: string) {
debug('Try to create dir: %i', dir)
if (options.dryRun) {
return
}
await rawMkdirAsync(dir, {
recursive: true,
})
}
async function writeFileAsync(file: string, content: string) {
debug('Writing file %i', file)
if (options.dryRun) {
debug(content)
return
}
await rawWriteFileAsync(file, content)
}
const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
const npmPath = resolve(options.cwd, options.npmDir)
debug(`Read content from [${options.configPath ?? packageJsonPath}]`)
const { targets, binaryName, packageName, packageJson } =
await readNapiConfig(
packageJsonPath,
options.configPath ? resolve(options.cwd, options.configPath) : undefined,
)
const wasmRuntimeVersion = targets.some((target) => target.arch === 'wasm32')
? await getLatestWasmRuntimeVersion()
: undefined
for (const target of targets) {
const targetDir = join(npmPath, `${target.platformArchABI}`)
await mkdirAsync(targetDir)
const binaryFileName =
target.arch === 'wasm32'
? `${binaryName}.${target.platformArchABI}.wasm`
: `${binaryName}.${target.platformArchABI}.node`
const scopedPackageJson: CommonPackageJsonFields = {
name: `${packageName}-${target.platformArchABI}`,
version: packageJson.version,
cpu: target.arch !== 'universal' ? [target.arch] : undefined,
main: binaryFileName,
files: [binaryFileName],
...pick(
packageJson,
'description',
'keywords',
'author',
'authors',
'homepage',
'license',
'engines',
'repository',
'bugs',
),
}
if (packageJson.publishConfig) {
scopedPackageJson.publishConfig = pick(
packageJson.publishConfig,
'registry',
'access',
)
}
if (target.arch !== 'wasm32') {
scopedPackageJson.os = [target.platform]
} else {
const entry = `${binaryName}.wasi.cjs`
scopedPackageJson.main = entry
scopedPackageJson.browser = `${binaryName}.wasi-browser.js`
scopedPackageJson.files?.push(
entry,
scopedPackageJson.browser,
`wasi-worker.mjs`,
`wasi-worker-browser.mjs`,
)
scopedPackageJson.engines = {
...scopedPackageJson.engines,
node: scopedPackageJson.engines?.node
? restrictWasiNodeEngine(scopedPackageJson.engines.node)
: minimumWasiNodeVersion,
}
const emnapiVersion = require('emnapi/package.json').version
scopedPackageJson.dependencies = {
'@napi-rs/wasm-runtime': `^${wasmRuntimeVersion}`,
'@emnapi/core': emnapiVersion,
'@emnapi/runtime': emnapiVersion,
}
}
if (target.abi === 'gnu') {
scopedPackageJson.libc = ['glibc']
} else if (target.abi === 'musl') {
scopedPackageJson.libc = ['musl']
}
const targetPackageJson = join(targetDir, 'package.json')
await writeFileAsync(
targetPackageJson,
JSON.stringify(scopedPackageJson, null, 2) + '\n',
)
const targetReadme = join(targetDir, 'README.md')
await writeFileAsync(targetReadme, readme(packageName, target))
debug.info(`${packageName} -${target.platformArchABI} created`)
}
}
function readme(packageName: string, target: Target) {
return `# \`${packageName}-${target.platformArchABI}\`
This is the **${target.triple}** binary for \`${packageName}\`
`
}
function restrictWasiNodeEngine(nodeRange: string) {
try {
if (subset(nodeRange, minimumWasiNodeVersion)) {
return nodeRange
}
if (subset(minimumWasiNodeVersion, nodeRange)) {
return minimumWasiNodeVersion
}
const minimumComparator = new Comparator(minimumWasiNodeVersion)
const restrictedRangeSets = new Range(nodeRange).set
.map((comparators) =>
normalizeComparatorSet([...comparators, minimumComparator]),
)
.filter(
(candidate): candidate is string =>
candidate !== undefined && minVersion(candidate) !== null,
)
if (restrictedRangeSets.length > 0) {
return restrictedRangeSets.join(' || ')
}
} catch {
// ignore
}
return minimumWasiNodeVersion
}
function normalizeComparatorSet(comparators: Comparator[]) {
const exactMatch = comparators.find(({ operator }) => operator === '')
if (exactMatch) {
return comparators.every((comparator) => comparator.test(exactMatch.semver))
? exactMatch.value
: undefined
}
let lowerBound: Comparator | undefined
let upperBound: Comparator | undefined
for (const comparator of comparators) {
if (comparator.operator === '>' || comparator.operator === '>=') {
if (
!lowerBound ||
comparator.semver.compare(lowerBound.semver) > 0 ||
(comparator.semver.compare(lowerBound.semver) === 0 &&
comparator.operator === '>')
) {
lowerBound = comparator
}
} else if (comparator.operator === '<' || comparator.operator === '<=') {
if (
!upperBound ||
comparator.semver.compare(upperBound.semver) < 0 ||
(comparator.semver.compare(upperBound.semver) === 0 &&
comparator.operator === '<')
) {
upperBound = comparator
}
}
}
return [lowerBound?.value, upperBound?.value].filter(Boolean).join(' ')
}