UNPKG

@northegg/node-sea

Version:
200 lines (194 loc) 7.34 kB
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); } }