create-koishi-ce 1.3.0 → 1.3.1
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/lib/bin.mjs +1 -1
- package/lib/index.d.ts +24 -16
- package/lib/index.mjs +1 -1
- package/lib/{src-DiZgIQu5.mjs → src-Dh_67mkm.mjs} +244 -170
- package/package.json +58 -63
- package/src/__tests__/run-default.test.ts +7 -9
- package/src/__tests__/run-remote.test.ts +48 -32
- package/src/__tests__/tar-pack.ts +188 -0
- package/src/index.ts +77 -243
- package/src/manifest.ts +45 -0
- package/src/registry.ts +52 -0
- package/src/remote.ts +120 -0
- package/src/template.ts +2 -2
- package/src/utils.ts +46 -0
package/package.json
CHANGED
|
@@ -1,64 +1,59 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"default": "./lib/index.mjs"
|
|
61
|
-
},
|
|
62
|
-
"./package.json": "./package.json"
|
|
63
|
-
}
|
|
64
|
-
}
|
|
2
|
+
"name": "create-koishi-ce",
|
|
3
|
+
"description": "Setup a Koishi application",
|
|
4
|
+
"version": "1.3.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.mjs",
|
|
7
|
+
"module": "./lib/index.mjs",
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-koishi-ce": "./lib/bin.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"contributors": [
|
|
17
|
+
"Shigma <shigma10826@gmail.com>",
|
|
18
|
+
"Oppenheymu <oppenheymu@gmail.com>"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Koishi-CE/koishi.git",
|
|
24
|
+
"directory": "apps/koishi-create"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Koishi-CE/koishi/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://koishi.chat",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsdown"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"bot",
|
|
35
|
+
"chatbot",
|
|
36
|
+
"koishi",
|
|
37
|
+
"discord",
|
|
38
|
+
"telegram",
|
|
39
|
+
"create",
|
|
40
|
+
"scaffold",
|
|
41
|
+
"template",
|
|
42
|
+
"generator",
|
|
43
|
+
"boilerplate"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@clack/prompts": "^1.7.0",
|
|
47
|
+
"picocolors": "^1.1.1",
|
|
48
|
+
"giget": "^3.3.1"
|
|
49
|
+
},
|
|
50
|
+
"exports": {
|
|
51
|
+
".": {
|
|
52
|
+
"source": "./src/index.ts",
|
|
53
|
+
"types": "./lib/index.d.ts",
|
|
54
|
+
"import": "./lib/index.mjs",
|
|
55
|
+
"default": "./lib/index.mjs"
|
|
56
|
+
},
|
|
57
|
+
"./package.json": "./package.json"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -19,7 +19,7 @@ import { basename, join } from "node:path";
|
|
|
19
19
|
* start() 默认(内置模板)主流程的端到端测试。
|
|
20
20
|
*
|
|
21
21
|
* 手段:把 process.argv 指为无位置参数 + --git,chdir 到临时目录后动态
|
|
22
|
-
* 导入被测模块(其内部 cwd / argv
|
|
22
|
+
* 导入被测模块(其内部 cwd / argv 在模块顶层固化);@clack/prompts 与
|
|
23
23
|
* node:child_process 均 mock,分别提供可编程的问询答案与子进程记录,
|
|
24
24
|
* process.exit 替换为可捕获的异常,从而覆盖全部交互分支。
|
|
25
25
|
*/
|
|
@@ -34,17 +34,15 @@ const hadUserAgent = userAgentKey in process.env;
|
|
|
34
34
|
const previousUserAgent = process.env[userAgentKey];
|
|
35
35
|
delete process.env[userAgentKey];
|
|
36
36
|
|
|
37
|
-
/** prompts 的可编程答案队列(按
|
|
37
|
+
/** @clack/prompts 的可编程答案队列(按 prompt 类型分发) */
|
|
38
38
|
const nameAnswers: string[] = [];
|
|
39
39
|
const confirmAnswers: boolean[] = [];
|
|
40
40
|
|
|
41
|
-
mock.module("prompts", () => ({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return { name: nameAnswers.shift() ?? "" };
|
|
47
|
-
},
|
|
41
|
+
mock.module("@clack/prompts", () => ({
|
|
42
|
+
text: async () => nameAnswers.shift() ?? "",
|
|
43
|
+
confirm: async () => confirmAnswers.shift() ?? false,
|
|
44
|
+
isCancel: (value: unknown) => typeof value === "symbol",
|
|
45
|
+
cancel: () => {},
|
|
48
46
|
}));
|
|
49
47
|
|
|
50
48
|
/** spawnSync 的调用记录与可编程行为 */
|
|
@@ -13,19 +13,22 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import { tarPack } from "./tar-pack.ts";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* start() 远程模板分支(--template)的端到端测试。
|
|
20
20
|
*
|
|
21
21
|
* 以 Bun.serve 起本地伪 registry(127.0.0.1 随机端口),按用例切换
|
|
22
22
|
* 场景(正常 / 元数据 404 / dist-tag 缺失 / tarball 404 / tarball 损坏);
|
|
23
|
-
* tarball 由 tar
|
|
23
|
+
* tarball 由 __tests__/tar-pack.ts 的 tarPack 打包(ustar + gzip,与真实
|
|
24
|
+
* npm tarball 同构),scaffoldRemote 的下载-解包-改写全链路真实执行
|
|
25
|
+
* (解包走 giget,含超长名 pax 条目的端到端验证)。
|
|
24
26
|
*
|
|
25
27
|
* 覆盖率口径说明:bun 的覆盖率对同一路径的多个 query 实例只统计最后
|
|
26
28
|
* 求值的那份,本文件是 koishi-create 全部测试中最后加载的实例,因此
|
|
27
29
|
* 在此补齐 registry 探测纯函数、emptyDir 与 initGit(--git)等只在其它
|
|
28
|
-
* 实例出现过的分支,保证
|
|
30
|
+
* 实例出现过的分支,保证 index.ts 及其子模块(manifest / registry /
|
|
31
|
+
* utils / remote)的行覆盖完整。
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
34
|
const workspaceRoot = mkdtempSync(join(tmpdir(), "ckc-run-remote-"));
|
|
@@ -38,7 +41,10 @@ let scenario: "ok" | "meta-404" | "missing-ref" | "tarball-404" | "corrupt" =
|
|
|
38
41
|
"ok";
|
|
39
42
|
|
|
40
43
|
/** 预先打包好的模板 tarball(package/ 前缀目录,strip:1 解包) */
|
|
41
|
-
let tarball:
|
|
44
|
+
let tarball: Uint8Array;
|
|
45
|
+
|
|
46
|
+
/** 超长文件名(>100 字节,触发 pax 扩展头),归档与落盘共用同一串 */
|
|
47
|
+
const longName = "deep".repeat(30);
|
|
42
48
|
|
|
43
49
|
// fetch 回调内引用 registry.port,显式标注返回类型以切断 initializer 的循环推断
|
|
44
50
|
const registry = Bun.serve({
|
|
@@ -81,16 +87,14 @@ const registry = Bun.serve({
|
|
|
81
87
|
},
|
|
82
88
|
});
|
|
83
89
|
|
|
84
|
-
/** prompts mock:install 的 confirm 默认拒绝,可按用例入队覆盖 */
|
|
90
|
+
/** @clack/prompts mock:install 的 confirm 默认拒绝,可按用例入队覆盖 */
|
|
85
91
|
const confirmAnswers: boolean[] = [];
|
|
86
92
|
|
|
87
|
-
mock.module("prompts", () => ({
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return { name: "" };
|
|
93
|
-
},
|
|
93
|
+
mock.module("@clack/prompts", () => ({
|
|
94
|
+
text: async () => "",
|
|
95
|
+
confirm: async () => confirmAnswers.shift() ?? false,
|
|
96
|
+
isCancel: (value: unknown) => typeof value === "symbol",
|
|
97
|
+
cancel: () => {},
|
|
94
98
|
}));
|
|
95
99
|
|
|
96
100
|
// erasableSyntaxOnly 禁用参数属性,改为显式字段声明
|
|
@@ -135,26 +139,32 @@ beforeAll(async () => {
|
|
|
135
139
|
console.log = (...args: unknown[]) => {
|
|
136
140
|
logs.push(args.map((arg) => `${arg}`).join(" "));
|
|
137
141
|
};
|
|
138
|
-
// 造模板载荷并打包:package/package.json + package/index.js
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
// 造模板载荷并打包:package/package.json + package/index.js + 超长名文件
|
|
143
|
+
const encoder = new TextEncoder();
|
|
144
|
+
tarball = await tarPack([
|
|
145
|
+
{
|
|
146
|
+
path: "package/package.json",
|
|
147
|
+
data: encoder.encode(
|
|
148
|
+
JSON.stringify({
|
|
149
|
+
name: "fake-tpl",
|
|
150
|
+
version: "2.0.0",
|
|
151
|
+
private: false,
|
|
152
|
+
workspaces: ["src/*"],
|
|
153
|
+
scripts: { start: "node index.js" },
|
|
154
|
+
}),
|
|
155
|
+
),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
path: "package/index.js",
|
|
159
|
+
data: encoder.encode("console.log('tpl');\n"),
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
// 124 字符 > ustar 的 100 字节 name 上限:tarPack 前置 pax
|
|
163
|
+
// 扩展头,giget(tar 库)经 pax 还原后同样能正确落盘
|
|
164
|
+
path: `package/${longName}.txt`,
|
|
165
|
+
data: encoder.encode("pax"),
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
158
168
|
});
|
|
159
169
|
|
|
160
170
|
afterAll(() => {
|
|
@@ -170,6 +180,10 @@ function reset(sc: typeof scenario): void {
|
|
|
170
180
|
scenario = sc;
|
|
171
181
|
logs.length = 0;
|
|
172
182
|
rmSync(join(workspaceRoot, "myapp"), { recursive: true, force: true });
|
|
183
|
+
// giget 把 tarball 缓存到系统临时目录(win32 为 tmpdir()/giget,不受
|
|
184
|
+
// XDG_CACHE_HOME 影响);逐用例清理,避免上一场景的缓存(tarball-404
|
|
185
|
+
// 的缺失或 corrupt 的损坏归档)被 giget 的「下载失败回退缓存」吞掉
|
|
186
|
+
rmSync(join(tmpdir(), "giget"), { recursive: true, force: true });
|
|
173
187
|
}
|
|
174
188
|
|
|
175
189
|
describe("create-koishi-ce 远程模板", () => {
|
|
@@ -178,6 +192,8 @@ describe("create-koishi-ce 远程模板", () => {
|
|
|
178
192
|
await start();
|
|
179
193
|
const dir = join(workspaceRoot, "myapp");
|
|
180
194
|
expect(existsSync(join(dir, "index.js"))).toBe(true);
|
|
195
|
+
// 超长名文件经 pax 扩展头正常落盘(tarPack 打包 → giget 解包全链路)
|
|
196
|
+
expect(readFileSync(join(dir, `${longName}.txt`), "utf8")).toBe("pax");
|
|
181
197
|
const manifest = JSON.parse(
|
|
182
198
|
readFileSync(join(dir, "package.json"), "utf8"),
|
|
183
199
|
) as Record<string, unknown>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 测试专用 .tgz fixture 构造器(ustar + 超长名 pax 兜底 + gzip)。
|
|
6
|
+
*
|
|
7
|
+
* 生产代码的远程模板解包已改走 giget(见 src/index.ts scaffoldRemote),
|
|
8
|
+
* 这里只保留「打包」一侧供测试构造伪 registry 的 tarball 响应。tar
|
|
9
|
+
* 归档格式稳定,此工具不随业务演进,故不再单独维护解析逻辑与单测
|
|
10
|
+
* ——其正确性由 __tests__/run-remote.test.ts 的「打包 → giget 解包」
|
|
11
|
+
* 端到端链路间接验证。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { gzipSync } from "node:zlib";
|
|
15
|
+
|
|
16
|
+
const BLOCK = 512;
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
|
|
19
|
+
/** 单条目(供 tarPack 构造归档) */
|
|
20
|
+
export interface TarEntry {
|
|
21
|
+
/** 归档内路径(相对归档根;目录以 / 结尾) */
|
|
22
|
+
path: string;
|
|
23
|
+
/** 条目类型(默认 file) */
|
|
24
|
+
type?: "file" | "dir" | "symlink" | "hardlink";
|
|
25
|
+
/** 文件内容(file 类型) */
|
|
26
|
+
data?: Uint8Array;
|
|
27
|
+
/** 链接目标(symlink / hardlink 类型,归档内相对路径) */
|
|
28
|
+
link?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 构造单条 pax 记录(len 前缀自洽的数字迭代) */
|
|
32
|
+
function paxRecord(key: string, value: string): string {
|
|
33
|
+
const body = `${key}=${value}\n`;
|
|
34
|
+
let length = body.length + 2;
|
|
35
|
+
for (;;) {
|
|
36
|
+
const prefix = `${length} `;
|
|
37
|
+
if (prefix.length + body.length === length) return `${prefix}${body}`;
|
|
38
|
+
length = prefix.length + body.length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 收集条目父目录(含显式目录自身),保证目录条目先于引用内容出现 */
|
|
43
|
+
function collectDirs(entries: readonly TarEntry[]): string[] {
|
|
44
|
+
const dirs = new Set<string>();
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.type === "dir" || entry.path.endsWith("/")) {
|
|
47
|
+
dirs.add(entry.path.replace(/\/+$/, ""));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let parent = entry.path.slice(0, entry.path.lastIndexOf("/"));
|
|
51
|
+
while (parent) {
|
|
52
|
+
dirs.add(parent);
|
|
53
|
+
const index = parent.lastIndexOf("/");
|
|
54
|
+
parent = index < 0 ? "" : parent.slice(0, index);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...dirs];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 构造一个 512 字节的 ustar 头部 */
|
|
61
|
+
function createHeader(
|
|
62
|
+
name: string,
|
|
63
|
+
type: string,
|
|
64
|
+
size: number,
|
|
65
|
+
link = "",
|
|
66
|
+
): Uint8Array {
|
|
67
|
+
const header = new Uint8Array(BLOCK);
|
|
68
|
+
// name / linkname 由调用方保证经 pax 兜底(此处截断仅作占位)
|
|
69
|
+
header.set(encoder.encode(name), 0);
|
|
70
|
+
header.set(encoder.encode(link), 157);
|
|
71
|
+
writeOctal(header, 100, 8, type === "5" ? 0o755 : 0o644); // mode
|
|
72
|
+
writeOctal(header, 108, 8, 0); // uid
|
|
73
|
+
writeOctal(header, 116, 8, 0); // gid
|
|
74
|
+
writeOctal(header, 124, 12, size);
|
|
75
|
+
writeOctal(header, 136, 12, 0); // mtime
|
|
76
|
+
header[156] = type.charCodeAt(0) ?? 0;
|
|
77
|
+
header.set(encoder.encode("ustar\0"), 257);
|
|
78
|
+
header.set(encoder.encode("00"), 263);
|
|
79
|
+
// 校验和:先以空格占位,再求和回填(八进制 6 位 + \0 + 空格)
|
|
80
|
+
header.fill(0x20, 148, 156);
|
|
81
|
+
const sum = header.reduce((total, byte) => total + byte, 0);
|
|
82
|
+
const checksum = `${sum.toString(8).padStart(6, "0")}\0 `;
|
|
83
|
+
header.set(encoder.encode(checksum), 148);
|
|
84
|
+
return header;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 把八进制数字写入定长字段,采用 tar 标准布局:前导 ASCII '0' 填充 +
|
|
89
|
+
* 数字 + 空格 + '\0' 结尾(对齐 GNU tar / node-tar 的 encSmallNumber)。
|
|
90
|
+
* 数字必须全部落在字段首个 NUL 之前——部分实现按 `\0` 截断解析,若用
|
|
91
|
+
* NUL 做前导填充(旧实现写法)会把字段整体读成 0。
|
|
92
|
+
*/
|
|
93
|
+
function writeOctal(
|
|
94
|
+
block: Uint8Array,
|
|
95
|
+
offset: number,
|
|
96
|
+
length: number,
|
|
97
|
+
value: number,
|
|
98
|
+
): void {
|
|
99
|
+
const digits = value.toString(8);
|
|
100
|
+
if (digits.length === length - 1) {
|
|
101
|
+
// 数字恰好占满(无空格),直接写 NUL 结尾
|
|
102
|
+
block.set(encoder.encode(digits), offset);
|
|
103
|
+
block[offset + length - 1] = 0;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const before = length - digits.length - 2;
|
|
107
|
+
block.fill(0x30, offset, offset + before); // 前导 '0'
|
|
108
|
+
block.set(encoder.encode(digits), offset + before);
|
|
109
|
+
block[offset + before + digits.length] = 0x20; // 数字与 NUL 间留空格
|
|
110
|
+
block[offset + length - 1] = 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 追加内容块并按 512 补齐 */
|
|
114
|
+
function pushPadded(blocks: Uint8Array[], content: Uint8Array): void {
|
|
115
|
+
blocks.push(content);
|
|
116
|
+
const rest = content.length % BLOCK;
|
|
117
|
+
if (rest) blocks.push(new Uint8Array(BLOCK - rest));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 追加一个数据条目:超长名(ustar 上限 100 字节)自动前置 pax 扩展头 */
|
|
121
|
+
function pushEntry(
|
|
122
|
+
blocks: Uint8Array[],
|
|
123
|
+
name: string,
|
|
124
|
+
type: string,
|
|
125
|
+
size: number,
|
|
126
|
+
content: Uint8Array,
|
|
127
|
+
link = "",
|
|
128
|
+
): void {
|
|
129
|
+
const records: string[] = [];
|
|
130
|
+
if (encoder.encode(name).length > 100) {
|
|
131
|
+
records.push(paxRecord("path", name));
|
|
132
|
+
}
|
|
133
|
+
if (link && encoder.encode(link).length > 100) {
|
|
134
|
+
records.push(paxRecord("linkpath", link));
|
|
135
|
+
}
|
|
136
|
+
if (records.length) {
|
|
137
|
+
const body = encoder.encode(records.join(""));
|
|
138
|
+
// pax 扩展头自身也须有非空 name(node-tar 校验「path is
|
|
139
|
+
// required」,空名会把整条 x 头判为无效并导致后续流错位);
|
|
140
|
+
// PaxHeader/ 前缀与 node-tar pack 的命名约定一致
|
|
141
|
+
blocks.push(createHeader(`PaxHeader/${name}`, "x", body.length));
|
|
142
|
+
pushPadded(blocks, body);
|
|
143
|
+
}
|
|
144
|
+
blocks.push(createHeader(name.slice(0, 100), type, size, link));
|
|
145
|
+
if (size) pushPadded(blocks, content);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 把条目表打包成 .tgz 字节(ustar + pax 扩展 + gzip)。
|
|
150
|
+
* 仅供测试构造 fixture;产物与真实 npm tarball 的结构一致。
|
|
151
|
+
*/
|
|
152
|
+
export async function tarPack(
|
|
153
|
+
entries: readonly TarEntry[],
|
|
154
|
+
): Promise<Uint8Array> {
|
|
155
|
+
const blocks: Uint8Array[] = [];
|
|
156
|
+
for (const dir of collectDirs(entries)) {
|
|
157
|
+
blocks.push(createHeader(`${dir}/`, "5", 0));
|
|
158
|
+
}
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (entry.type === "dir" || entry.path.endsWith("/")) continue;
|
|
161
|
+
if (entry.type === "symlink" || entry.type === "hardlink") {
|
|
162
|
+
const type = entry.type === "symlink" ? "2" : "1";
|
|
163
|
+
pushEntry(
|
|
164
|
+
blocks,
|
|
165
|
+
entry.path,
|
|
166
|
+
type,
|
|
167
|
+
0,
|
|
168
|
+
new Uint8Array(0),
|
|
169
|
+
entry.link ?? "",
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
const data = entry.data ?? new Uint8Array(0);
|
|
173
|
+
pushEntry(blocks, entry.path, "0", data.length, data);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
blocks.push(new Uint8Array(BLOCK * 2)); // 归档结束标志(全零块)
|
|
177
|
+
|
|
178
|
+
const total = blocks.reduce((sum, block) => sum + block.length, 0);
|
|
179
|
+
const raw = new Uint8Array(total);
|
|
180
|
+
let cursor = 0;
|
|
181
|
+
for (const block of blocks) {
|
|
182
|
+
raw.set(block, cursor);
|
|
183
|
+
cursor += block.length;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// gzip 压缩为 .tgz(与 npm registry 的 tarball 同构)
|
|
187
|
+
return gzipSync(raw);
|
|
188
|
+
}
|