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
text/typescript
// 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);
});
}