@northegg/node-sea
Version:
200 lines (194 loc) • 7.34 kB
text/typescript
import { exec as exec_origin } from "child_process";
import util from "util";
import { basename, dirname, extname, join, resolve } from "path";
import { copyFile, writeFile, mkdir, readFile } from "fs/promises";
import {
is_directory_exists,
is_file_exists,
spinner_log,
get_node_executable,
nccPack,
} from "./utils.js";
import ora from "ora";
import { rimraf } from "rimraf";
import { randomUUID } from "crypto";
// @ts-ignore
import { inject } from "postject";
// promisify exec, let exec block until the process exits
const exec = util.promisify(exec_origin);
type Options = {
/** 输出的可执行文件名(无需包含文件扩展名) */
executable_name?: string;
/** 输出可执行文件路径(包括文件名及扩展名)。默认输出目录为 script_entry_path 目录下的 `dist` 文件夹,没有则会新建 `dist` 文件夹 */
executable_path?: string;
/** 关闭实验性警告。默认为 `true` */
disableExperimentalSEAWarning?: boolean;
/**启动快照支持。默认为 `false`,生成跨平台 SEA 时必须为 `false`。
*
* 当 useSnapshot 为 `true` 时,主脚本必须调用 `v8.startupSnapshot.setDeserializeMainFunction()` API 来配置用户启动最终可执行文件时需要运行的代码
*/
useSnapshot?: boolean;
/**V8 代码缓存支持。默认为 `false`,生成跨平台 SEA 时必须为 `false`。
*
* 注意:当 useCodeCache 为 true 时,动态导入 `import()` 不起作用。
* */
useCodeCache?: boolean;
/** 是否使用本地的node,默认为 `true`
*
* 如不使用本地的 node 则去 node 官方或者提供的镜像地址根据 `nodeVersion`、`arch`、`target` 参数查找下载
* */
useSystemNode?: boolean;
/** 要下载的 node 版本,默认为 `22.14.0` */
nodeVersion?: string;
/** node 架构,默认为 `x64` */
arch?: "x64" | "arm64";
/** 目标平台,默认为当前平台 */
target?: "win" | "darwin" | "linux";
/**资源文件
* @see https://nodejs.cn/api/single-executable-applications.html#资源
*/
assets?: {
[fileName: string]: string;
};
/** ts文件仅转译,不进行检查。默认为 `false` */
transpileOnly?: boolean;
/** node 镜像下载地址 如:https://registry.npmmirror.com/-/binary/node/ */
mirrorUrl?: string;
/** 开启 `debug` 模式时不会删除临时文件夹 */
debug?: boolean;
};
export default async function sea(
/** 入口文件路径(包括入口文件名及扩展名) */
script_entry_path: string,
options: Options = {}
) {
const {
disableExperimentalSEAWarning = true,
useSnapshot = false,
useCodeCache = false,
useSystemNode = true,
nodeVersion = "22.14.0",
arch = "x64",
target = process.platform.includes("win")
? "win"
: (process.platform as "win" | "darwin" | "linux"),
assets = undefined,
transpileOnly = false,
mirrorUrl,
executable_name,
debug = false,
} = options;
let { executable_path } = options;
const startDir = process.cwd();
// normalize the script_entry_path and executable_path
script_entry_path = resolve(process.cwd(), script_entry_path);
if (executable_path) {
executable_path = resolve(process.cwd(), executable_path);
} else {
console.warn("使用默认输出目录");
executable_path = resolve(
dirname(process.argv[1]!),
`./dist/${
executable_name ? executable_name : basename(script_entry_path, extname(script_entry_path))
}${target === "win" ? ".exe" : ""}`
);
if (await is_directory_exists(dirname(executable_path))) {
console.warn("默认输出目录 dist 已存在");
} else {
await spinner_log("创建输出文件目录 dist", async () => {
await mkdir(dirname(executable_path!));
});
}
}
// check if script_entry_path exists and is a file
if (!(await is_file_exists(script_entry_path))) {
throw new Error(`脚本文件 ${script_entry_path} 不存在`);
}
// check if executable directory exists
if (!(await is_directory_exists(dirname(executable_path)))) {
throw new Error(`输出执行文件目录 ${dirname(executable_path)} 不存在`);
}
// check if executable_path exists
if (await is_file_exists(executable_path)) {
console.warn(`可执行文件 ${executable_path} 已存在, 将被覆盖`);
}
// check node version, needs to be at least 20.0.0
if (process.version < "v20.0.0") {
throw new Error(`系统 Node 版本 ${process.version} 太老了, 至少需要 v20.0.0`);
}
const uuid = randomUUID();
// create a temporary directory for the processing work
const temp_dir = resolve(dirname(executable_path), `./${uuid}`);
// create the temporary directory if it does not exist
if (!(await is_directory_exists(temp_dir))) {
await spinner_log(`创建临时目录 ${temp_dir}`, async () => {
await mkdir(temp_dir);
});
}
try {
// 获取 node 可执行文件
const node_executable: string = await spinner_log(
`${useSystemNode ? "使用系统安装的 node" : `下载 node-v${nodeVersion}-${target}-${arch}`}`,
async () => {
return await get_node_executable(
temp_dir,
useSystemNode,
nodeVersion,
target,
arch,
mirrorUrl
);
}
);
// 复制可执行文件作为输出可执行文件
await copyFile(node_executable, executable_path);
// 将工作目录更改为temp_dir
process.chdir(temp_dir);
/** 调用ncc打包文件 */
const packFilePath = await nccPack(script_entry_path, { temp_dir, transpileOnly });
if (!packFilePath) return;
// Create a configuration file building a blob that can be injected into the single executable application
const preparation_blob_path = join(temp_dir, "sea-prep.blob");
const sea_config_path = join(temp_dir, "sea-config.json");
const sea_config = {
main: packFilePath,
output: preparation_blob_path,
disableExperimentalSEAWarning,
useSnapshot,
useCodeCache,
assets,
};
await spinner_log(`将配置文件写入 ${sea_config_path}`, async () => {
await writeFile(sea_config_path, JSON.stringify(sea_config));
});
// Generate the blob to be injected
await spinner_log(`生成blob到 ${preparation_blob_path}`, async () => {
await exec(`node --experimental-sea-config "${sea_config_path}"`);
});
// Inject the blob into the copied binary by running postject
await spinner_log(`将 blob 注入 ${basename(executable_path)}`, async () => {
const blob = await readFile(preparation_blob_path);
await inject(executable_path, "NODE_SEA_BLOB", blob, {
machoSegmentName: "NODE_SEA",
sentinelFuse: "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
});
});
// Remove the temporary directory
if (!debug) {
await spinner_log(`删除临时目录 ${temp_dir}`, async () => {
process.chdir(startDir);
await rimraf(temp_dir);
});
}
ora("All done!").succeed();
} catch (error) {
if (!debug) {
await spinner_log(`删除临时目录 ${temp_dir}`, async () => {
process.chdir(startDir);
await rimraf(temp_dir);
});
}
ora("打包出错!").fail();
console.log(error);
}
}