@napuketto/loader 0.0.13 → 0.0.15
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/assets/7zip/7zz +0 -0
- package/assets/7zip/License-linux.txt +144 -0
- package/dist/host/self-host.cjs +172 -25
- package/dist/index.cjs +229 -52
- package/dist/index.d.cts +350 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +350 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +219 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -1
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import process$1 from "node:process";
|
|
@@ -6,6 +7,171 @@ import { fileURLToPath } from "node:url";
|
|
|
6
7
|
import { chmod, copyFile, mkdir, readdir, rm } from "node:fs/promises";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
9
|
import { get } from "node:https";
|
|
10
|
+
//#region src/host/ipc/ipc-types.ts
|
|
11
|
+
/**
|
|
12
|
+
* ipc-types.ts:IPC 协议消息(koishi 插件 ↔ self-host 子进程,单一来源)。
|
|
13
|
+
*
|
|
14
|
+
* 协议契约(koishi 插件 design.md §5.6):`{ v, type, id?, payload }` JSON 行。
|
|
15
|
+
* 用 zod v4 定义单一 schema,两侧(loader host + koishi 插件)import 同一份:
|
|
16
|
+
* 类型经 z.infer 派生、运行时经 safeParse 校验,消除「两份手写 types.ts」漂移。
|
|
17
|
+
*
|
|
18
|
+
* 领域字段(LoginState / SelfInfo / QrCodeData)与 kernel 同名类型结构等价,
|
|
19
|
+
* 在协议边界内联为 zod schema(kernel 不依赖 zod,故不在 kernel 定义)。
|
|
20
|
+
*/
|
|
21
|
+
/** IPC 协议版本(解码校验)。 */
|
|
22
|
+
const IPC_VERSION = 1;
|
|
23
|
+
/** 引导阶段(status 消息)。 */
|
|
24
|
+
const IpcBootPhaseSchema = z.enum([
|
|
25
|
+
"booting",
|
|
26
|
+
"dlopening",
|
|
27
|
+
"logging",
|
|
28
|
+
"sessioning",
|
|
29
|
+
"ready",
|
|
30
|
+
"failed"
|
|
31
|
+
]);
|
|
32
|
+
/** 结构化日志级别(pino level)。 */
|
|
33
|
+
const IpcLogLevelSchema = z.enum([
|
|
34
|
+
"trace",
|
|
35
|
+
"debug",
|
|
36
|
+
"info",
|
|
37
|
+
"warn",
|
|
38
|
+
"error",
|
|
39
|
+
"fatal"
|
|
40
|
+
]);
|
|
41
|
+
/** 登录状态(与 kernel LoginState 结构等价)。 */
|
|
42
|
+
const LoginStateSchema = z.enum([
|
|
43
|
+
"idle",
|
|
44
|
+
"waiting_scan",
|
|
45
|
+
"scanned",
|
|
46
|
+
"logged_in",
|
|
47
|
+
"failed"
|
|
48
|
+
]);
|
|
49
|
+
/** selfInfo(与 kernel SelfInfo 结构等价)。 */
|
|
50
|
+
const SelfInfoSchema = z.object({
|
|
51
|
+
uin: z.string(),
|
|
52
|
+
uid: z.string(),
|
|
53
|
+
nick: z.string()
|
|
54
|
+
});
|
|
55
|
+
/** status 消息 payload。 */
|
|
56
|
+
const IpcStatusPayloadSchema = z.object({
|
|
57
|
+
phase: IpcBootPhaseSchema,
|
|
58
|
+
message: z.string().optional(),
|
|
59
|
+
error: z.object({
|
|
60
|
+
code: z.string(),
|
|
61
|
+
message: z.string()
|
|
62
|
+
}).optional()
|
|
63
|
+
});
|
|
64
|
+
/** login 消息 payload。 */
|
|
65
|
+
const IpcLoginPayloadSchema = z.object({
|
|
66
|
+
state: LoginStateSchema,
|
|
67
|
+
selfInfo: SelfInfoSchema.optional(),
|
|
68
|
+
/** 失败原因(state=failed 时;如「登录超时,请刷新页面重试」)。 */
|
|
69
|
+
message: z.string().optional()
|
|
70
|
+
});
|
|
71
|
+
/** event 消息 payload(kernel 事件通道形状,翻译层按 service/name 断言具体类型)。 */
|
|
72
|
+
const IpcEventPayloadSchema = z.object({
|
|
73
|
+
service: z.string(),
|
|
74
|
+
name: z.string(),
|
|
75
|
+
args: z.array(z.unknown())
|
|
76
|
+
});
|
|
77
|
+
/** log 消息 payload。 */
|
|
78
|
+
const IpcLogPayloadSchema = z.object({
|
|
79
|
+
level: IpcLogLevelSchema,
|
|
80
|
+
message: z.string()
|
|
81
|
+
});
|
|
82
|
+
/** 动作响应 payload(result 消息)。 */
|
|
83
|
+
const IpcResultPayloadSchema = z.union([z.object({
|
|
84
|
+
ok: z.literal(true),
|
|
85
|
+
value: z.unknown().optional()
|
|
86
|
+
}), z.object({
|
|
87
|
+
ok: z.literal(false),
|
|
88
|
+
error: z.object({
|
|
89
|
+
code: z.string(),
|
|
90
|
+
message: z.string()
|
|
91
|
+
})
|
|
92
|
+
})]);
|
|
93
|
+
/** 动作请求 payload(action 消息)。 */
|
|
94
|
+
const IpcActionPayloadSchema = z.object({
|
|
95
|
+
action: z.string(),
|
|
96
|
+
params: z.record(z.string(), z.unknown()).optional()
|
|
97
|
+
});
|
|
98
|
+
/** 控制指令 payload(control 消息,父→子)。 */
|
|
99
|
+
const IpcControlPayloadSchema = z.union([
|
|
100
|
+
z.object({ command: z.literal("stop") }),
|
|
101
|
+
z.object({ command: z.literal("restart") }),
|
|
102
|
+
z.object({
|
|
103
|
+
command: z.literal("login"),
|
|
104
|
+
uin: z.string().optional(),
|
|
105
|
+
qr: z.boolean().optional()
|
|
106
|
+
})
|
|
107
|
+
]);
|
|
108
|
+
const IpcStatusMessageSchema = z.object({
|
|
109
|
+
v: z.literal(1),
|
|
110
|
+
type: z.literal("status"),
|
|
111
|
+
payload: IpcStatusPayloadSchema
|
|
112
|
+
});
|
|
113
|
+
const IpcLoginMessageSchema = z.object({
|
|
114
|
+
v: z.literal(1),
|
|
115
|
+
type: z.literal("login"),
|
|
116
|
+
payload: IpcLoginPayloadSchema
|
|
117
|
+
});
|
|
118
|
+
const IpcQrMessageSchema = z.object({
|
|
119
|
+
v: z.literal(1),
|
|
120
|
+
type: z.literal("qr"),
|
|
121
|
+
payload: z.object({
|
|
122
|
+
pngBase64: z.string(),
|
|
123
|
+
qrcodeUrl: z.string()
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
const IpcEventMessageSchema = z.object({
|
|
127
|
+
v: z.literal(1),
|
|
128
|
+
type: z.literal("event"),
|
|
129
|
+
payload: IpcEventPayloadSchema
|
|
130
|
+
});
|
|
131
|
+
const IpcResultMessageSchema = z.object({
|
|
132
|
+
v: z.literal(1),
|
|
133
|
+
type: z.literal("result"),
|
|
134
|
+
id: z.number(),
|
|
135
|
+
payload: IpcResultPayloadSchema
|
|
136
|
+
});
|
|
137
|
+
const IpcLogMessageSchema = z.object({
|
|
138
|
+
v: z.literal(1),
|
|
139
|
+
type: z.literal("log"),
|
|
140
|
+
payload: IpcLogPayloadSchema
|
|
141
|
+
});
|
|
142
|
+
const IpcPingMessageSchema = z.object({
|
|
143
|
+
v: z.literal(1),
|
|
144
|
+
type: z.literal("ping")
|
|
145
|
+
});
|
|
146
|
+
const IpcPongMessageSchema = z.object({
|
|
147
|
+
v: z.literal(1),
|
|
148
|
+
type: z.literal("pong")
|
|
149
|
+
});
|
|
150
|
+
const IpcActionMessageSchema = z.object({
|
|
151
|
+
v: z.literal(1),
|
|
152
|
+
type: z.literal("action"),
|
|
153
|
+
id: z.number(),
|
|
154
|
+
payload: IpcActionPayloadSchema
|
|
155
|
+
});
|
|
156
|
+
const IpcControlMessageSchema = z.object({
|
|
157
|
+
v: z.literal(1),
|
|
158
|
+
type: z.literal("control"),
|
|
159
|
+
payload: IpcControlPayloadSchema
|
|
160
|
+
});
|
|
161
|
+
/** IPC 消息判别联合(协议边界,运行时经 safeParse 校验)。 */
|
|
162
|
+
const IpcMessageSchema = z.discriminatedUnion("type", [
|
|
163
|
+
IpcStatusMessageSchema,
|
|
164
|
+
IpcLoginMessageSchema,
|
|
165
|
+
IpcQrMessageSchema,
|
|
166
|
+
IpcEventMessageSchema,
|
|
167
|
+
IpcResultMessageSchema,
|
|
168
|
+
IpcLogMessageSchema,
|
|
169
|
+
IpcPingMessageSchema,
|
|
170
|
+
IpcPongMessageSchema,
|
|
171
|
+
IpcActionMessageSchema,
|
|
172
|
+
IpcControlMessageSchema
|
|
173
|
+
]);
|
|
174
|
+
//#endregion
|
|
9
175
|
//#region src/instance-lock.ts
|
|
10
176
|
/**
|
|
11
177
|
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
@@ -110,6 +276,34 @@ function registerLockCleanup(dataDir) {
|
|
|
110
276
|
});
|
|
111
277
|
}
|
|
112
278
|
//#endregion
|
|
279
|
+
//#region src/data-root.ts
|
|
280
|
+
/**
|
|
281
|
+
* data-root.ts:轻量数据根解析(loader 侧,避免新增 kernel 依赖)。
|
|
282
|
+
*
|
|
283
|
+
* 从 locate-qq.ts / win-node.ts 抽出的同构逻辑(2026-08 消重:两处
|
|
284
|
+
* 22 行重复 resolveDataRoot + findProjectRoot)。
|
|
285
|
+
*
|
|
286
|
+
* 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
|
|
287
|
+
* P2 若需完整语义(同 kernel resolveDataRoot)再切换。
|
|
288
|
+
*/
|
|
289
|
+
/** 解析数据根(NAPKETTO_DATA 显式 > 项目根/.napuketto > cwd 兜底)。 */
|
|
290
|
+
function resolveDataRoot(dataRoot) {
|
|
291
|
+
const explicit = dataRoot ?? process$1.env["NAPKETTO_DATA"];
|
|
292
|
+
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
293
|
+
const projectRoot = findProjectRoot(process$1.cwd()) ?? process$1.cwd();
|
|
294
|
+
return join(projectRoot, ".napuketto");
|
|
295
|
+
}
|
|
296
|
+
/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
|
|
297
|
+
function findProjectRoot(start) {
|
|
298
|
+
let dir = resolve(start);
|
|
299
|
+
for (;;) {
|
|
300
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "package.json"))) return dir;
|
|
301
|
+
const parent = dirname(dir);
|
|
302
|
+
if (parent === dir) return null;
|
|
303
|
+
dir = parent;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
113
307
|
//#region src/qq-download.ts
|
|
114
308
|
/**
|
|
115
309
|
* qq-download.ts:QQ 官方安装包下载器(P1)。
|
|
@@ -233,8 +427,8 @@ async function downloadFile(options) {
|
|
|
233
427
|
const EXTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".node", ".dll"]);
|
|
234
428
|
/**
|
|
235
429
|
* 探测 7z 可执行文件:
|
|
236
|
-
* NAPUTO_7Z_PATH 环境变量 > 内置资产(assets/7zip/7z.exe
|
|
237
|
-
*
|
|
430
|
+
* NAPUTO_7Z_PATH 环境变量 > 内置资产(Windows assets/7zip/7z.exe、Linux
|
|
431
|
+
* assets/7zip/7zz——2026-08-14 治本,静态二进制已打进发布包)> 系统 PATH 7z。
|
|
238
432
|
*/
|
|
239
433
|
function findSevenZip() {
|
|
240
434
|
const envPath = process$1.env["NAPUTO_7Z_PATH"];
|
|
@@ -242,8 +436,9 @@ function findSevenZip() {
|
|
|
242
436
|
exe: envPath,
|
|
243
437
|
source: "环境变量 NAPUTO_7Z_PATH"
|
|
244
438
|
};
|
|
245
|
-
if (process$1.platform === "win32") {
|
|
246
|
-
const
|
|
439
|
+
if (process$1.platform === "win32" || process$1.platform === "linux") {
|
|
440
|
+
const exeName = process$1.platform === "win32" ? "7z.exe" : "7zz";
|
|
441
|
+
const bundled = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "7zip", exeName);
|
|
247
442
|
if (existsSync(bundled)) return {
|
|
248
443
|
exe: bundled,
|
|
249
444
|
source: "内置资产 assets/7zip"
|
|
@@ -386,23 +581,6 @@ async function unzipNode(zipPath, cacheDir) {
|
|
|
386
581
|
}
|
|
387
582
|
/** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */
|
|
388
583
|
const DEFAULT_WIN_NODE_VERSION = "v24.16.0";
|
|
389
|
-
/** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */
|
|
390
|
-
function resolveDataRoot(dataRoot) {
|
|
391
|
-
const explicit = dataRoot ?? process$1.env["NAPKETTO_DATA"];
|
|
392
|
-
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
393
|
-
const projectRoot = findProjectRoot$1(process$1.cwd()) ?? process$1.cwd();
|
|
394
|
-
return join(projectRoot, ".napuketto");
|
|
395
|
-
}
|
|
396
|
-
/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
|
|
397
|
-
function findProjectRoot$1(start) {
|
|
398
|
-
let dir = resolve(start);
|
|
399
|
-
for (;;) {
|
|
400
|
-
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "package.json"))) return dir;
|
|
401
|
-
const parent = dirname(dir);
|
|
402
|
-
if (parent === dir) return null;
|
|
403
|
-
dir = parent;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
584
|
//#endregion
|
|
407
585
|
//#region src/wine.ts
|
|
408
586
|
/**
|
|
@@ -656,8 +834,8 @@ function resolveDownloadUrl(release) {
|
|
|
656
834
|
*/
|
|
657
835
|
/** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */
|
|
658
836
|
const QQ_FILES_DIR_NAME = "qq-files";
|
|
659
|
-
/** 7-Zip 官方 Linux 版版本号(tar.xz
|
|
660
|
-
const SEVEN_ZIP_LINUX_VERSION = "
|
|
837
|
+
/** 7-Zip 官方 Linux 版版本号(tar.xz 文件名组成部分;兜底下载用,内置资产优先)。 */
|
|
838
|
+
const SEVEN_ZIP_LINUX_VERSION = "2501";
|
|
661
839
|
/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
|
|
662
840
|
const REG_QUERY_RE = /"([^"]+)"/;
|
|
663
841
|
/** Windows 盘符路径正则(`C:/...` / `C:\...`,wslMappedPath 映射用,模块级常量)。 */
|
|
@@ -751,7 +929,7 @@ async function resolveQqFiles(options = {}) {
|
|
|
751
929
|
try {
|
|
752
930
|
return resolveQqInstall();
|
|
753
931
|
} catch {}
|
|
754
|
-
const dataRoot =
|
|
932
|
+
const dataRoot = resolveDataRoot(options.dataRoot);
|
|
755
933
|
const cached = tryResolveCached(dataRoot);
|
|
756
934
|
if (cached !== null) return cached;
|
|
757
935
|
if (options.autoDownload !== false) return ensureQqFiles({
|
|
@@ -778,17 +956,25 @@ function linuxSevenZipUrl() {
|
|
|
778
956
|
return process$1.env["NAPUTO_7Z_URL"] ?? `https://www.7-zip.org/a/7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`;
|
|
779
957
|
}
|
|
780
958
|
/**
|
|
781
|
-
* 确保 Linux 7zz
|
|
959
|
+
* 确保 Linux 7zz 就绪(治本:内置资产优先,下载兜底)。
|
|
782
960
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* 7zz
|
|
786
|
-
* 免 root 免系统包,下载到 <数据根>/runtime/7zip/7zz(与 win-node 同模式)。
|
|
961
|
+
* 优先级:内置资产 assets/7zip/7zz(2026-08-14 治本——与 Windows 7z.exe 同模式,
|
|
962
|
+
* 7zz 静态二进制已打进发布包,LGPL 合规可分发,零运行时下载)> 数据根缓存
|
|
963
|
+
* runtime/7zip/7zz(历史下载产物复用)> 官网下载(兜底,NAPUTO_7Z_URL 可覆盖)。
|
|
787
964
|
*
|
|
788
|
-
*
|
|
965
|
+
* 背景:7-Zip 官网只保留最新版 tar.xz,硬编码旧版本号(如 2409)会被删档 404,
|
|
966
|
+
* 运行时下载既脆弱又依赖外网。内置资产彻底消灭该依赖。
|
|
789
967
|
*/
|
|
790
968
|
async function ensureLinuxSevenZip(options = {}) {
|
|
791
|
-
const
|
|
969
|
+
const bundled = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "7zip", "7zz");
|
|
970
|
+
if (existsSync(bundled)) {
|
|
971
|
+
await chmod(bundled, 493).catch(() => {});
|
|
972
|
+
return {
|
|
973
|
+
exe: bundled,
|
|
974
|
+
source: "内置资产 assets/7zip"
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const dataRoot = resolveDataRoot(options.dataRoot);
|
|
792
978
|
const cacheDir = join(dataRoot, "runtime", "7zip");
|
|
793
979
|
const exe = join(cacheDir, "7zz");
|
|
794
980
|
if (existsSync(exe)) return {
|
|
@@ -828,7 +1014,7 @@ async function ensureLinuxSevenZip(options = {}) {
|
|
|
828
1014
|
* → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。
|
|
829
1015
|
*/
|
|
830
1016
|
async function ensureQqFiles(options = {}) {
|
|
831
|
-
const dataRoot =
|
|
1017
|
+
const dataRoot = resolveDataRoot(options.dataRoot);
|
|
832
1018
|
const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);
|
|
833
1019
|
mkdirSync(cacheRoot, { recursive: true });
|
|
834
1020
|
const existing = tryResolveCached(dataRoot);
|
|
@@ -861,27 +1047,7 @@ async function ensureQqFiles(options = {}) {
|
|
|
861
1047
|
});
|
|
862
1048
|
}
|
|
863
1049
|
}
|
|
864
|
-
/**
|
|
865
|
-
* 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。
|
|
866
|
-
* 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
|
|
867
|
-
*/
|
|
868
|
-
function resolveDataRootLight(dataRoot) {
|
|
869
|
-
const explicit = dataRoot ?? process$1.env["NAPKETTO_DATA"];
|
|
870
|
-
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
871
|
-
const projectRoot = findProjectRoot(process$1.cwd()) ?? process$1.cwd();
|
|
872
|
-
return join(projectRoot, ".napuketto");
|
|
873
|
-
}
|
|
874
|
-
/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
|
|
875
|
-
function findProjectRoot(start) {
|
|
876
|
-
let dir = resolve(start);
|
|
877
|
-
for (;;) {
|
|
878
|
-
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "package.json"))) return dir;
|
|
879
|
-
const parent = dirname(dir);
|
|
880
|
-
if (parent === dir) return null;
|
|
881
|
-
dir = parent;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
1050
|
//#endregion
|
|
885
|
-
export { DownloadError, ENV, INSTANCE_LOCK_FILE, QQ_FILES_DIR_NAME, QqReleasesError, WIN_NODE_DIR_NAME, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
1051
|
+
export { DownloadError, ENV, INSTANCE_LOCK_FILE, IPC_VERSION, IpcActionPayloadSchema, IpcBootPhaseSchema, IpcControlPayloadSchema, IpcEventPayloadSchema, IpcLogLevelSchema, IpcLogPayloadSchema, IpcLoginPayloadSchema, IpcMessageSchema, IpcResultPayloadSchema, IpcStatusPayloadSchema, QQ_FILES_DIR_NAME, QqReleasesError, WIN_NODE_DIR_NAME, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
886
1052
|
|
|
887
1053
|
//# sourceMappingURL=index.mjs.map
|