Files
ts-scripts/single-scripts/build.ts
2026-02-08 23:33:17 +08:00

156 lines
4.5 KiB
TypeScript
Executable File

#!/usr/bin/env runts -- --allow-all
import {
execCommandShell,
existsPath,
log,
stringifyPretty,
term,
uint8ArrayToHexString,
writeStringToFile,
} from "https://script.hatter.ink/@35/deno-commons-mod.ts";
import {parseArgs} from "jsr:@std/cli/parse-args";
import {readFileToString} from "https://global.hatter.ink/script/get/@1/deno-commons-1.6.0-mod.ts";
async function main() {
const flags = parseArgs(Deno.args, {
boolean: [
"help",
"no-min",
"skip-sign",
"force",
],
alias: {
h: "help",
S: "skip-sign",
M: "no-min",
F: "force",
},
});
if (flags.help) {
console.log(term.auto(`Usage:
[blue][bold]build.ts[//] --help
[blue][bold]build.ts[//] [-M|--no-min] [-S|--skip-sign] [-F|--force] FILES`));
return;
}
if (flags._.length === 0) {
log.error(`No files specified`);
return;
}
flags.min = !flags["no-min"];
const filesCount = flags._.length;
log.info(`Total ${filesCount} file(s)`);
for (let i = 0; i < filesCount; i++) {
const file = flags._[i];
if (file.includes(".bundle.")) {
log.warn(`Skip bundle file: ${file} #${i + 1} of ${filesCount}`);
continue;
}
log.info(`Building file: ${file} #${i + 1} of ${filesCount}`);
await buildFile(file, flags);
}
}
function getBundleFilename(file: string): string {
const lastIndexOfDot = file.lastIndexOf(".");
if (lastIndexOfDot === -1) {
return `${file}.bundle.ts`;
}
return `${file.substring(0, lastIndexOfDot)}.bundle.${
file.substring(lastIndexOfDot + 1)
}`;
}
async function sha256OfString(input: string): Promise<string> {
const data = new TextEncoder().encode(input);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return uint8ArrayToHexString(new Uint8Array(hashBuffer));
}
interface SourceFileMeta {
sha256: string;
builtTime: number;
min: boolean;
}
async function writeBundleSourceSha256(
sourceFile: string,
bundleFile: string,
flags: any,
) {
const sourceFileSha256 = `${bundleFile}.source-meta`;
const sourceFileContent = await readFileToString(sourceFile);
const sourceFileContentSha256 = await sha256OfString(sourceFileContent);
await writeStringToFile(
sourceFileSha256,
stringifyPretty({
sha256: sourceFileContentSha256,
builtTime: Date.now(),
min: flags.min,
} as SourceFileMeta),
);
}
async function checkBundleSourceSha256(
sourceFile: string,
bundleFile: string,
flags: any,
) {
const sourceFileSha256 = `${bundleFile}.source-meta`;
if (!await existsPath(sourceFileSha256)) {
return false;
}
const sourceFileSha256Content = await readFileToString(sourceFileSha256);
const sourceFileSha256Meta = JSON.parse(
sourceFileSha256Content,
) as SourceFileMeta;
if (sourceFileSha256Meta.min !== flags.min) {
return false;
}
const sourceFileContent = await readFileToString(sourceFile);
const sourceFileContentSha256 = await sha256OfString(sourceFileContent);
return sourceFileContentSha256 === sourceFileSha256Meta.sha256;
}
async function buildFile(file: string, flags: any) {
const denoCmd = "deno";
const bundleFile = getBundleFilename(file);
const denoBundleArgs = ["bundle"];
if (flags.min) {
denoBundleArgs.push("--minify");
}
denoBundleArgs.push(file);
denoBundleArgs.push("-o");
denoBundleArgs.push(bundleFile);
const isSourceFileMatches = await checkBundleSourceSha256(
file,
bundleFile,
flags,
);
if (isSourceFileMatches && !flags.force) {
log.info(`Check file ${file} sha256 matches, skip build`);
return;
}
if (isSourceFileMatches) {
log.info(`Force build file is on`);
}
log.debug("Run deno build: ", denoCmd, denoBundleArgs);
await execCommandShell(denoCmd, denoBundleArgs);
if (flags["skip-sign"]) {
log.warn(`Skip signature for file: ${bundleFile}`);
} else {
await execCommandShell("script-sign.rs", [bundleFile]);
}
await execCommandShell("chmod", ["+x", bundleFile]);
await writeBundleSourceSha256(file, bundleFile, flags);
}
await main();
// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260208T233252+08:00.MEUCIBDG/ENx5vAqlNOXA0j9
// j8lVm77bTAIYJGQTB0WRz4VSAiEAjGRY1+99Y1srS8ohxkJ5GFhX7skwAIZTnhlBjXlVRd4=