UNPKG

@napi-rs/cli

Version:

Cli tools for napi-rs

562 lines (463 loc) 15.5 kB
import { existsSync } from 'node:fs' import { mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { createServer } from 'node:http' import { createRequire } from 'node:module' import { tmpdir } from 'node:os' import { join } from 'node:path' import ava, { type TestFn } from 'ava' import { createNpmDirs } from '../create-npm-dirs.js' const require = createRequire(import.meta.url) const test = ava as TestFn<{ tmpDir: string packageJsonPath: string npmConfigRegistry?: string }> test.beforeEach(async (t) => { // Create a unique temp directory for tests const timestamp = Date.now() const random = Math.random().toString(36).substring(7) const tmpDir = join( tmpdir(), 'napi-rs-test', `create-npm-dirs-${timestamp}-${random}`, ) const packageJsonPath = join(tmpDir, 'package.json') // Create the directory await mkdir(tmpDir, { recursive: true }) t.context = { tmpDir, packageJsonPath, npmConfigRegistry: process.env.npm_config_registry, } }) test.afterEach.always(async (t) => { if (t.context.npmConfigRegistry === undefined) { delete process.env.npm_config_registry } else { process.env.npm_config_registry = t.context.npmConfigRegistry } // Clean up any created directories if (existsSync(t.context.tmpDir)) { await rm(t.context.tmpDir, { recursive: true, force: true }) } }) async function startRegistryServer( responseBody: Record<string, unknown> = { 'dist-tags': { latest: '1.2.3', }, }, ) { const requests: string[] = [] const server = createServer((req, res) => { requests.push(req.url ?? '') res.setHeader('content-type', 'application/json') res.end(JSON.stringify(responseBody)) }) await new Promise<void>((resolve, reject) => { server.once('error', reject) server.listen(0, '127.0.0.1', () => resolve()) }) const address = server.address() if (!address || typeof address === 'string') { server.close() throw new Error('Failed to resolve test registry server address') } return { requests, close: () => new Promise<void>((resolve, reject) => { server.close((error) => { if (error) { reject(error) } else { resolve() } }) }), origin: `http://127.0.0.1:${address.port}`, } } test('should omit exports fields from publishConfig in scoped packages', async (t) => { const { tmpDir, packageJsonPath } = t.context // Create a package.json with publishConfig that includes exports field const packageJson = { name: 'test-package', version: '1.0.0', publishConfig: { registry: 'https://custom-registry.com', access: 'public', exports: { '.': './dist/index.js', './package.json': './package.json', }, tag: 'beta', }, napi: { binaryName: 'test-package', targets: ['x86_64-unknown-linux-gnu'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) // Check that the scoped package directory was created const scopedDir = join(tmpDir, 'npm', 'linux-x64-gnu') t.true(existsSync(scopedDir)) // Read the generated package.json for the scoped package const scopedPackageJsonPath = join(scopedDir, 'package.json') t.true(existsSync(scopedPackageJsonPath)) const scopedPackageJson = JSON.parse( await readFile(scopedPackageJsonPath, 'utf-8'), ) // Verify that publishConfig only contains registry and access, not exports t.truthy(scopedPackageJson.publishConfig) t.is(scopedPackageJson.publishConfig.registry, 'https://custom-registry.com') t.is(scopedPackageJson.publishConfig.access, 'public') t.is(scopedPackageJson.publishConfig.exports, undefined) t.is(scopedPackageJson.publishConfig.tag, undefined) }) test('should handle package without publishConfig', async (t) => { const { tmpDir, packageJsonPath } = t.context // Create a package.json without publishConfig const packageJson = { name: 'test-package-no-config', version: '1.0.0', napi: { binaryName: 'test-package-no-config', targets: ['x86_64-unknown-linux-gnu'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) // Check that the scoped package directory was created const scopedDir = join(tmpDir, 'npm', 'linux-x64-gnu') t.true(existsSync(scopedDir)) // Read the generated package.json for the scoped package const scopedPackageJsonPath = join(scopedDir, 'package.json') const scopedPackageJson = JSON.parse( await readFile(scopedPackageJsonPath, 'utf-8'), ) // Verify that publishConfig is not present when not in source t.is(scopedPackageJson.publishConfig, undefined) }) test('should preserve only registry and access in publishConfig', async (t) => { const { tmpDir, packageJsonPath } = t.context // Create a package.json with minimal publishConfig const packageJson = { name: 'test-package-minimal', version: '1.0.0', publishConfig: { registry: 'https://npm.company.com', access: 'restricted', }, napi: { binaryName: 'test-package-minimal', targets: ['aarch64-apple-darwin'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) // Check that the scoped package directory was created const scopedDir = join(tmpDir, 'npm', 'darwin-arm64') t.true(existsSync(scopedDir)) // Read the generated package.json for the scoped package const scopedPackageJsonPath = join(scopedDir, 'package.json') const scopedPackageJson = JSON.parse( await readFile(scopedPackageJsonPath, 'utf-8'), ) // Verify that publishConfig contains exactly registry and access t.truthy(scopedPackageJson.publishConfig) t.is(scopedPackageJson.publishConfig.registry, 'https://npm.company.com') t.is(scopedPackageJson.publishConfig.access, 'restricted') t.is(Object.keys(scopedPackageJson.publishConfig).length, 2) }) test('should handle WASM targets correctly with publishConfig', async (t) => { const { tmpDir, packageJsonPath } = t.context const registryServer = await startRegistryServer() // Create a package.json for WASM target const packageJson = { name: 'test-wasm-package', version: '1.0.0', publishConfig: { registry: `${registryServer.origin}/wasm`, access: 'public', exports: './wasm.js', browser: './browser.js', }, napi: { binaryName: 'test-wasm-package', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) try { await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) // Check that the scoped package directory was created const scopedDir = join(tmpDir, 'npm', 'wasm32-wasi') t.true(existsSync(scopedDir)) // Read the generated package.json for the scoped package const scopedPackageJsonPath = join(scopedDir, 'package.json') const scopedPackageJson = JSON.parse( await readFile(scopedPackageJsonPath, 'utf-8'), ) // Verify that publishConfig is correctly filtered for WASM too t.truthy(scopedPackageJson.publishConfig) t.is( scopedPackageJson.publishConfig.registry, `${registryServer.origin}/wasm`, ) t.is(scopedPackageJson.publishConfig.access, 'public') t.is(scopedPackageJson.publishConfig.exports, undefined) t.is(scopedPackageJson.publishConfig.browser, undefined) // Verify WASM-specific fields are set correctly t.truthy(scopedPackageJson.main) t.truthy(scopedPackageJson.browser) t.truthy(scopedPackageJson.dependencies) } finally { await registryServer.close() } }) test('should preserve stricter node engine ranges for WASM targets', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-wasm-engines', version: '1.0.0', engines: { node: '>=18', }, napi: { binaryName: 'test-wasm-engines', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedPackageJson = JSON.parse( await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'), ) t.is(scopedPackageJson.engines.node, '>=18') }) test('should intersect mixed node engine ranges with the WASI minimum', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-wasm-mixed-engines', version: '1.0.0', engines: { node: '>=12 <14 || >=18', }, napi: { binaryName: 'test-wasm-mixed-engines', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedPackageJson = JSON.parse( await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'), ) t.is(scopedPackageJson.engines.node, '>=18.0.0') }) test('should preserve sibling engine constraints when node is missing for WASM targets', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-wasm-sibling-engines', version: '1.0.0', engines: { npm: '>=10', }, napi: { binaryName: 'test-wasm-sibling-engines', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedPackageJson = JSON.parse( await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'), ) t.deepEqual(scopedPackageJson.engines, { npm: '>=10', node: '>=14.0.0', }) }) test('should replace an exact node engine below the WASI minimum for WASM targets', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-wasm-exact-node-engine', version: '1.0.0', engines: { node: '13.0.0', }, napi: { binaryName: 'test-wasm-exact-node-engine', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedPackageJson = JSON.parse( await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'), ) t.is(scopedPackageJson.engines.node, '>=14.0.0') }) test('should drop exact node engine branches below the WASI minimum for WASM targets', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-wasm-exact-node-branch', version: '1.0.0', engines: { node: '13.0.0 || >=18', }, napi: { binaryName: 'test-wasm-exact-node-branch', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedPackageJson = JSON.parse( await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'), ) t.is(scopedPackageJson.engines.node, '>=18.0.0') }) test('should set @emnapi/core and @emnapi/runtime versions to match emnapi for WASM targets', async (t) => { const { tmpDir, packageJsonPath } = t.context const packageJson = { name: 'test-emnapi-versions', version: '1.0.0', napi: { binaryName: 'test-emnapi-versions', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) const scopedDir = join(tmpDir, 'npm', 'wasm32-wasi') const scopedPackageJson = JSON.parse( await readFile(join(scopedDir, 'package.json'), 'utf-8'), ) const emnapiVersion = require('emnapi/package.json').version t.is(scopedPackageJson.dependencies['@emnapi/core'], emnapiVersion) t.is(scopedPackageJson.dependencies['@emnapi/runtime'], emnapiVersion) }) test.serial( 'should reject an empty latest dist-tag when resolving wasm runtime metadata', async (t) => { const { tmpDir, packageJsonPath } = t.context const registryServer = await startRegistryServer({ 'dist-tags': { latest: ' ', }, }) process.env.npm_config_registry = `${registryServer.origin}/npm` const packageJson = { name: 'test-wasm-empty-latest-tag', version: '1.0.0', napi: { binaryName: 'test-wasm-empty-latest-tag', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) try { const error = await t.throwsAsync(() => createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }), ) t.regex(error.message, /did not include a latest dist-tag/) t.deepEqual(registryServer.requests, ['/npm/@napi-rs/wasm-runtime']) } finally { await registryServer.close() } }, ) test.serial( 'should ignore publishConfig.registry when resolving wasm runtime metadata', async (t) => { const { tmpDir, packageJsonPath } = t.context const registryServer = await startRegistryServer() const envRegistryServer = await startRegistryServer() process.env.npm_config_registry = `${envRegistryServer.origin}/env` const packageJson = { name: 'test-wasm-publish-registry', version: '1.0.0', publishConfig: { registry: `${registryServer.origin}/custom`, }, napi: { binaryName: 'test-wasm-publish-registry', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) try { await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) t.deepEqual(registryServer.requests, []) t.deepEqual(envRegistryServer.requests, ['/env/@napi-rs/wasm-runtime']) } finally { await registryServer.close() await envRegistryServer.close() } }, ) test.serial( 'should resolve wasm runtime metadata from npm_config_registry', async (t) => { const { tmpDir, packageJsonPath } = t.context const registryServer = await startRegistryServer() process.env.npm_config_registry = `${registryServer.origin}/npm` const packageJson = { name: 'test-wasm-env-registry', version: '1.0.0', napi: { binaryName: 'test-wasm-env-registry', targets: ['wasm32-wasi-preview1-threads'], }, } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) try { await createNpmDirs({ cwd: tmpDir, packageJsonPath: 'package.json', }) t.deepEqual(registryServer.requests, ['/npm/@napi-rs/wasm-runtime']) } finally { await registryServer.close() } }, )