@easy-electron/preload 1.0.1-alpha.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/dist/index.mjs ADDED
@@ -0,0 +1,1048 @@
1
+ import { ConfigUtil } from '@easy-electron/utils';
2
+ import { Logger } from '@easy-electron/logger';
3
+ import { TsParser } from '@easy-electron/ts-parser';
4
+ import fg from 'fast-glob';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ export { E as EVENT_MODULE_KEY, I as IPC_MODULE_KEY, d as defineEvents, a as defineIpc } from './shared/preload.CkLc48BZ.mjs';
8
+
9
+ const DEFAULT_EASY_PRELOAD_GENERATOR_CONFIG = {
10
+ dir: "./electron",
11
+ source: ["**/*.ipc.ts", "**/ipc/**/*.ts"],
12
+ events: ["**/*.events.ts", "**/events/**/*.ts"],
13
+ ignores: ["**/node_modules/**", "**/dist/**", "**/dist-electron/**"],
14
+ output: "./electron/preload.ts",
15
+ typesOutput: "./types/electron.d.ts",
16
+ globalName: "elec",
17
+ preserveComments: true
18
+ };
19
+
20
+ class IpcFileScanner {
21
+ dir;
22
+ source;
23
+ ignores;
24
+ parser;
25
+ logger = Logger.scope("IpcFileScanner");
26
+ /**
27
+ * @param dir 扫描根目录
28
+ * @param source 文件匹配模式(glob 格式,支持数组)
29
+ * @param ignores 忽略的文件模式
30
+ */
31
+ constructor(dir, source, ignores) {
32
+ this.dir = dir;
33
+ this.source = source;
34
+ this.ignores = ignores;
35
+ this.parser = new TsParser();
36
+ }
37
+ /**
38
+ * 扫描所有匹配的 IPC 定义文件,返回解析结果列表
39
+ */
40
+ scan() {
41
+ const patterns = Array.isArray(this.source) ? this.source : [this.source];
42
+ const files = fg.sync(patterns, {
43
+ cwd: this.dir,
44
+ absolute: true,
45
+ ignore: this.ignores
46
+ });
47
+ const results = [];
48
+ for (const file of files) {
49
+ try {
50
+ const defi = this.parseFile(file);
51
+ if (defi) {
52
+ results.push(defi);
53
+ }
54
+ } catch (e) {
55
+ this.logger.warn(`\u89E3\u6790 IPC \u6587\u4EF6\u5931\u8D25: ${file}`, e);
56
+ }
57
+ }
58
+ return results;
59
+ }
60
+ /**
61
+ * 解析单个 IPC 定义文件
62
+ *
63
+ * 查找文件中的 `export default defineIpc(...)` 调用,
64
+ * 提取模块名和所有处理器定义。
65
+ *
66
+ * @param filePath 文件路径
67
+ * @returns IPC 定义信息,文件中无 defineIpc 调用时返回 null
68
+ */
69
+ parseFile(filePath) {
70
+ const parsed = this.parser.parse(filePath);
71
+ const defaultExport = parsed.defaultExport;
72
+ if (!defaultExport || defaultExport.kind !== "call" || !defaultExport.call) {
73
+ return null;
74
+ }
75
+ const call = defaultExport.call;
76
+ if (call.callee !== "defineIpc") {
77
+ return null;
78
+ }
79
+ return this.parseDefineIpcCall(filePath, call, parsed);
80
+ }
81
+ /**
82
+ * 解析 defineIpc 函数调用,提取模块名和处理器映射
83
+ *
84
+ * 支持两种调用形式:
85
+ * - `defineIpc({ ... })` — 无模块名
86
+ * - `defineIpc('moduleName', { ... })` — 有模块名
87
+ *
88
+ * @param filePath 文件路径
89
+ * @param call 函数调用表达式信息
90
+ * @param parsed 完整的文件解析结果(用于提取 import 信息)
91
+ * @returns IPC 定义信息
92
+ */
93
+ parseDefineIpcCall(filePath, call, parsed) {
94
+ const args = call.arguments;
95
+ if (args.length < 1) {
96
+ this.logger.warn(`defineIpc \u8C03\u7528\u53C2\u6570\u4E0D\u8DB3: ${filePath}`);
97
+ return null;
98
+ }
99
+ let moduleName;
100
+ let handlersArg;
101
+ if (args.length >= 2 && args[0].kind === "string") {
102
+ moduleName = String(args[0].value);
103
+ handlersArg = args[1];
104
+ } else {
105
+ handlersArg = args[0];
106
+ }
107
+ if (handlersArg.kind !== "object" || !handlersArg.properties) {
108
+ this.logger.warn(`defineIpc \u5904\u7406\u5668\u53C2\u6570\u4E0D\u662F\u5BF9\u8C61\u5B57\u9762\u91CF: ${filePath}`);
109
+ return null;
110
+ }
111
+ const handlers = /* @__PURE__ */ new Map();
112
+ for (const prop of handlersArg.properties) {
113
+ const handlerInfo = this.parseHandler(prop);
114
+ if (handlerInfo) {
115
+ handlers.set(prop.name, handlerInfo);
116
+ }
117
+ }
118
+ const imports = this.collectImports(parsed, filePath);
119
+ return {
120
+ filePath,
121
+ module: moduleName,
122
+ handlers,
123
+ imports
124
+ };
125
+ }
126
+ /**
127
+ * 解析单个处理器属性
128
+ *
129
+ * 支持三种写法:
130
+ * 1. 箭头函数(默认 handle 类型):
131
+ * `getData: async (_event, id: string): Promise<Data> => { ... }`
132
+ *
133
+ * 2. 方法简写(默认 handle 类型):
134
+ * `getUser() { return ... }`
135
+ *
136
+ * 3. 对象形式(显式指定类型):
137
+ * `logMessage: { type: 'on', handler: (_event, msg: string) => { ... } }`
138
+ *
139
+ * @param prop 对象属性信息
140
+ * @returns IPC 处理器信息,无法解析时返回 null
141
+ */
142
+ parseHandler(prop) {
143
+ const value = prop.value;
144
+ if (value.kind === "function" && value.function) {
145
+ return this.extractHandlerFromFunction(value.function, "handle", prop.comment);
146
+ }
147
+ if (value.kind === "object" && value.properties) {
148
+ return this.parseObjectHandler(value.properties, prop.comment);
149
+ }
150
+ return null;
151
+ }
152
+ /**
153
+ * 从对象形式的处理器定义中提取信息
154
+ *
155
+ * 对象中可包含:
156
+ * - `type`: 处理器类型字符串('handle' | 'on' | 'handleOnce' | 'once')
157
+ * - `handler`: 处理器函数
158
+ *
159
+ * @param properties 对象属性列表
160
+ * @param comment 外层属性的 JSDoc 注释
161
+ * @returns IPC 处理器信息
162
+ */
163
+ parseObjectHandler(properties, comment) {
164
+ let handlerType = "handle";
165
+ let handlerFunc;
166
+ for (const p of properties) {
167
+ if (p.name === "type" && p.value.kind === "string") {
168
+ handlerType = String(p.value.value);
169
+ } else if (p.name === "handler") {
170
+ handlerFunc = p.value;
171
+ }
172
+ }
173
+ if (!handlerFunc || handlerFunc.kind !== "function" || !handlerFunc.function) {
174
+ return null;
175
+ }
176
+ return this.extractHandlerFromFunction(handlerFunc.function, handlerType, comment);
177
+ }
178
+ /**
179
+ * 从函数表达式中提取处理器的参数和返回类型信息
180
+ *
181
+ * 智能跳过第一个 event 参数:
182
+ * - 参数名以 _ 开头或名为 event/e → 视为 event 参数,跳过
183
+ * - 否则保留所有参数
184
+ *
185
+ * @param func 函数表达式信息
186
+ * @param handlerType 处理器类型
187
+ * @param comment JSDoc 注释
188
+ * @returns IPC 处理器信息
189
+ */
190
+ extractHandlerFromFunction(func, handlerType, comment) {
191
+ const params = this.skipEventParam(func.parameters);
192
+ const paramNames = params.map((p) => p.name);
193
+ const paramTypes = params.map((p) => p.type ?? "any");
194
+ let returnType = func.returnType ?? (func.isAsync ? "Promise<any>" : "any");
195
+ if (func.isAsync && func.returnType && !func.returnType.startsWith("Promise")) {
196
+ returnType = `Promise<${func.returnType}>`;
197
+ }
198
+ return {
199
+ paramNames,
200
+ paramTypes,
201
+ returnType,
202
+ comment: comment ?? void 0,
203
+ handlerType
204
+ };
205
+ }
206
+ /**
207
+ * 智能跳过函数参数列表中的 event 参数
208
+ *
209
+ * 判断第一个参数是否为 Electron IPC 事件对象:
210
+ * - 参数名以 _ 开头(如 _event, _e)
211
+ * - 参数名为 event 或 e
212
+ * - 参数类型包含 Event
213
+ *
214
+ * @param params 原始参数列表
215
+ * @returns 跳过 event 参数后的参数列表
216
+ */
217
+ skipEventParam(params) {
218
+ if (params.length === 0) return params;
219
+ const first = params[0];
220
+ const name = first.name;
221
+ const type = first.type ?? "";
222
+ const isEventParam = name.startsWith("_") || name === "event" || name === "e" || type.includes("Event");
223
+ return isEventParam ? params.slice(1) : params;
224
+ }
225
+ /**
226
+ * 收集文件中引用的类型信息
227
+ *
228
+ * 包括两部分:
229
+ * 1. import 语句中导入的类型(过滤掉 runtime 导入)
230
+ * 2. 文件内定义的 export interface/type(作为同文件类型)
231
+ *
232
+ * @param parsed 文件解析结果
233
+ * @param filePath 文件路径(用于本地类型的模块路径)
234
+ * @returns 类型名 -> 模块路径 的映射
235
+ */
236
+ collectImports(parsed, filePath) {
237
+ const imports = /* @__PURE__ */ new Map();
238
+ for (const imp of parsed.imports) {
239
+ if (imp.moduleSpecifier.includes("epreload/runtime") || imp.namedImports.some((n) => n.name === "defineIpc")) {
240
+ continue;
241
+ }
242
+ if (imp.defaultImport) {
243
+ imports.set(imp.defaultImport, imp.moduleSpecifier);
244
+ }
245
+ for (const named of imp.namedImports) {
246
+ const name = named.alias ?? named.name;
247
+ imports.set(name, imp.moduleSpecifier);
248
+ }
249
+ }
250
+ const localModulePath = this.toModulePath(filePath);
251
+ for (const iface of parsed.interfaces) {
252
+ if (iface.isExported) {
253
+ imports.set(iface.name, localModulePath);
254
+ }
255
+ }
256
+ for (const typeAlias of parsed.typeAliases) {
257
+ if (typeAlias.isExported) {
258
+ imports.set(typeAlias.name, localModulePath);
259
+ }
260
+ }
261
+ return imports;
262
+ }
263
+ /**
264
+ * 将文件绝对路径转换为相对模块路径(去掉扩展名)
265
+ *
266
+ * @param filePath 文件绝对路径
267
+ * @returns 相对模块路径
268
+ */
269
+ toModulePath(filePath) {
270
+ return filePath.replace(/\.ts$/, "");
271
+ }
272
+ }
273
+
274
+ class EventHandlerModel {
275
+ /**
276
+ * 数据类型
277
+ */
278
+ dataType;
279
+ /**
280
+ * JSDoc 注释
281
+ */
282
+ comment;
283
+ /**
284
+ * 是否为一次性监听
285
+ */
286
+ once;
287
+ /**
288
+ * Remove 方法类型
289
+ */
290
+ remove;
291
+ constructor(dataType, comment, once = false, remove = false) {
292
+ this.dataType = dataType;
293
+ this.comment = comment;
294
+ this.once = once;
295
+ this.remove = remove;
296
+ }
297
+ /**
298
+ * 获取回调函数类型签名
299
+ */
300
+ getCallbackSignature() {
301
+ return `(data: ${this.dataType}) => void`;
302
+ }
303
+ /**
304
+ * 获取监听方法类型签名
305
+ */
306
+ getListenerSignature() {
307
+ return `(callback: ${this.getCallbackSignature()}) => () => void`;
308
+ }
309
+ /**
310
+ * 获取 remove 方法类型签名(single)
311
+ */
312
+ getRemoveSingleSignature() {
313
+ return `(callback: ${this.getCallbackSignature()}) => void`;
314
+ }
315
+ /**
316
+ * 获取 removeAll 方法类型签名
317
+ */
318
+ getRemoveAllSignature() {
319
+ return `() => void`;
320
+ }
321
+ }
322
+
323
+ class EventFileScanner {
324
+ dir;
325
+ source;
326
+ ignores;
327
+ parser;
328
+ logger = Logger.scope("EventFileScanner");
329
+ /**
330
+ * @param dir 扫描根目录
331
+ * @param source 文件匹配模式(glob 格式,支持数组)
332
+ * @param ignores 忽略的文件模式
333
+ */
334
+ constructor(dir, source, ignores) {
335
+ this.dir = dir;
336
+ this.source = source;
337
+ this.ignores = ignores;
338
+ this.parser = new TsParser();
339
+ }
340
+ /**
341
+ * 扫描所有匹配的事件定义文件,返回解析结果列表
342
+ */
343
+ scan() {
344
+ const patterns = Array.isArray(this.source) ? this.source : [this.source];
345
+ const files = fg.sync(patterns, {
346
+ cwd: this.dir,
347
+ absolute: true,
348
+ ignore: this.ignores
349
+ });
350
+ const results = [];
351
+ for (const file of files) {
352
+ try {
353
+ const defi = this.parseFile(file);
354
+ if (defi) {
355
+ results.push(defi);
356
+ }
357
+ } catch (e) {
358
+ this.logger.warn(`\u89E3\u6790\u4E8B\u4EF6\u6587\u4EF6\u5931\u8D25: ${file}`, e);
359
+ }
360
+ }
361
+ return results;
362
+ }
363
+ /**
364
+ * 解析单个事件定义文件
365
+ *
366
+ * 查找文件中的 `export default defineEvents(...)` 调用,
367
+ * 提取所有事件处理器定义。
368
+ *
369
+ * @param filePath 文件路径
370
+ * @returns 事件定义信息,文件中无 defineEvents 调用时返回 null
371
+ */
372
+ parseFile(filePath) {
373
+ const parsed = this.parser.parse(filePath);
374
+ const defaultExport = parsed.defaultExport;
375
+ if (!defaultExport || defaultExport.kind !== "call" || !defaultExport.call) {
376
+ return null;
377
+ }
378
+ const call = defaultExport.call;
379
+ if (call.callee !== "defineEvents") {
380
+ return null;
381
+ }
382
+ return this.parseDefineEventsCall(filePath, call, parsed);
383
+ }
384
+ /**
385
+ * 解析 defineEvents 函数调用,提取事件处理器映射
386
+ *
387
+ * 支持两种调用形式:
388
+ * - `defineEvents({ ... })` — 无模块名
389
+ * - `defineEvents('moduleName', { ... })` — 有模块名
390
+ *
391
+ * @param filePath 文件路径
392
+ * @param call 函数调用表达式信息
393
+ * @param parsed 完整的文件解析结果(用于提取 import 信息)
394
+ * @returns 事件定义信息
395
+ */
396
+ parseDefineEventsCall(filePath, call, parsed) {
397
+ const args = call.arguments;
398
+ if (args.length < 1) {
399
+ this.logger.warn(`defineEvents \u8C03\u7528\u53C2\u6570\u4E0D\u8DB3: ${filePath}`);
400
+ return null;
401
+ }
402
+ let moduleName;
403
+ let handlersArg;
404
+ if (args.length >= 2 && args[0].kind === "string") {
405
+ moduleName = String(args[0].value);
406
+ handlersArg = args[1];
407
+ } else {
408
+ handlersArg = args[0];
409
+ }
410
+ if (handlersArg.kind !== "object" || !handlersArg.properties) {
411
+ this.logger.warn(`defineEvents \u5904\u7406\u5668\u53C2\u6570\u4E0D\u662F\u5BF9\u8C61\u5B57\u9762\u91CF: ${filePath}`);
412
+ return null;
413
+ }
414
+ const handlers = /* @__PURE__ */ new Map();
415
+ for (const prop of handlersArg.properties) {
416
+ const model = this.parseEventHandler(prop);
417
+ if (model) {
418
+ handlers.set(prop.name, model);
419
+ }
420
+ }
421
+ const imports = this.collectImports(parsed, filePath);
422
+ return {
423
+ filePath,
424
+ module: moduleName,
425
+ handlers,
426
+ imports
427
+ };
428
+ }
429
+ /**
430
+ * 解析单个事件处理器属性
431
+ *
432
+ * 支持两种写法:
433
+ *
434
+ * 1. 对象形式:
435
+ * ```ts
436
+ * onDownloadProgress: {
437
+ * sample: (data: DownloadProgressData) => data,
438
+ * remove: 'single',
439
+ * once: true
440
+ * }
441
+ * ```
442
+ *
443
+ * 2. 简写函数形式(无 remove,非 once):
444
+ * ```ts
445
+ * onAlert: (data: AlertData) => data
446
+ * ```
447
+ *
448
+ * @param prop 对象属性信息
449
+ * @returns EventHandlerModel 实例,无法解析时返回 null
450
+ */
451
+ parseEventHandler(prop) {
452
+ const value = prop.value;
453
+ if (value.kind === "object" && value.properties) {
454
+ return this.parseObjectEventHandler(value.properties, prop.comment);
455
+ }
456
+ if (value.kind === "function" && value.function) {
457
+ const dataType = this.extractDataTypeFromFunction(value.function);
458
+ const once = prop.name.startsWith("once");
459
+ return new EventHandlerModel(dataType, prop.comment, once, false);
460
+ }
461
+ return null;
462
+ }
463
+ /**
464
+ * 从对象形式的事件处理器中提取信息
465
+ *
466
+ * @param properties 对象属性列表
467
+ * @param comment 外层属性的 JSDoc 注释
468
+ * @returns EventHandlerModel 实例
469
+ */
470
+ parseObjectEventHandler(properties, comment) {
471
+ let dataType = "any";
472
+ let once = false;
473
+ let remove = false;
474
+ for (const p of properties) {
475
+ switch (p.name) {
476
+ case "sample":
477
+ if (p.value.kind === "function" && p.value.function) {
478
+ dataType = this.extractDataTypeFromFunction(p.value.function);
479
+ }
480
+ break;
481
+ case "remove":
482
+ remove = this.parseRemoveType(p.value);
483
+ break;
484
+ case "once":
485
+ once = p.value.kind === "boolean" && p.value.value === true;
486
+ break;
487
+ }
488
+ }
489
+ return new EventHandlerModel(dataType, comment, once, remove);
490
+ }
491
+ /**
492
+ * 从函数表达式中提取数据类型
493
+ *
494
+ * 事件 sample 函数签名: `(data: DataType) => data`
495
+ * 从第一个参数的类型注解中提取数据类型。
496
+ *
497
+ * @param func 函数表达式信息
498
+ * @returns 数据类型字符串,无法提取时返回 'any'
499
+ */
500
+ extractDataTypeFromFunction(func) {
501
+ const params = func.parameters;
502
+ if (params.length === 0) {
503
+ return "void";
504
+ }
505
+ return params[0].type ?? "any";
506
+ }
507
+ /**
508
+ * 解析 remove 属性值为 RemoveType
509
+ * @param arg remove 属性值
510
+ * @returns RemoveType 值
511
+ */
512
+ parseRemoveType(arg) {
513
+ if (arg.kind === "string") {
514
+ const val = String(arg.value);
515
+ if (val === "single" || val === "all" || val === "both") {
516
+ return val;
517
+ }
518
+ }
519
+ if (arg.kind === "boolean" && arg.value === false) {
520
+ return false;
521
+ }
522
+ return false;
523
+ }
524
+ /**
525
+ * 收集文件中引用的类型信息
526
+ *
527
+ * 包括两部分:
528
+ * 1. import 语句中导入的类型(过滤掉 runtime 导入)
529
+ * 2. 文件内定义的 export interface/type(作为同文件类型)
530
+ *
531
+ * @param parsed 文件解析结果
532
+ * @param filePath 文件路径
533
+ * @returns 类型名 -> 模块路径 的映射
534
+ */
535
+ collectImports(parsed, filePath) {
536
+ const imports = /* @__PURE__ */ new Map();
537
+ for (const imp of parsed.imports) {
538
+ if (imp.moduleSpecifier.includes("epreload/runtime") || imp.namedImports.some((n) => n.name === "defineEvents")) {
539
+ continue;
540
+ }
541
+ if (imp.defaultImport) {
542
+ imports.set(imp.defaultImport, imp.moduleSpecifier);
543
+ }
544
+ for (const named of imp.namedImports) {
545
+ const name = named.alias ?? named.name;
546
+ imports.set(name, imp.moduleSpecifier);
547
+ }
548
+ }
549
+ const localModulePath = filePath.replace(/\.ts$/, "");
550
+ for (const iface of parsed.interfaces) {
551
+ if (iface.isExported) {
552
+ imports.set(iface.name, localModulePath);
553
+ }
554
+ }
555
+ for (const typeAlias of parsed.typeAliases) {
556
+ if (typeAlias.isExported) {
557
+ imports.set(typeAlias.name, localModulePath);
558
+ }
559
+ }
560
+ return imports;
561
+ }
562
+ }
563
+
564
+ class EasyPreloadGenerator {
565
+ config;
566
+ logger = Logger.scope("EasyPreloadGenerator");
567
+ constructor(config) {
568
+ this.config = ConfigUtil.deepMergeConfig(DEFAULT_EASY_PRELOAD_GENERATOR_CONFIG, config);
569
+ }
570
+ /**
571
+ * 执行生成,扫描 IPC 和 Event 定义文件,输出 preload 和类型文件
572
+ */
573
+ generate() {
574
+ const ipcDefs = this.scanIpc();
575
+ const eventDefs = this.scanEvents();
576
+ const preloadContent = this.generatePreload(ipcDefs, eventDefs);
577
+ const typesContent = this.generateTypes(ipcDefs, eventDefs);
578
+ this.writeFile(this.config.output, preloadContent);
579
+ this.writeFile(this.config.typesOutput, typesContent);
580
+ this.logger.info(`\u751F\u6210\u5B8C\u6210: ${this.config.output}, ${this.config.typesOutput}`);
581
+ }
582
+ /**
583
+ * 扫描 IPC 定义文件
584
+ */
585
+ scanIpc() {
586
+ const scanner = new IpcFileScanner(this.config.dir, this.config.source, this.config.ignores);
587
+ return scanner.scan();
588
+ }
589
+ /**
590
+ * 扫描 Event 定义文件
591
+ */
592
+ scanEvents() {
593
+ const scanner = new EventFileScanner(this.config.dir, this.config.events, this.config.ignores);
594
+ return scanner.scan();
595
+ }
596
+ // ==================== Preload 文件生成 ====================
597
+ /**
598
+ * 生成 preload.ts 文件内容
599
+ *
600
+ * 结构:
601
+ * ```ts
602
+ * contextBridge.exposeInMainWorld('globalName', {
603
+ * // 无模块 IPC 方法(顶层)
604
+ * ping: () => ipcRenderer.invoke('ping'),
605
+ * // 有模块 IPC 方法(嵌套对象)
606
+ * test: {
607
+ * getData: (id) => ipcRenderer.invoke('test:getData', id),
608
+ * },
609
+ * // 事件(嵌套在 events 下)
610
+ * events: {
611
+ * onXxx: (callback) => { ... },
612
+ * module: { onYyy: (callback) => { ... } },
613
+ * },
614
+ * })
615
+ * ```
616
+ */
617
+ generatePreload(ipcDefs, eventDefs) {
618
+ const lines = [];
619
+ const typeImports = this.collectTypeImports(ipcDefs, eventDefs, this.config.output);
620
+ lines.push("import { contextBridge, ipcRenderer } from 'electron'");
621
+ lines.push("import type { IpcRendererEvent } from 'electron'");
622
+ for (const imp of typeImports) {
623
+ lines.push(imp);
624
+ }
625
+ lines.push("");
626
+ lines.push("// Auto-generated preload file");
627
+ lines.push("// Do not edit this file manually");
628
+ lines.push("");
629
+ lines.push(`contextBridge.exposeInMainWorld('${this.config.globalName}', {`);
630
+ this.generatePreloadIpc(ipcDefs, lines);
631
+ this.generatePreloadEvents(eventDefs, lines);
632
+ lines.push("})");
633
+ lines.push("");
634
+ return lines.join("\n");
635
+ }
636
+ /**
637
+ * 生成 preload 中的 IPC 调用代码
638
+ *
639
+ * 无模块的处理器直接放在顶层,有模块的嵌套在模块名对象下。
640
+ * 根据 handlerType 选择 invoke(handle/handleOnce)或 send(on/once)。
641
+ */
642
+ generatePreloadIpc(ipcDefs, lines) {
643
+ const noModule = [];
644
+ const withModule = [];
645
+ for (const def of ipcDefs) {
646
+ if (def.module) {
647
+ withModule.push(def);
648
+ } else {
649
+ noModule.push(def);
650
+ }
651
+ }
652
+ for (const def of noModule) {
653
+ for (const [name, handler] of def.handlers) {
654
+ lines.push(` ${this.generatePreloadIpcMethod(name, name, handler)},`);
655
+ }
656
+ }
657
+ for (const def of withModule) {
658
+ lines.push(` ${def.module}: {`);
659
+ for (const [name, handler] of def.handlers) {
660
+ const channel = `${def.module}:${name}`;
661
+ lines.push(` ${this.generatePreloadIpcMethod(name, channel, handler)},`);
662
+ }
663
+ lines.push(" },");
664
+ }
665
+ }
666
+ /**
667
+ * 生成单个 IPC 方法的 preload 代码
668
+ *
669
+ * @param name 方法名
670
+ * @param channel IPC 通道名
671
+ * @param handler 处理器信息
672
+ * @returns 方法代码字符串(不含尾部逗号)
673
+ */
674
+ generatePreloadIpcMethod(name, channel, handler) {
675
+ const params = handler.paramNames.map((n, i) => `${n}: ${handler.paramTypes[i]}`).join(", ");
676
+ const args = handler.paramNames.length > 0 ? ", " + handler.paramNames.join(", ") : "";
677
+ const type = handler.handlerType ?? "handle";
678
+ switch (type) {
679
+ case "handle":
680
+ case "handleOnce":
681
+ return `${name}: (${params}): ${this.ensurePromise(handler.returnType)} => ipcRenderer.invoke('${channel}'${args})`;
682
+ case "on":
683
+ case "once":
684
+ return `${name}: (${params}): void => ipcRenderer.send('${channel}'${args})`;
685
+ default:
686
+ return `${name}: (${params}): ${this.ensurePromise(handler.returnType)} => ipcRenderer.invoke('${channel}'${args})`;
687
+ }
688
+ }
689
+ /**
690
+ * 生成 preload 中的事件监听代码
691
+ *
692
+ * 所有事件嵌套在 `events` 对象下。
693
+ * 无模块的事件直接放在 events 下,有模块的嵌套在 events.module 下。
694
+ */
695
+ generatePreloadEvents(eventDefs, lines) {
696
+ if (eventDefs.length === 0) return;
697
+ const noModule = [];
698
+ const withModule = [];
699
+ for (const def of eventDefs) {
700
+ if (def.module) {
701
+ withModule.push(def);
702
+ } else {
703
+ noModule.push(def);
704
+ }
705
+ }
706
+ lines.push(" events: {");
707
+ for (const def of noModule) {
708
+ for (const [name, handler] of def.handlers) {
709
+ const channel = this.toEventChannel(name);
710
+ this.generatePreloadEventMethods(name, channel, handler, lines, " ");
711
+ }
712
+ }
713
+ for (const def of withModule) {
714
+ lines.push(` ${def.module}: {`);
715
+ for (const [name, handler] of def.handlers) {
716
+ const channel = `${def.module}:${this.toEventChannel(name)}`;
717
+ this.generatePreloadEventMethods(name, channel, handler, lines, " ");
718
+ }
719
+ lines.push(" },");
720
+ }
721
+ lines.push(" },");
722
+ }
723
+ /**
724
+ * 生成单个事件的监听/移除方法代码
725
+ *
726
+ * 根据 handler 的 once 和 remove 属性生成:
727
+ * - on/once 监听方法
728
+ * - removeSingle 方法(remove 为 'single' 或 'both')
729
+ * - removeAll 方法(remove 为 'all' 或 'both')
730
+ *
731
+ * @param name 事件名(如 onDownloadProgress)
732
+ * @param channel IPC 通道名
733
+ * @param handler 事件处理器模型
734
+ * @param lines 输出行数组
735
+ * @param indent 缩进字符串
736
+ */
737
+ generatePreloadEventMethods(name, channel, handler, lines, indent) {
738
+ const dataType = handler.dataType;
739
+ const listenMethod = handler.once ? "once" : "on";
740
+ const listenerName = handler.once && !name.startsWith("once") ? name.replace(/^on/, "once") : name;
741
+ lines.push(`${indent}${listenerName}: (callback: (data: ${dataType}) => void) => {`);
742
+ lines.push(`${indent} const listener = (_event: IpcRendererEvent, data: ${dataType}) => callback(data)`);
743
+ lines.push(`${indent} ipcRenderer.${listenMethod}('${channel}', listener)`);
744
+ if (!handler.once) {
745
+ lines.push(`${indent} return () => ipcRenderer.removeListener('${channel}', listener)`);
746
+ }
747
+ lines.push(`${indent}},`);
748
+ const baseName = this.stripEventPrefix(name);
749
+ this.generatePreloadRemoveMethods(baseName, channel, dataType, handler.remove, lines, indent);
750
+ }
751
+ /**
752
+ * 生成事件的 remove 方法代码
753
+ */
754
+ generatePreloadRemoveMethods(baseName, channel, dataType, remove, lines, indent) {
755
+ if (remove === "single" || remove === "both") {
756
+ lines.push(`${indent}remove${baseName}: (callback: (data: ${dataType}) => void) => {`);
757
+ lines.push(`${indent} const listener = (_event: IpcRendererEvent, data: ${dataType}) => callback(data)`);
758
+ lines.push(`${indent} ipcRenderer.removeListener('${channel}', listener)`);
759
+ lines.push(`${indent}},`);
760
+ }
761
+ if (remove === "all" || remove === "both") {
762
+ lines.push(`${indent}remove${baseName}All: () => {`);
763
+ lines.push(`${indent} ipcRenderer.removeAllListeners('${channel}')`);
764
+ lines.push(`${indent}},`);
765
+ }
766
+ }
767
+ // ==================== 类型文件生成 ====================
768
+ /**
769
+ * 生成类型定义文件内容
770
+ *
771
+ * 结构:
772
+ * ```ts
773
+ * interface ElecApi {
774
+ * ping: () => Promise<string>
775
+ * test: { getData: (id: string) => Promise<Data> }
776
+ * }
777
+ * export interface ElecEvents {
778
+ * onXxx: (callback: (data: T) => void) => () => void
779
+ * }
780
+ * declare global {
781
+ * interface Window { elec: ElecApi & { events: ElecEvents } }
782
+ * }
783
+ * export {}
784
+ * ```
785
+ */
786
+ generateTypes(ipcDefs, eventDefs) {
787
+ const lines = [];
788
+ const typeImports = this.collectTypeImports(ipcDefs, eventDefs, this.config.typesOutput);
789
+ for (const imp of typeImports) {
790
+ lines.push(imp);
791
+ }
792
+ lines.push("");
793
+ lines.push("// Auto-generated type definitions");
794
+ lines.push("// Do not edit this file manually");
795
+ lines.push("");
796
+ this.generateTypesIpc(ipcDefs, lines);
797
+ const hasEvents = eventDefs.length > 0;
798
+ if (hasEvents) {
799
+ lines.push("");
800
+ this.generateTypesEvents(eventDefs, lines);
801
+ }
802
+ lines.push("");
803
+ lines.push("declare global {");
804
+ lines.push(" interface Window {");
805
+ if (hasEvents) {
806
+ lines.push(` ${this.config.globalName}: ElecApi & { events: ElecEvents }`);
807
+ } else {
808
+ lines.push(` ${this.config.globalName}: ElecApi`);
809
+ }
810
+ lines.push(" }");
811
+ lines.push("}");
812
+ lines.push("");
813
+ lines.push("export {}");
814
+ lines.push("");
815
+ return lines.join("\n");
816
+ }
817
+ /**
818
+ * 生成 ElecApi 接口中的 IPC 方法类型
819
+ */
820
+ generateTypesIpc(ipcDefs, lines) {
821
+ const noModule = [];
822
+ const withModule = [];
823
+ for (const def of ipcDefs) {
824
+ if (def.module) {
825
+ withModule.push(def);
826
+ } else {
827
+ noModule.push(def);
828
+ }
829
+ }
830
+ lines.push("interface ElecApi {");
831
+ for (const def of noModule) {
832
+ for (const [name, handler] of def.handlers) {
833
+ this.generateTypesIpcMethod(name, handler, lines, " ");
834
+ }
835
+ }
836
+ for (const def of withModule) {
837
+ lines.push(` ${def.module}: {`);
838
+ for (const [name, handler] of def.handlers) {
839
+ this.generateTypesIpcMethod(name, handler, lines, " ");
840
+ }
841
+ lines.push(" }");
842
+ }
843
+ lines.push("}");
844
+ }
845
+ /**
846
+ * 生成单个 IPC 方法的类型签名
847
+ *
848
+ * @param name 方法名
849
+ * @param handler 处理器信息
850
+ * @param lines 输出行数组
851
+ * @param indent 缩进字符串
852
+ */
853
+ generateTypesIpcMethod(name, handler, lines, indent) {
854
+ if (this.config.preserveComments && handler.comment) {
855
+ const commentLines = handler.comment.split("\n");
856
+ for (const cl of commentLines) {
857
+ lines.push(`${indent}${cl.trim()}`);
858
+ }
859
+ }
860
+ const params = handler.paramNames.map((n, i) => `${n}: ${handler.paramTypes[i]}`).join(", ");
861
+ const type = handler.handlerType ?? "handle";
862
+ const returnType = type === "on" || type === "once" ? "void" : this.ensurePromise(handler.returnType);
863
+ lines.push(`${indent}${name}: (${params}) => ${returnType}`);
864
+ }
865
+ /**
866
+ * 生成 ElecEvents 接口中的事件方法类型
867
+ */
868
+ generateTypesEvents(eventDefs, lines) {
869
+ const noModule = [];
870
+ const withModule = [];
871
+ for (const def of eventDefs) {
872
+ if (def.module) {
873
+ withModule.push(def);
874
+ } else {
875
+ noModule.push(def);
876
+ }
877
+ }
878
+ lines.push("export interface ElecEvents {");
879
+ for (const def of noModule) {
880
+ for (const [name, handler] of def.handlers) {
881
+ this.generateTypesEventMethods(name, handler, lines, " ");
882
+ }
883
+ }
884
+ for (const def of withModule) {
885
+ lines.push(` ${def.module}: {`);
886
+ for (const [name, handler] of def.handlers) {
887
+ this.generateTypesEventMethods(name, handler, lines, " ");
888
+ }
889
+ lines.push(" }");
890
+ }
891
+ lines.push("}");
892
+ }
893
+ /**
894
+ * 生成单个事件的类型签名(监听 + remove 方法)
895
+ */
896
+ generateTypesEventMethods(name, handler, lines, indent) {
897
+ const listenerName = handler.once && !name.startsWith("once") ? name.replace(/^on/, "once") : name;
898
+ if (this.config.preserveComments && handler.comment) {
899
+ const commentLines = handler.comment.split("\n");
900
+ for (const cl of commentLines) {
901
+ lines.push(`${indent}${cl.trim()}`);
902
+ }
903
+ }
904
+ if (handler.once) {
905
+ lines.push(`${indent}${listenerName}: ${handler.getListenerSignature()}`);
906
+ } else {
907
+ lines.push(`${indent}${listenerName}: ${handler.getListenerSignature()}`);
908
+ }
909
+ const baseName = this.stripEventPrefix(name);
910
+ if (handler.remove === "single" || handler.remove === "both") {
911
+ lines.push(`${indent}remove${baseName}: ${handler.getRemoveSingleSignature()}`);
912
+ }
913
+ if (handler.remove === "all" || handler.remove === "both") {
914
+ lines.push(`${indent}remove${baseName}All: ${handler.getRemoveAllSignature()}`);
915
+ }
916
+ }
917
+ // ==================== 工具方法 ====================
918
+ /**
919
+ * 收集所有 IPC 和 Event 定义中引用的类型导入语句
920
+ *
921
+ * 将各文件的 imports 映射合并,按模块路径分组,
922
+ * 生成相对于输出文件的 import type 语句。
923
+ *
924
+ * @param ipcDefs IPC 定义列表
925
+ * @param eventDefs Event 定义列表
926
+ * @returns import 语句列表
927
+ */
928
+ collectTypeImports(ipcDefs, eventDefs, outputPath) {
929
+ const grouped = /* @__PURE__ */ new Map();
930
+ const addImports = (imports, sourceFilePath) => {
931
+ for (const [typeName, modulePath] of imports) {
932
+ const resolvedPath = this.resolveImportPath(sourceFilePath, modulePath, outputPath);
933
+ if (!grouped.has(resolvedPath)) {
934
+ grouped.set(resolvedPath, /* @__PURE__ */ new Set());
935
+ }
936
+ grouped.get(resolvedPath).add(typeName);
937
+ }
938
+ };
939
+ for (const def of ipcDefs) {
940
+ addImports(def.imports, def.filePath);
941
+ }
942
+ for (const def of eventDefs) {
943
+ addImports(def.imports, def.filePath);
944
+ }
945
+ const result = [];
946
+ for (const [modulePath, types] of grouped) {
947
+ const typeNames = Array.from(types).sort().join(", ");
948
+ result.push(`import type { ${typeNames} } from '${modulePath}'`);
949
+ }
950
+ return result;
951
+ }
952
+ /**
953
+ * 将源文件中的模块路径转换为相对于输出文件的路径
954
+ *
955
+ * 支持三种输入:
956
+ * - 相对路径(以 . 开头): 相对于源文件解析后再转换
957
+ * - 绝对路径: 直接计算相对于输出文件的路径
958
+ * - 包导入(非相对/绝对路径): 直接返回
959
+ *
960
+ * @param sourceFilePath 源文件绝对路径
961
+ * @param modulePath 模块路径(相对、绝对或包名)
962
+ * @returns 相对于输出文件的模块路径
963
+ */
964
+ resolveImportPath(sourceFilePath, modulePath, outputPath) {
965
+ let absoluteModulePath;
966
+ if (path.isAbsolute(modulePath)) {
967
+ absoluteModulePath = modulePath;
968
+ } else if (modulePath.startsWith(".")) {
969
+ const sourceDir = path.dirname(sourceFilePath);
970
+ absoluteModulePath = path.resolve(sourceDir, modulePath);
971
+ } else {
972
+ return modulePath;
973
+ }
974
+ const outputDir = path.dirname(path.resolve(outputPath));
975
+ let relativePath = path.relative(outputDir, absoluteModulePath);
976
+ relativePath = relativePath.replace(/\\/g, "/");
977
+ if (!relativePath.startsWith(".")) {
978
+ relativePath = "./" + relativePath;
979
+ }
980
+ return relativePath;
981
+ }
982
+ /**
983
+ * 将事件名转换为 IPC 通道名(camelCase → kebab-case)
984
+ *
985
+ * 去掉 on/once 前缀后转换:
986
+ * - onDownloadProgress → download-progress
987
+ * - onceReady → ready
988
+ *
989
+ * @param name 事件名
990
+ * @returns 通道名
991
+ */
992
+ toEventChannel(name) {
993
+ let baseName = name;
994
+ if (baseName.startsWith("once")) {
995
+ baseName = baseName.slice(4);
996
+ } else if (baseName.startsWith("on")) {
997
+ baseName = baseName.slice(2);
998
+ }
999
+ baseName = baseName.charAt(0).toLowerCase() + baseName.slice(1);
1000
+ return baseName.replace(/([A-Z])/g, "-$1").toLowerCase();
1001
+ }
1002
+ /**
1003
+ * 去掉事件名的 on/once 前缀,返回大写开头的基础名
1004
+ *
1005
+ * - onDownloadProgress → DownloadProgress
1006
+ * - onceReady → Ready
1007
+ *
1008
+ * @param name 事件名
1009
+ * @returns 基础名(首字母大写)
1010
+ */
1011
+ stripEventPrefix(name) {
1012
+ if (name.startsWith("once")) {
1013
+ return name.slice(4);
1014
+ }
1015
+ if (name.startsWith("on")) {
1016
+ return name.slice(2);
1017
+ }
1018
+ return name.charAt(0).toUpperCase() + name.slice(1);
1019
+ }
1020
+ /**
1021
+ * 确保返回类型被 Promise 包裹
1022
+ *
1023
+ * - 已经是 Promise<T> → 原样返回
1024
+ * - 其他类型 → 包裹为 Promise<T>
1025
+ *
1026
+ * @param returnType 原始返回类型
1027
+ * @returns Promise 包裹后的返回类型
1028
+ */
1029
+ ensurePromise(returnType) {
1030
+ if (returnType.startsWith("Promise<")) {
1031
+ return returnType;
1032
+ }
1033
+ return `Promise<${returnType}>`;
1034
+ }
1035
+ /**
1036
+ * 写入文件,自动创建目录
1037
+ */
1038
+ writeFile(filePath, content) {
1039
+ const dir = path.dirname(filePath);
1040
+ if (!fs.existsSync(dir)) {
1041
+ fs.mkdirSync(dir, { recursive: true });
1042
+ }
1043
+ fs.writeFileSync(filePath, content, "utf-8");
1044
+ this.logger.info(`\u5DF2\u5199\u5165: ${filePath}`);
1045
+ }
1046
+ }
1047
+
1048
+ export { EasyPreloadGenerator };