UNPKG

slint-ui

Version:

Slint is a declarative GUI toolkit to build native user interfaces for desktop and embedded applications.

257 lines (240 loc) 8.67 kB
// Copyright © SixtyFPS GmbH <info@slint.dev> // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 // Shared helpers for assembling the slint-ui npm packages, used both by the // publish workflow (as a CLI, e.g. `node packaging.mts pack-binary …`) and by // the registry e2e test (which imports the functions directly). This is the // single source of truth for the set of platform binary packages. // // Run with Node's TypeScript type stripping (Node >= 23, or --experimental-strip-types). import { execFile } from "node:child_process"; import { cpSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const here = dirname(fileURLToPath(import.meta.url)); // api/node/scripts const nodeDir = join(here, ".."); // api/node const repoRoot = join(nodeDir, "..", ".."); // repository root /** The napi-rs platform suffixes for the binary packages. */ export const NAPI_TARGETS = [ "linux-x64-gnu", "linux-arm64-gnu", "darwin-arm64", "win32-x64-msvc", "win32-arm64-msvc", ] as const; /** Base npm package names for the release and dev platform binaries. */ export const BASE_BINARY_PACKAGE = "@slint-ui/slint-ui-binary"; export const DEV_BINARY_PACKAGE = "@slint-ui/slint-ui-dev-binary"; /** Full npm package names for a binary base name, e.g. `@slint-ui/slint-ui-binary`. */ export function binaryPackageNames( base: string, targets: readonly string[] = NAPI_TARGETS, ): string[] { return targets.map((target) => `${base}-${target}`); } export interface RunResult { stdout: string; stderr: string; } /** Run an external command, capturing its output and throwing a readable error. */ export async function run( cmd: string, args: string[], opts: { cwd?: string; env?: NodeJS.ProcessEnv } = {}, ): Promise<RunResult> { try { const { stdout, stderr } = await execFileAsync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, // npm/npx/pnpm are .cmd shims on Windows, which execFile cannot launch // directly; run through the shell there. (The args below are package // names and ./-relative paths, so no extra quoting is needed.) shell: process.platform === "win32", ...opts, }); return { stdout, stderr }; } catch (error) { const e = error as { stdout?: string; stderr?: string; code?: number }; throw new Error( `${cmd} ${args.join(" ")} failed (${e.code}):\n${e.stdout}\n${e.stderr}`, ); } } function editManifest( file: string, edit: (manifest: Record<string, any>) => void, ): void { const manifest = JSON.parse(readFileSync(file, "utf8")); edit(manifest); writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`); } /** Pack a single platform's prebuilt native binary into an npm package tarball. */ export async function packBinary(opts: { config: string; // napi binaries config (binaries.json / binaries-dev.json) target: string; // napi-rs platform suffix, e.g. linux-x64-gnu binary: string; // the <name>.<target>.node prefix, e.g. slint-ui / slint-ui-dev dest: string; // directory the .tgz is written to }): Promise<void> { const { config, target, binary, dest } = opts; const npmDir = `npm-${binary}`; await run( "npx", [ "napi", "create-npm-dirs", "--npm-dir", `./${npmDir}`, "-c", `./${config}`, ], { cwd: nodeDir }, ); cpSync( join(nodeDir, `${binary}.${target}.node`), join(nodeDir, npmDir, target, `${binary}.${target}.node`), ); const pkgDir = join(nodeDir, npmDir, target); await run( "pnpm", [ "pkg", "set", "repository.type=git", "repository.url=https://github.com/slint-ui/slint", ], { cwd: pkgDir }, ); await run("pnpm", ["pack", "--pack-destination", dest], { cwd: pkgDir }); } /** * Inject the base binary optionalDependencies and the optional slint-ui-dev peer * dependency into the main slint-ui package.json. Only the release base binaries * are optional dependencies of the main package, so `npm i slint-ui` stays lean; * the dev binaries ship via the separate slint-ui-dev package, declared here as * an optional peer. */ export function setMainBinaryDeps(opts: { version: string; targets?: readonly string[]; }): void { const { version, targets } = opts; editManifest(join(nodeDir, "package.json"), (manifest) => { manifest.optionalDependencies ??= {}; for (const pkg of binaryPackageNames(BASE_BINARY_PACKAGE, targets)) { manifest.optionalDependencies[pkg] = version; } manifest.peerDependencies = { ...manifest.peerDependencies, "slint-ui-dev": version, }; manifest.peerDependenciesMeta = { ...manifest.peerDependenciesMeta, "slint-ui-dev": { optional: true }, }; }); } /** * Assemble and pack the slint-ui-dev meta-package: it ships the dev loader and * pulls in the matching platform's dev binary. Requires the dev loader * (rust-module-dev.cjs) to be built in api/node. */ export async function packDevMeta(opts: { version: string; dest: string; targets?: readonly string[]; }): Promise<void> { const { version, dest, targets } = opts; const devDir = join(nodeDir, "dev-package"); cpSync(join(repoRoot, "LICENSE.md"), join(devDir, "LICENSE.md")); cpSync( join(nodeDir, "rust-module-dev.cjs"), join(devDir, "rust-module-dev.cjs"), ); editManifest(join(devDir, "package.json"), (manifest) => { manifest.version = version; manifest.optionalDependencies ??= {}; for (const pkg of binaryPackageNames(DEV_BINARY_PACKAGE, targets)) { manifest.optionalDependencies[pkg] = version; } manifest.peerDependencies = { ...manifest.peerDependencies, "slint-ui": version, }; }); await run("pnpm", ["pack", "--pack-destination", dest], { cwd: devDir }); } /** * Publish every *.tgz in `dir` to a registry. Used identically against the * throwaway Verdaccio registry (the e2e gate) and the real npm registry; only * the registry URL (and the ambient auth in env) differ. */ export async function publishAll(opts: { dir: string; registry: string; tag?: string; env?: NodeJS.ProcessEnv; }): Promise<void> { const { dir, registry, tag, env } = opts; const tagArgs = tag ? ["--tag", tag] : []; for (const file of readdirSync(dir)) { if (!file.endsWith(".tgz")) { continue; } await run( "pnpm", [ "publish", "--no-git-checks", "--access", "public", ...tagArgs, "--registry", registry, join(dir, file), ], { cwd: dir, env }, ); } } async function main(argv: string[]): Promise<void> { const [command, ...args] = argv; switch (command) { case "pack-binary": { const [config, target, binary, dest] = args; await packBinary({ config, target, binary, dest }); break; } case "set-main-deps": { const [version, ...targets] = args; setMainBinaryDeps({ version, targets: targets.length ? targets : undefined, }); break; } case "pack-dev-meta": { const [version, dest, ...targets] = args; await packDevMeta({ version, dest, targets: targets.length ? targets : undefined, }); break; } case "publish-all": { const [dir, registry, tag] = args; await publishAll({ dir, registry, tag: tag || undefined }); break; } default: throw new Error(`unknown command: ${command}`); } } // Run as a CLI when executed directly (rather than imported). if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { main(process.argv.slice(2)).catch((error) => { console.error(error); process.exit(1); }); }