@actiondock/builder 2.0.11 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/dist/archive.d.ts +47 -0
- package/dist/archive.js +519 -0
- package/dist/build.d.ts +9 -0
- package/dist/build.js +576 -0
- package/dist/dependency-check.d.ts +14 -0
- package/dist/dependency-check.js +246 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +22 -0
- package/dist/exporter.d.ts +69 -0
- package/dist/exporter.js +761 -0
- package/dist/fs-utils.d.ts +16 -0
- package/dist/fs-utils.js +146 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +75 -0
- package/dist/manifest.js +213 -0
- package/dist/pack.d.ts +13 -0
- package/dist/pack.js +339 -0
- package/dist/planner.d.ts +68 -0
- package/dist/planner.js +681 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.js +1 -0
- package/package.json +15 -17
- package/src/archive.ts +0 -248
- package/src/build.ts +0 -106
- package/src/compiler.ts +0 -303
- package/src/errors.ts +0 -67
- package/src/exporter.ts +0 -680
- package/src/index.ts +0 -6
- package/src/planner.ts +0 -731
- package/src/types.ts +0 -337
package/src/archive.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { deflateRawSync, gzipSync } from "node:zlib";
|
|
3
|
-
import { basename, dirname, join, relative, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 纯 Node 实现的归档压缩模块。
|
|
7
|
-
*
|
|
8
|
-
* zip 与 tar.gz 均在进程内完成打包,不依赖宿主机的 zip / tar 命令行工具
|
|
9
|
-
* (Windows 与最小化 Linux 环境普遍缺少 zip 命令)。
|
|
10
|
-
* 压缩能力复用 node:zlib 内置的 deflateRaw(zip 条目)与 gzip(tar.gz 容器)。
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** 目录内相对路径条目(目录在前、同级按名称排序,保证归档内容顺序稳定) */
|
|
14
|
-
interface ArchiveEntry {
|
|
15
|
-
/** 相对根目录的 POSIX 风格路径(目录以 / 结尾仅出现在 zip 条目名层面) */
|
|
16
|
-
relPath: string;
|
|
17
|
-
/** 是否为目录 */
|
|
18
|
-
isDir: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 递归收集目录内全部条目 */
|
|
22
|
-
function collectEntries(dir: string, baseDir = dir): ArchiveEntry[] {
|
|
23
|
-
const entries: ArchiveEntry[] = [];
|
|
24
|
-
for (const name of readdirSync(dir).sort()) {
|
|
25
|
-
const fullPath = join(dir, name);
|
|
26
|
-
const stat = statSync(fullPath);
|
|
27
|
-
if (stat.isDirectory()) {
|
|
28
|
-
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: true });
|
|
29
|
-
entries.push(...collectEntries(fullPath, baseDir));
|
|
30
|
-
} else if (stat.isFile()) {
|
|
31
|
-
entries.push({ relPath: relative(baseDir, fullPath).split(sep).join("/"), isDir: false });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return entries;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* ------------------------------------------------------------------ */
|
|
38
|
-
/* zip 格式写入 */
|
|
39
|
-
/* ------------------------------------------------------------------ */
|
|
40
|
-
|
|
41
|
-
/** CRC32 查表(IEEE 802.3 多项式) */
|
|
42
|
-
const CRC32_TABLE: Uint32Array = (() => {
|
|
43
|
-
const table = new Uint32Array(256);
|
|
44
|
-
for (let n = 0; n < 256; n++) {
|
|
45
|
-
let c = n;
|
|
46
|
-
for (let k = 0; k < 8; k++) {
|
|
47
|
-
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
48
|
-
}
|
|
49
|
-
table[n] = c >>> 0;
|
|
50
|
-
}
|
|
51
|
-
return table;
|
|
52
|
-
})();
|
|
53
|
-
|
|
54
|
-
function crc32(buf: Buffer): number {
|
|
55
|
-
let crc = 0xffffffff;
|
|
56
|
-
for (let i = 0; i < buf.length; i++) {
|
|
57
|
-
crc = CRC32_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
|
|
58
|
-
}
|
|
59
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Unix 毫秒时间戳转 DOS 时间格式(date/time 各 16 位) */
|
|
63
|
-
function dosDateTime(ms: number): { date: number; time: number } {
|
|
64
|
-
const d = new Date(ms);
|
|
65
|
-
const year = Math.max(d.getFullYear(), 1980);
|
|
66
|
-
const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
|
|
67
|
-
const date = ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
|
|
68
|
-
return { date, time };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** central directory 记录(写入 EOCD 前聚合) */
|
|
72
|
-
interface ZipCentralRecord {
|
|
73
|
-
header: Buffer;
|
|
74
|
-
localOffset: number;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 将目录打包为标准 zip 归档(PKZIP APPNOTE 结构)。
|
|
79
|
-
*
|
|
80
|
-
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
81
|
-
* @param outPath 输出 zip 文件路径
|
|
82
|
-
*/
|
|
83
|
-
export function createZipArchive(dir: string, outPath: string): void {
|
|
84
|
-
const rootName = basename(dir);
|
|
85
|
-
const entries = collectEntries(dir);
|
|
86
|
-
const chunks: Buffer[] = [];
|
|
87
|
-
const centralRecords: ZipCentralRecord[] = [];
|
|
88
|
-
let offset = 0;
|
|
89
|
-
|
|
90
|
-
for (const entry of entries) {
|
|
91
|
-
// 归档内路径带根目录前缀;目录条目名以 / 结尾
|
|
92
|
-
const name = `${rootName}/${entry.relPath}${entry.isDir ? "/" : ""}`;
|
|
93
|
-
const nameBuf = Buffer.from(name, "utf8");
|
|
94
|
-
const stat = statSync(join(dir, entry.relPath));
|
|
95
|
-
const { date, time } = dosDateTime(stat.mtimeMs);
|
|
96
|
-
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(join(dir, entry.relPath));
|
|
97
|
-
|
|
98
|
-
let method = 0;
|
|
99
|
-
let payload = content;
|
|
100
|
-
if (!entry.isDir && content.length > 0) {
|
|
101
|
-
const deflated = deflateRawSync(content, { level: 9 });
|
|
102
|
-
if (deflated.length < content.length) {
|
|
103
|
-
method = 8;
|
|
104
|
-
payload = deflated;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const crc = crc32(content);
|
|
109
|
-
|
|
110
|
-
// Local File Header(30 字节 + 文件名)
|
|
111
|
-
const local = Buffer.alloc(30);
|
|
112
|
-
local.writeUInt32LE(0x04034b50, 0);
|
|
113
|
-
local.writeUInt16LE(20, 4);
|
|
114
|
-
local.writeUInt16LE(0x0800, 6); // UTF-8 文件名标志
|
|
115
|
-
local.writeUInt16LE(method, 8);
|
|
116
|
-
local.writeUInt16LE(time, 10);
|
|
117
|
-
local.writeUInt16LE(date, 12);
|
|
118
|
-
local.writeUInt32LE(crc, 14);
|
|
119
|
-
local.writeUInt32LE(payload.length, 18);
|
|
120
|
-
local.writeUInt32LE(content.length, 22);
|
|
121
|
-
local.writeUInt16LE(nameBuf.length, 26);
|
|
122
|
-
local.writeUInt16LE(0, 28);
|
|
123
|
-
chunks.push(local, nameBuf, payload);
|
|
124
|
-
|
|
125
|
-
// Central Directory Header(46 字节 + 文件名)
|
|
126
|
-
const central = Buffer.alloc(46);
|
|
127
|
-
central.writeUInt32LE(0x02014b50, 0);
|
|
128
|
-
central.writeUInt16LE(20, 4); // version made by
|
|
129
|
-
central.writeUInt16LE(20, 6); // version needed
|
|
130
|
-
central.writeUInt16LE(0x0800, 8);
|
|
131
|
-
central.writeUInt16LE(method, 10);
|
|
132
|
-
central.writeUInt16LE(time, 12);
|
|
133
|
-
central.writeUInt16LE(date, 14);
|
|
134
|
-
central.writeUInt32LE(crc, 16);
|
|
135
|
-
central.writeUInt32LE(payload.length, 20);
|
|
136
|
-
central.writeUInt32LE(content.length, 24);
|
|
137
|
-
central.writeUInt16LE(nameBuf.length, 28);
|
|
138
|
-
central.writeUInt16LE(0, 30); // extra len
|
|
139
|
-
central.writeUInt16LE(0, 32); // comment len
|
|
140
|
-
central.writeUInt16LE(0, 34); // disk start
|
|
141
|
-
central.writeUInt16LE(0, 36); // internal attrs
|
|
142
|
-
// 外部属性:Unix 权限左移 16 位,目录附加 MS-DOS 目录位
|
|
143
|
-
const extAttrs = ((entry.isDir ? 0o40755 : 0o100644) << 16) | (entry.isDir ? 0x10 : 0);
|
|
144
|
-
central.writeUInt32LE(extAttrs >>> 0, 38);
|
|
145
|
-
central.writeUInt32LE(offset, 42);
|
|
146
|
-
centralRecords.push({ header: Buffer.concat([central, nameBuf]), localOffset: offset });
|
|
147
|
-
|
|
148
|
-
offset += local.length + nameBuf.length + payload.length;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const centralDirOffset = offset;
|
|
152
|
-
let centralDirSize = 0;
|
|
153
|
-
for (const record of centralRecords) {
|
|
154
|
-
chunks.push(record.header);
|
|
155
|
-
centralDirSize += record.header.length;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// End of Central Directory Record(22 字节)
|
|
159
|
-
const eocd = Buffer.alloc(22);
|
|
160
|
-
eocd.writeUInt32LE(0x06054b50, 0);
|
|
161
|
-
eocd.writeUInt16LE(0, 4);
|
|
162
|
-
eocd.writeUInt16LE(0, 6);
|
|
163
|
-
eocd.writeUInt16LE(centralRecords.length, 8);
|
|
164
|
-
eocd.writeUInt16LE(centralRecords.length, 10);
|
|
165
|
-
eocd.writeUInt32LE(centralDirSize, 12);
|
|
166
|
-
eocd.writeUInt32LE(centralDirOffset, 16);
|
|
167
|
-
eocd.writeUInt16LE(0, 20); // comment len
|
|
168
|
-
chunks.push(eocd);
|
|
169
|
-
|
|
170
|
-
writeFileSync(outPath, Buffer.concat(chunks));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/* ------------------------------------------------------------------ */
|
|
174
|
-
/* tar.gz 格式写入 */
|
|
175
|
-
/* ------------------------------------------------------------------ */
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 构造单个条目的 USTAR 头块(512 字节)。
|
|
179
|
-
* 路径超长时按 USTAR prefix 字段拆分,仍放不下则抛错。
|
|
180
|
-
*/
|
|
181
|
-
function tarHeader(path: string, size: number, mtimeSec: number, isDir: boolean): Buffer {
|
|
182
|
-
let name = path;
|
|
183
|
-
let prefix = "";
|
|
184
|
-
if (Buffer.byteLength(name, "utf8") > 100) {
|
|
185
|
-
const split = name.lastIndexOf("/");
|
|
186
|
-
if (split <= 0 || split > 155 || name.length - split - 1 > 100) {
|
|
187
|
-
throw new Error(`归档路径超出 USTAR 字段容量: ${path}`);
|
|
188
|
-
}
|
|
189
|
-
prefix = name.slice(0, split);
|
|
190
|
-
name = name.slice(split + 1);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const buf = Buffer.alloc(512);
|
|
194
|
-
buf.write(name, 0, 100, "utf8");
|
|
195
|
-
buf.write(isDir ? "0000755" : "0000644", 100, 8, "ascii"); // mode
|
|
196
|
-
buf.write("0000000", 108, 8, "ascii"); // uid
|
|
197
|
-
buf.write("0000000", 116, 8, "ascii"); // gid
|
|
198
|
-
buf.write(size.toString(8).padStart(11, "0"), 124, 12, "ascii"); // size
|
|
199
|
-
buf.write(Math.floor(mtimeSec).toString(8).padStart(11, "0"), 136, 12, "ascii"); // mtime
|
|
200
|
-
buf.fill(" ", 148, 156); // chksum 占位(计算时视为空格)
|
|
201
|
-
buf.write(isDir ? "5" : "0", 156, 1, "ascii"); // typeflag
|
|
202
|
-
buf.write("ustar\0", 257, 6, "ascii"); // magic
|
|
203
|
-
buf.write("00", 263, 2, "ascii"); // version
|
|
204
|
-
if (prefix) buf.write(prefix, 345, 155, "utf8");
|
|
205
|
-
|
|
206
|
-
// 校验和:整块字节的无符号累加和,写作 6 位八进制 + NUL + 空格
|
|
207
|
-
let sum = 0;
|
|
208
|
-
for (let i = 0; i < 512; i++) sum += buf[i];
|
|
209
|
-
buf.write(sum.toString(8).padStart(6, "0"), 148, 7, "ascii");
|
|
210
|
-
buf.write("\0", 154, 1, "ascii");
|
|
211
|
-
buf.write(" ", 155, 1, "ascii");
|
|
212
|
-
return buf;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** 将字节数补齐到 512 的整数倍 */
|
|
216
|
-
function pad512(length: number): Buffer {
|
|
217
|
-
const rem = length % 512;
|
|
218
|
-
return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* 将目录打包为标准 tar.gz 归档(USTAR + gzip)。
|
|
223
|
-
*
|
|
224
|
-
* @param dir 待打包目录(目录名即归档内根文件夹名)
|
|
225
|
-
* @param outPath 输出 tar.gz 文件路径
|
|
226
|
-
*/
|
|
227
|
-
export function createTarGzArchive(dir: string, outPath: string): void {
|
|
228
|
-
const rootName = basename(dir);
|
|
229
|
-
const entries = collectEntries(dir);
|
|
230
|
-
const chunks: Buffer[] = [];
|
|
231
|
-
|
|
232
|
-
for (const entry of entries) {
|
|
233
|
-
const path = `${rootName}/${entry.relPath}`;
|
|
234
|
-
const fullPath = join(dir, entry.relPath);
|
|
235
|
-
const stat = statSync(fullPath);
|
|
236
|
-
const content = entry.isDir ? Buffer.alloc(0) : readFileSync(fullPath);
|
|
237
|
-
|
|
238
|
-
chunks.push(tarHeader(path, content.length, Math.floor(stat.mtimeMs / 1000), entry.isDir));
|
|
239
|
-
if (!entry.isDir) {
|
|
240
|
-
chunks.push(content, pad512(content.length));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// 归档结尾:两个全零块
|
|
245
|
-
chunks.push(Buffer.alloc(1024));
|
|
246
|
-
|
|
247
|
-
writeFileSync(outPath, gzipSync(Buffer.concat(chunks), { level: 9 }));
|
|
248
|
-
}
|
package/src/build.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
import { generateStandaloneEntrypoint, getPackageSlug } from "@actiondock/core";
|
|
4
|
-
import { BunCompiler } from "./compiler";
|
|
5
|
-
import { BuilderError } from "./errors";
|
|
6
|
-
import { BuildPlanner } from "./planner";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 独立二进制可执行文件构建选项。
|
|
10
|
-
*/
|
|
11
|
-
export interface BuildOptions {
|
|
12
|
-
/** 目标项目根目录 */
|
|
13
|
-
projectRoot: string;
|
|
14
|
-
/** 目标架构(如 "bun-linux-x64", "bun-darwin-arm64", "bun-windows-x64" 等) */
|
|
15
|
-
target?: string;
|
|
16
|
-
/** 输出可执行文件的目标路径(默认输出到 dist/ 目录) */
|
|
17
|
-
outfile?: string;
|
|
18
|
-
/** 是否开启代码压缩混淆(默认 true) */
|
|
19
|
-
minify?: boolean;
|
|
20
|
-
/** 是否编译为 V8/JavaScriptCore 字节码(默认 true) */
|
|
21
|
-
bytecode?: boolean;
|
|
22
|
-
/** 显式挑选打包的 Action ID 清单(用于按需子集打包) */
|
|
23
|
-
actions?: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 独立二进制构建完成后的元数据结果对象。
|
|
28
|
-
*/
|
|
29
|
-
export interface BuildResult {
|
|
30
|
-
/** 所属 Package ID */
|
|
31
|
-
packageId: string;
|
|
32
|
-
/** 打包的项目版本号 */
|
|
33
|
-
version: string;
|
|
34
|
-
/** 编译的目标平台架构 */
|
|
35
|
-
target: string;
|
|
36
|
-
/** 生成的独立二进制可执行文件绝对路径 */
|
|
37
|
-
executablePath: string;
|
|
38
|
-
/** 生成的 sidecar 元数据 JSON 文件绝对路径 */
|
|
39
|
-
metadataPath: string;
|
|
40
|
-
/** 打包内置的 Action ID 列表 */
|
|
41
|
-
actions: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 调用 Bun 原生编译引擎(Bun.build --compile)将 Action Package 打包为零外部依赖的独立二进制可执行文件。
|
|
46
|
-
*
|
|
47
|
-
* @param options 构建参数
|
|
48
|
-
* @returns 构建产物结果元数据
|
|
49
|
-
*/
|
|
50
|
-
export async function buildProject(options: BuildOptions): Promise<BuildResult> {
|
|
51
|
-
const root = resolve(options.projectRoot);
|
|
52
|
-
const plan = BuildPlanner.plan({
|
|
53
|
-
projectRoot: root,
|
|
54
|
-
actions: options.actions,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (plan.actions.length === 0) {
|
|
58
|
-
throw new BuilderError("No actions resolved for standalone compilation");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const buildDir = join(root, ".actiondock", ".build");
|
|
62
|
-
mkdirSync(buildDir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
const entryCode = generateStandaloneEntrypoint(
|
|
65
|
-
plan.packageId,
|
|
66
|
-
plan.version,
|
|
67
|
-
plan.description,
|
|
68
|
-
plan.actions.map((a) => ({ id: a.id, filePath: a.resolvedPath })),
|
|
69
|
-
plan.configDefs
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const entryFileName = `entry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.ts`;
|
|
73
|
-
const entryPath = join(buildDir, entryFileName);
|
|
74
|
-
writeFileSync(entryPath, entryCode, "utf-8");
|
|
75
|
-
|
|
76
|
-
const binaryName = getPackageSlug(plan.packageId);
|
|
77
|
-
const defaultOutfile = join(root, "dist", binaryName);
|
|
78
|
-
const outfile = resolve(options.outfile || defaultOutfile);
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const result = await BunCompiler.compile({
|
|
82
|
-
entrypoint: entryPath,
|
|
83
|
-
outfile,
|
|
84
|
-
target: options.target,
|
|
85
|
-
minify: options.minify,
|
|
86
|
-
bytecode: options.bytecode,
|
|
87
|
-
cwd: root,
|
|
88
|
-
packageId: plan.packageId,
|
|
89
|
-
version: plan.version,
|
|
90
|
-
actions: plan.actions.map((a) => a.id),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
packageId: result.packageId || plan.packageId,
|
|
95
|
-
version: result.version || plan.version,
|
|
96
|
-
target: result.target,
|
|
97
|
-
executablePath: result.executablePath,
|
|
98
|
-
metadataPath: result.metadataPath || "",
|
|
99
|
-
actions: plan.actions.map((a) => a.id),
|
|
100
|
-
};
|
|
101
|
-
} finally {
|
|
102
|
-
if (existsSync(entryPath)) {
|
|
103
|
-
rmSync(entryPath, { force: true });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/compiler.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { basename, dirname, resolve } from "node:path";
|
|
5
|
-
import { CompilerError, CompilerValidationError } from "./errors";
|
|
6
|
-
import type { BunCompilerOptions, BunCompilerResult } from "./types";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 官方支持的 Bun 独立二进制编译目标列表。
|
|
10
|
-
*/
|
|
11
|
-
const SUPPORTED_TARGETS = new Set<string>([
|
|
12
|
-
"bun",
|
|
13
|
-
"host",
|
|
14
|
-
"bun-linux-x64",
|
|
15
|
-
"linux-x64",
|
|
16
|
-
"bun-linux-arm64",
|
|
17
|
-
"linux-arm64",
|
|
18
|
-
"bun-darwin-x64",
|
|
19
|
-
"darwin-x64",
|
|
20
|
-
"bun-darwin-arm64",
|
|
21
|
-
"darwin-arm64",
|
|
22
|
-
"bun-windows-x64",
|
|
23
|
-
"windows-x64",
|
|
24
|
-
"bun-linux-x64-baseline",
|
|
25
|
-
"linux-x64-baseline",
|
|
26
|
-
"bun-linux-x64-modern",
|
|
27
|
-
"linux-x64-modern",
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 校验目标架构并规范化。
|
|
32
|
-
*/
|
|
33
|
-
function normalizeAndValidateTarget(target?: string): string {
|
|
34
|
-
if (!target || target === "host" || target === "bun") {
|
|
35
|
-
return "bun";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const cleanTarget = target.trim();
|
|
39
|
-
if (!SUPPORTED_TARGETS.has(cleanTarget)) {
|
|
40
|
-
throw new CompilerValidationError(
|
|
41
|
-
`Unsupported compilation target: '${target}'. Standalone compilation only supports Bun targets (e.g. linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64).`,
|
|
42
|
-
{ code: "UNSUPPORTED_TARGET", target }
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return cleanTarget.startsWith("bun-") ? cleanTarget : `bun-${cleanTarget}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 规范化编译器错误输出。
|
|
51
|
-
*/
|
|
52
|
-
function normalizeCompilerError(rawStderr: string, rawStdout: string, exitCode: number, target?: string): CompilerError {
|
|
53
|
-
const combined = `${rawStderr}\n${rawStdout}`.trim();
|
|
54
|
-
const lines = combined
|
|
55
|
-
.split(/\r?\n/)
|
|
56
|
-
.map((l) => l.trim())
|
|
57
|
-
.filter((l) => l.length > 0);
|
|
58
|
-
|
|
59
|
-
let code = "COMPILATION_FAILED";
|
|
60
|
-
if (combined.includes("Unsupported target")) {
|
|
61
|
-
code = "UNSUPPORTED_TARGET";
|
|
62
|
-
} else if (
|
|
63
|
-
combined.includes("Cannot find package") ||
|
|
64
|
-
combined.includes("Cannot find module") ||
|
|
65
|
-
combined.includes("Could not resolve")
|
|
66
|
-
) {
|
|
67
|
-
code = "MODULE_NOT_FOUND";
|
|
68
|
-
} else if (
|
|
69
|
-
combined.includes("SyntaxError") ||
|
|
70
|
-
combined.includes("Parse error") ||
|
|
71
|
-
combined.includes("Expected") ||
|
|
72
|
-
combined.includes("Unexpected")
|
|
73
|
-
) {
|
|
74
|
-
code = "SYNTAX_ERROR";
|
|
75
|
-
} else if (combined.includes("EACCES") || combined.includes("Permission denied")) {
|
|
76
|
-
code = "PERMISSION_DENIED";
|
|
77
|
-
} else if (combined.includes("ENOSPC") || combined.includes("no space left")) {
|
|
78
|
-
code = "NO_SPACE";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const summaryLine = lines.find((l) => l.startsWith("error:")) || lines[0] || "Bun compilation failed";
|
|
82
|
-
|
|
83
|
-
return new CompilerError(
|
|
84
|
-
`Bun compilation failed with exit code ${exitCode} (${code}): ${summaryLine}`,
|
|
85
|
-
{
|
|
86
|
-
code,
|
|
87
|
-
exitCode,
|
|
88
|
-
rawError: combined,
|
|
89
|
-
details: lines,
|
|
90
|
-
target,
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Bun 独立二进制编译器。
|
|
97
|
-
* 封装 bun build --compile 调用,提前校验选项组合,收集构建产物与元数据。
|
|
98
|
-
*/
|
|
99
|
-
export class BunCompiler {
|
|
100
|
-
/**
|
|
101
|
-
* 执行独立二进制编译。
|
|
102
|
-
*
|
|
103
|
-
* @param options 编译参数
|
|
104
|
-
* @returns 编译产物元数据结果
|
|
105
|
-
*/
|
|
106
|
-
public async compile(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
107
|
-
const startTime = Date.now();
|
|
108
|
-
|
|
109
|
-
// 1. 入口点存在性校验
|
|
110
|
-
if (!options.entrypoint) {
|
|
111
|
-
throw new CompilerValidationError("Entrypoint must be specified for standalone compilation", {
|
|
112
|
-
code: "INVALID_ENTRYPOINT",
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const resolvedEntry = resolve(options.entrypoint);
|
|
117
|
-
if (!existsSync(resolvedEntry)) {
|
|
118
|
-
throw new CompilerValidationError(
|
|
119
|
-
`Entrypoint file not found: ${options.entrypoint}`,
|
|
120
|
-
{ code: "ENTRYPOINT_NOT_FOUND" }
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 2. 输出路径校验
|
|
125
|
-
if (!options.outfile) {
|
|
126
|
-
throw new CompilerValidationError("Outfile path must be specified for standalone compilation", {
|
|
127
|
-
code: "INVALID_OUTFILE",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const resolvedOutfile = resolve(options.outfile);
|
|
132
|
-
const outDir = dirname(resolvedOutfile);
|
|
133
|
-
mkdirSync(outDir, { recursive: true });
|
|
134
|
-
|
|
135
|
-
// 3. 目标平台与选项校验
|
|
136
|
-
const normalizedTarget = normalizeAndValidateTarget(options.target);
|
|
137
|
-
const isWindowsTarget =
|
|
138
|
-
normalizedTarget.includes("windows") ||
|
|
139
|
-
(!options.target && process.platform === "win32");
|
|
140
|
-
|
|
141
|
-
// 校验布尔选项
|
|
142
|
-
if (options.minify !== undefined && typeof options.minify !== "boolean") {
|
|
143
|
-
throw new CompilerValidationError("Option 'minify' must be a boolean", {
|
|
144
|
-
code: "INVALID_MINIFY_OPTION",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
if (options.bytecode !== undefined && typeof options.bytecode !== "boolean") {
|
|
148
|
-
throw new CompilerValidationError("Option 'bytecode' must be a boolean", {
|
|
149
|
-
code: "INVALID_BYTECODE_OPTION",
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 4. 组装 Bun build 命令参数
|
|
154
|
-
const args = [
|
|
155
|
-
"bun",
|
|
156
|
-
"build",
|
|
157
|
-
resolvedEntry,
|
|
158
|
-
"--compile",
|
|
159
|
-
"--outfile",
|
|
160
|
-
resolvedOutfile,
|
|
161
|
-
];
|
|
162
|
-
|
|
163
|
-
const bytecode = options.bytecode !== false;
|
|
164
|
-
if (bytecode) {
|
|
165
|
-
args.push("--bytecode");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const minify = options.minify !== false;
|
|
169
|
-
if (minify) {
|
|
170
|
-
args.push("--minify");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (normalizedTarget !== "bun") {
|
|
174
|
-
args.push(`--target=${normalizedTarget}`);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 5. 执行外部编译器进程
|
|
178
|
-
const cwd = options.cwd ? resolve(options.cwd) : dirname(resolvedEntry);
|
|
179
|
-
const proc = spawnSync(args[0], args.slice(1), {
|
|
180
|
-
cwd,
|
|
181
|
-
stdio: "pipe",
|
|
182
|
-
env: {
|
|
183
|
-
...process.env,
|
|
184
|
-
...options.env,
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
if (proc.error) {
|
|
189
|
-
if ((proc.error as any).code === "ENOENT") {
|
|
190
|
-
throw new CompilerError(
|
|
191
|
-
"Bun binary not found in PATH. Standalone compilation requires Bun to be installed (e.g. via 'curl -fsSL https://bun.sh/install | bash' or 'npm install -g bun').",
|
|
192
|
-
{
|
|
193
|
-
code: "BUN_NOT_FOUND",
|
|
194
|
-
exitCode: 1,
|
|
195
|
-
rawError: proc.error.message,
|
|
196
|
-
target: options.target,
|
|
197
|
-
}
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
throw new CompilerError(`Failed to spawn Bun compiler: ${proc.error.message}`, {
|
|
201
|
-
code: "SPAWN_FAILED",
|
|
202
|
-
exitCode: 1,
|
|
203
|
-
rawError: proc.error.message,
|
|
204
|
-
target: options.target,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (proc.status !== 0) {
|
|
209
|
-
const rawStderr = proc.stderr?.toString("utf-8") || "";
|
|
210
|
-
const rawStdout = proc.stdout?.toString("utf-8") || "";
|
|
211
|
-
throw normalizeCompilerError(rawStderr, rawStdout, proc.status ?? 1, options.target);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 6. 解析生成的可执行二进制物理路径(适配 Windows .exe 后缀特性)
|
|
215
|
-
let actualExecutablePath = resolvedOutfile;
|
|
216
|
-
if (!existsSync(actualExecutablePath) && isWindowsTarget) {
|
|
217
|
-
const withExe = `${resolvedOutfile}.exe`;
|
|
218
|
-
if (existsSync(withExe)) {
|
|
219
|
-
actualExecutablePath = withExe;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (!existsSync(actualExecutablePath)) {
|
|
224
|
-
throw new CompilerError(
|
|
225
|
-
`Compiler completed with exit code 0 but output executable not found at ${resolvedOutfile}`,
|
|
226
|
-
{
|
|
227
|
-
code: "OUTPUT_NOT_FOUND",
|
|
228
|
-
exitCode: 0,
|
|
229
|
-
rawError: "Output executable file not found on disk",
|
|
230
|
-
target: options.target,
|
|
231
|
-
}
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// 7. 计算二进制大小与校验和
|
|
236
|
-
const binaryBuffer = readFileSync(actualExecutablePath);
|
|
237
|
-
const sizeBytes = statSync(actualExecutablePath).size;
|
|
238
|
-
const sha256 = createHash("sha256").update(binaryBuffer).digest("hex");
|
|
239
|
-
const durationMs = Date.now() - startTime;
|
|
240
|
-
const compiledAt = new Date().toISOString();
|
|
241
|
-
|
|
242
|
-
// 8. 写入 metadata 产物文件
|
|
243
|
-
let metadataPath: string | undefined;
|
|
244
|
-
if (options.emitMetadata !== false) {
|
|
245
|
-
metadataPath = resolve(outDir, "artifact.json");
|
|
246
|
-
const bunVersion =
|
|
247
|
-
(typeof (globalThis as any).Bun !== "undefined" && (globalThis as any).Bun.version) ||
|
|
248
|
-
(() => {
|
|
249
|
-
try {
|
|
250
|
-
const vProc = spawnSync("bun", ["--version"], { stdio: "pipe" });
|
|
251
|
-
if (vProc.status === 0) {
|
|
252
|
-
return vProc.stdout.toString().trim();
|
|
253
|
-
}
|
|
254
|
-
} catch {}
|
|
255
|
-
return "unknown";
|
|
256
|
-
})();
|
|
257
|
-
|
|
258
|
-
const metadata = {
|
|
259
|
-
packageId: options.packageId,
|
|
260
|
-
version: options.version,
|
|
261
|
-
target: options.target || "host",
|
|
262
|
-
executable: basename(actualExecutablePath),
|
|
263
|
-
sizeBytes,
|
|
264
|
-
sha256,
|
|
265
|
-
bunVersion,
|
|
266
|
-
minify,
|
|
267
|
-
bytecode,
|
|
268
|
-
actions: options.actions || [],
|
|
269
|
-
compiledAt,
|
|
270
|
-
};
|
|
271
|
-
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return {
|
|
275
|
-
packageId: options.packageId,
|
|
276
|
-
version: options.version,
|
|
277
|
-
target: options.target || "host",
|
|
278
|
-
executablePath: actualExecutablePath,
|
|
279
|
-
sizeBytes,
|
|
280
|
-
sha256,
|
|
281
|
-
metadataPath,
|
|
282
|
-
minify,
|
|
283
|
-
bytecode,
|
|
284
|
-
durationMs,
|
|
285
|
-
compiledAt,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* 静态辅助调用方法。
|
|
291
|
-
*/
|
|
292
|
-
public static async compile(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
293
|
-
const compiler = new BunCompiler();
|
|
294
|
-
return compiler.compile(options);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* 快捷独立二进制编译函数。
|
|
300
|
-
*/
|
|
301
|
-
export async function compileBinary(options: BunCompilerOptions): Promise<BunCompilerResult> {
|
|
302
|
-
return BunCompiler.compile(options);
|
|
303
|
-
}
|