repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
scripts/build/build.ts
Rawimport { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import {
copyFileSync,
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { bundleExtension } from "./bundle.ts";
import { generatePackageJson } from "./manifest.ts";
import { publishPackage } from "./publish.ts";
import { readBuildSpec } from "./spec.ts";
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
export const DIST_ROOT = join(ROOT, "dist");
export interface BuildExtensionOptions {
/** Repository root; defaults to this checkout. */
root?: string;
/** Canonical package parent; defaults to `<root>/dist`. */
distRoot?: string;
release?: boolean;
}
export interface BuiltPackage {
name: string;
dir: string;
entries: string[];
}
export function allExtensionNames(root = ROOT): string[] {
return readdirSync(join(root, "extensions"), { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() &&
existsSync(join(root, "extensions", entry.name, "package.json")),
)
.map((entry) => entry.name)
.sort();
}
export function rootVersion(root = ROOT): string {
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
version?: string;
};
if (!pkg.version) throw new Error("root package.json has no version");
return pkg.version;
}
/**
* Externals declared with an incompatibility reason ship as a real dependency
* tree inside the package, so a relocated copy resolves them without the
* workspace. Peer dependencies stay Pi-provided and are never installed.
*/
function installExternals(packageDir: string): void {
const result = spawnSync(
process.platform === "win32" ? "npm.cmd" : "npm",
[
"install",
"--omit=dev",
"--legacy-peer-deps",
"--ignore-scripts",
"--no-audit",
"--no-fund",
"--no-package-lock",
"--prefer-offline",
"--loglevel=error",
],
{ cwd: packageDir, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
);
if (result.status !== 0)
throw new Error(
`npm install of external dependencies failed:\n${result.stderr || result.stdout}`,
);
}
/**
* Build one extension into `<distRoot>/<name>`.
* Everything is produced in a staging directory first; the canonical package is
* touched only once the staging content is complete.
*/
export async function buildExtension(
name: string,
options: BuildExtensionOptions = {},
): Promise<BuiltPackage> {
const root = options.root ?? ROOT;
const distRoot = options.distRoot ?? join(root, "dist");
const spec = readBuildSpec(join(root, "extensions", name));
const staging = join(distRoot, ".staging", `${name}-${randomUUID()}`);
mkdirSync(staging, { recursive: true });
try {
const bundled = await bundleExtension(spec, staging, {
release: options.release,
});
for (const resource of spec.resources) {
const destination = join(staging, resource);
mkdirSync(dirname(destination), { recursive: true });
cpSync(join(spec.dir, resource), destination, {
recursive: true,
dereference: true,
});
}
for (const [destination, source] of Object.entries(spec.assets)) {
const target = join(staging, destination);
mkdirSync(dirname(target), { recursive: true });
copyFileSync(source, target);
}
writeFileSync(
join(staging, "package.json"),
`${JSON.stringify(generatePackageJson(spec, rootVersion(root)), null, "\t")}\n`,
);
if (Object.keys(spec.externals).length) installExternals(staging);
const target = join(distRoot, name);
publishPackage(staging, target, bundled.entries);
return { name, dir: target, entries: bundled.entries };
} catch (error) {
rmSync(staging, { recursive: true, force: true });
throw error;
} finally {
const stagingRoot = join(distRoot, ".staging");
if (existsSync(stagingRoot)) {
try {
rmdirSync(stagingRoot);
} catch {
// Another build is still staging.
}
}
}
}