parent
3395b91ad1
commit
b331b9f6a3
12 changed files with 433 additions and 75 deletions
243
create-web-release.mjs
Normal file
243
create-web-release.mjs
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { access, cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".");
|
||||
const packageJsonPath = path.join(repositoryRoot, "package.json");
|
||||
|
||||
function usage() {
|
||||
return `Usage: node create-web-release.mjs [options]
|
||||
|
||||
Build and pack the browser package using the version of the root Cargo package.
|
||||
|
||||
Options:
|
||||
--skip-build Pack the existing dist/ and wasm/pkg/ artifacts
|
||||
--output-dir <path> Write the archive to this directory (default: repository root)
|
||||
--help Show this help
|
||||
`;
|
||||
}
|
||||
|
||||
function parseArguments(arguments_) {
|
||||
const options = {
|
||||
outputDir: repositoryRoot,
|
||||
skipBuild: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
if (argument === "--help") {
|
||||
options.help = true;
|
||||
} else if (argument === "--skip-build") {
|
||||
options.skipBuild = true;
|
||||
} else if (argument === "--output-dir") {
|
||||
const outputDir = arguments_[index + 1];
|
||||
if (!outputDir || outputDir.startsWith("--")) {
|
||||
throw new Error("--output-dir requires a directory path");
|
||||
}
|
||||
options.outputDir = path.resolve(repositoryRoot, outputDir);
|
||||
index += 1;
|
||||
} else if (argument.startsWith("--output-dir=")) {
|
||||
const outputDir = argument.slice("--output-dir=".length);
|
||||
if (!outputDir) {
|
||||
throw new Error("--output-dir requires a directory path");
|
||||
}
|
||||
options.outputDir = path.resolve(repositoryRoot, outputDir);
|
||||
} else {
|
||||
throw new Error(`Unknown option: ${argument}`);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readJson(filePath) {
|
||||
const source = await readFile(filePath, "utf8");
|
||||
try {
|
||||
return JSON.parse(source);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON in ${path.relative(repositoryRoot, filePath)}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function run(command, arguments_, options = {}) {
|
||||
const renderedArguments = arguments_.map((argument) => JSON.stringify(argument)).join(" ");
|
||||
console.log(`\n> ${command}${renderedArguments ? ` ${renderedArguments}` : ""}`);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, arguments_, {
|
||||
cwd: options.cwd ?? repositoryRoot,
|
||||
env: options.env ?? process.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
child.once("error", (error) => {
|
||||
reject(new Error(`Failed to run ${command}: ${error.message}`, { cause: error }));
|
||||
});
|
||||
child.once("exit", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const reason = signal ? `signal ${signal}` : `exit code ${code}`;
|
||||
reject(new Error(`${command} failed with ${reason}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readCargoVersion() {
|
||||
let stdout;
|
||||
try {
|
||||
({ stdout } = await execFileAsync(
|
||||
"cargo",
|
||||
[
|
||||
"metadata",
|
||||
"--no-deps",
|
||||
"--format-version",
|
||||
"1",
|
||||
"--manifest-path",
|
||||
path.join(repositoryRoot, "Cargo.toml"),
|
||||
],
|
||||
{ cwd: repositoryRoot, maxBuffer: 1024 * 1024 },
|
||||
));
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to read the root Cargo package version: ${error.message}`, {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
throw new Error("cargo metadata returned invalid JSON", { cause: error });
|
||||
}
|
||||
|
||||
const rootPackage = metadata.packages?.find((packageMetadata) => packageMetadata.name === "mtp");
|
||||
if (!rootPackage || typeof rootPackage.version !== "string") {
|
||||
throw new Error("The root Cargo package named 'mtp' was not found");
|
||||
}
|
||||
|
||||
return rootPackage.version;
|
||||
}
|
||||
|
||||
function packageRelativePath(entry) {
|
||||
if (typeof entry !== "string" || entry.length === 0) {
|
||||
throw new Error("package.json files entries must be non-empty strings");
|
||||
}
|
||||
|
||||
const relativePath = entry.replace(/\/$/, "");
|
||||
if (
|
||||
!relativePath ||
|
||||
path.isAbsolute(relativePath) ||
|
||||
relativePath.split(/[\\/]/u).includes("..") ||
|
||||
relativePath.includes("*")
|
||||
) {
|
||||
throw new Error(`Unsupported package file entry: ${entry}`);
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
async function copyPackageFiles(stageRoot, packageJson) {
|
||||
if (!Array.isArray(packageJson.files)) {
|
||||
throw new Error("package.json must declare a files array for Web releases");
|
||||
}
|
||||
|
||||
for (const entry of packageJson.files) {
|
||||
const relativePath = packageRelativePath(entry);
|
||||
const sourcePath = path.join(repositoryRoot, relativePath);
|
||||
const destinationPath = path.join(stageRoot, relativePath);
|
||||
|
||||
try {
|
||||
await access(sourcePath);
|
||||
} catch (error) {
|
||||
throw new Error(`Release file is missing: ${relativePath}`, { cause: error });
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(destinationPath), { recursive: true });
|
||||
await cp(sourcePath, destinationPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function createRelease({ outputDir, packageJson, version }) {
|
||||
const stageRoot = await mkdtemp(path.join(os.tmpdir(), "mtp-web-release-"));
|
||||
const stagedPackageJson = {
|
||||
...packageJson,
|
||||
version,
|
||||
};
|
||||
|
||||
try {
|
||||
await writeFile(
|
||||
path.join(stageRoot, "package.json"),
|
||||
`${JSON.stringify(stagedPackageJson, null, 2)}\n`,
|
||||
);
|
||||
await copyPackageFiles(stageRoot, packageJson);
|
||||
|
||||
const stagedWasmPackagePath = path.join(stageRoot, "wasm", "pkg", "package.json");
|
||||
const stagedWasmPackageJson = await readJson(stagedWasmPackagePath);
|
||||
stagedWasmPackageJson.version = version;
|
||||
await writeFile(
|
||||
stagedWasmPackagePath,
|
||||
`${JSON.stringify(stagedWasmPackageJson, null, 2)}\n`,
|
||||
);
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const archiveName = `${packageJson.name}-${version}.tgz`;
|
||||
const archivePath = path.join(outputDir, archiveName);
|
||||
await rm(archivePath, { force: true });
|
||||
|
||||
await run("npm", ["pack", "--pack-destination", outputDir], { cwd: stageRoot });
|
||||
|
||||
try {
|
||||
await access(archivePath);
|
||||
} catch (error) {
|
||||
throw new Error(`npm pack did not create ${archiveName}`, { cause: error });
|
||||
}
|
||||
|
||||
return archivePath;
|
||||
} finally {
|
||||
await rm(stageRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
if (options.help) {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
|
||||
const packageJson = await readJson(packageJsonPath);
|
||||
if (packageJson.name !== "mtp") {
|
||||
throw new Error("package.json must describe the 'mtp' Web package");
|
||||
}
|
||||
|
||||
const version = await readCargoVersion();
|
||||
console.log(`Using Cargo package version ${version}`);
|
||||
|
||||
if (!options.skipBuild) {
|
||||
await run("pnpm", ["run", "clean"]);
|
||||
await run("pnpm", ["run", "build"]);
|
||||
}
|
||||
|
||||
const archivePath = await createRelease({
|
||||
outputDir: options.outputDir,
|
||||
packageJson,
|
||||
version,
|
||||
});
|
||||
console.log(`\nCreated ${path.relative(repositoryRoot, archivePath) || archivePath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`\n${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Reference in a new issue