@dino-dev/bridge-core-ts 0.0.4 → 0.0.5

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.
@@ -0,0 +1,390 @@
1
+ /**
2
+ * 浏览器 Mock Bridge —— 让 H5 页面脱离 App 容器,在 Chrome 里直接开发调试。
3
+ *
4
+ * 不安装时,浏览器环境下 `whenReady()` 永不 resolve、所有原生调用静默返回 `null`,
5
+ * 页面会停在空状态且不报错。安装后即可拿到可控的假数据与事件。
6
+ *
7
+ * 仅供开发使用,请通过子路径引入,避免被打进生产包:
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * if (import.meta.env.DEV) {
12
+ * const { installMockBridge } = await import('@h5-container/bridge-core-ts/mock');
13
+ * installMockBridge({
14
+ * packageId: 'student_micro_pages',
15
+ * allowedModules: ['user', 'audio', 'navigation'],
16
+ * modules: {
17
+ * user: { getProfile: { userId: '1', nickname: '测试同学' } },
18
+ * audio: { play: async () => ({ ok: true }) },
19
+ * },
20
+ * });
21
+ * }
22
+ * ```
23
+ */
24
+
25
+ import { bridge, type BridgeMessage } from './mrp-bridge';
26
+ import type { ArAdaptationInfo, SafeAreaInsets } from './system-module';
27
+
28
+ // ============================================================
29
+ // 类型定义
30
+ // ============================================================
31
+
32
+ /**
33
+ * 单个模块的 mock 实现。
34
+ *
35
+ * 值可以是函数(接收原始调用参数),也可以是静态数据(直接作为返回值)。
36
+ */
37
+ export type MockModuleImpl = Record<
38
+ string,
39
+ ((...args: any[]) => unknown | Promise<unknown>) | unknown
40
+ >;
41
+
42
+ /** 一次原生调用的记录 */
43
+ export interface MockCallRecord {
44
+ namespace: string;
45
+ method: string;
46
+ args: unknown[];
47
+ result: unknown;
48
+ /** 未命中 mock 定义时为 true */
49
+ missing: boolean;
50
+ timestamp: number;
51
+ }
52
+
53
+ export interface MockBridgeConfig {
54
+ /** 容器信息里的包 ID */
55
+ packageId?: string;
56
+ /**
57
+ * 授权模块白名单。
58
+ *
59
+ * 传入后会像真实容器一样拦截越权调用并返回 `MODULE_ACCESS_DENIED`,
60
+ * 可在浏览器阶段提前发现 `package.meta.json` 的 `allowedModules` 漏配。
61
+ * 不传则不做权限校验。
62
+ */
63
+ allowedModules?: string[];
64
+ /** 容器已注册模块列表,默认与 allowedModules 一致 */
65
+ registeredModules?: string[];
66
+ /** 屏幕适配信息,缺省字段按当前窗口尺寸推算 */
67
+ adaptation?: Partial<ArAdaptationInfo>;
68
+ /** 注入到 getContainerInfo().auth,供 h5_app_http 读取 */
69
+ auth?: Record<string, unknown>;
70
+ /** 追加到 getContainerInfo() 的其他字段 */
71
+ containerInfoExtra?: Record<string, unknown>;
72
+ /** 各命名空间的 mock 实现 */
73
+ modules?: Record<string, MockModuleImpl>;
74
+ /** 处理 `bridge.send()`,返回值作为调用方收到的结果 */
75
+ onSend?: (
76
+ type: string,
77
+ action: string,
78
+ payload: Record<string, unknown>,
79
+ ) => unknown;
80
+ /** 是否打印调用日志,默认 true */
81
+ log?: boolean;
82
+ /** 窗口尺寸变化时自动重算并广播 adaptation,默认 true */
83
+ followWindowResize?: boolean;
84
+ }
85
+
86
+ /** 安装后返回的控制句柄 */
87
+ export interface MockBridgeHandle {
88
+ /** 模拟一条 Native → H5 消息 */
89
+ emit(type: string, action: string, payload?: Record<string, unknown>): void;
90
+ /** 更新屏幕适配信息并广播 `adaptation/changed` */
91
+ setAdaptation(patch: Partial<ArAdaptationInfo>): void;
92
+ /** 按时间顺序记录的原生调用 */
93
+ readonly calls: MockCallRecord[];
94
+ /** 打印所有未命中 mock 定义的调用,便于补齐 */
95
+ printMissing(): void;
96
+ /** 卸载 mock,恢复安装前的 window 状态 */
97
+ uninstall(): void;
98
+ }
99
+
100
+ // ============================================================
101
+ // 内部状态
102
+ // ============================================================
103
+
104
+ const MOCK_FLAG = '__MRP_MOCK_BRIDGE__';
105
+ const DEFAULT_DESIGN_LONG_SIDE = 812;
106
+ const ZERO_SAFE_AREA: SafeAreaInsets = { top: 0, bottom: 0, left: 0, right: 0 };
107
+
108
+ declare global {
109
+ interface Window {
110
+ /** 已安装的 Mock Bridge 句柄,可在控制台直接操作 */
111
+ __MRP_MOCK__?: MockBridgeHandle;
112
+ }
113
+ }
114
+
115
+ function buildAdaptation(
116
+ patch: Partial<ArAdaptationInfo> | undefined,
117
+ ): ArAdaptationInfo {
118
+ const screenWidth = patch?.screenWidth ?? window.innerWidth;
119
+ const screenHeight = patch?.screenHeight ?? window.innerHeight;
120
+ const designLongSide = patch?.designLongSide ?? DEFAULT_DESIGN_LONG_SIDE;
121
+ const scale =
122
+ patch?.scale ?? Math.max(screenWidth, screenHeight) / designLongSide;
123
+ return {
124
+ designLongSide,
125
+ screenWidth,
126
+ screenHeight,
127
+ scale,
128
+ safeArea: patch?.safeArea ?? ZERO_SAFE_AREA,
129
+ };
130
+ }
131
+
132
+ // ============================================================
133
+ // 安装
134
+ // ============================================================
135
+
136
+ /**
137
+ * 安装 Mock Bridge。
138
+ *
139
+ * 已经在真实容器中运行时会直接跳过,因此可以无条件调用。
140
+ */
141
+ export function installMockBridge(
142
+ config: MockBridgeConfig = {},
143
+ ): MockBridgeHandle | null {
144
+ const existing = window.__MRP_MOCK__;
145
+ if (existing) {
146
+ console.warn('[Mock Bridge] 已安装,忽略重复调用');
147
+ return existing;
148
+ }
149
+ if (window.__FLUTTER_BRIDGE__) {
150
+ console.info('[Mock Bridge] 检测到真实原生容器,跳过安装');
151
+ return null;
152
+ }
153
+
154
+ const log = config.log ?? true;
155
+ const calls: MockCallRecord[] = [];
156
+ let adaptation = buildAdaptation(config.adaptation);
157
+
158
+ const previousNativeModules = window.NativeModules;
159
+
160
+ const emit = (
161
+ type: string,
162
+ action: string,
163
+ payload: Record<string, unknown> = {},
164
+ ): void => {
165
+ const message: BridgeMessage = {
166
+ type,
167
+ action,
168
+ payload,
169
+ timestamp: Date.now(),
170
+ };
171
+ if (log) {
172
+ console.log(`[Mock Bridge] → H5 ${type}/${action}`, payload);
173
+ }
174
+ window.dispatchEvent(
175
+ new CustomEvent('flutterMessage', { detail: message }),
176
+ );
177
+ };
178
+
179
+ // ---- system 模块:容器内置,始终可用 ----
180
+
181
+ const systemModule: MockModuleImpl = {
182
+ isModuleAllowed: (params: { namespace: string }) => ({
183
+ allowed: isAllowed(params?.namespace),
184
+ namespace: params?.namespace,
185
+ }),
186
+ getAvailableModules: () => ({
187
+ modules: config.registeredModules ?? config.allowedModules ?? [],
188
+ }),
189
+ getContainerInfo: () => ({
190
+ packageId: config.packageId ?? 'mock_package',
191
+ registeredModules:
192
+ config.registeredModules ?? config.allowedModules ?? [],
193
+ allowedModules: config.allowedModules ?? null,
194
+ adaptation,
195
+ auth: config.auth,
196
+ debugEnabled: true,
197
+ isMockBridge: true,
198
+ ...config.containerInfoExtra,
199
+ }),
200
+ };
201
+
202
+ function isAllowed(namespace: string): boolean {
203
+ if (namespace === 'system') return true;
204
+ if (!config.allowedModules) return true;
205
+ return config.allowedModules.includes(namespace);
206
+ }
207
+
208
+ function resolveImpl(namespace: string, method: string): unknown {
209
+ if (namespace === 'system') return systemModule[method];
210
+ return config.modules?.[namespace]?.[method];
211
+ }
212
+
213
+ // ---- NativeModules 代理 ----
214
+
215
+ const moduleProxyCache = new Map<string, Record<string, unknown>>();
216
+
217
+ function createModuleProxy(namespace: string): Record<string, unknown> {
218
+ const cached = moduleProxyCache.get(namespace);
219
+ if (cached) return cached;
220
+
221
+ const proxy = new Proxy({} as Record<string, unknown>, {
222
+ // module() 用 `NativeModules[ns][method]` 做存在性判断,必须恒为真
223
+ has: () => true,
224
+ get: (_target, method: string | symbol) => {
225
+ if (typeof method !== 'string') return undefined;
226
+ return async (...args: unknown[]) => {
227
+ if (!isAllowed(namespace)) {
228
+ console.error(
229
+ `[Mock Bridge] 模块 "${namespace}" 不在 allowedModules 中,` +
230
+ `调用 ${namespace}.${method} 被拒绝。` +
231
+ '请检查 package.meta.json 的 allowedModules。',
232
+ );
233
+ return {
234
+ __error__: 'MODULE_ACCESS_DENIED',
235
+ __message__: `Module "${namespace}" not allowed`,
236
+ };
237
+ }
238
+
239
+ const impl = resolveImpl(namespace, method);
240
+ const missing = impl === undefined;
241
+ const result = missing
242
+ ? null
243
+ : typeof impl === 'function'
244
+ ? await (impl as (...a: unknown[]) => unknown)(...args)
245
+ : impl;
246
+
247
+ calls.push({
248
+ namespace,
249
+ method,
250
+ args,
251
+ result,
252
+ missing,
253
+ timestamp: Date.now(),
254
+ });
255
+
256
+ if (missing) {
257
+ console.warn(
258
+ `[Mock Bridge] 未定义 ${namespace}.${method},返回 null。` +
259
+ `可在 installMockBridge 的 modules.${namespace}.${method} 中补充。`,
260
+ { args },
261
+ );
262
+ } else if (log) {
263
+ console.log(`[Mock Bridge] ${namespace}.${method}`, {
264
+ args,
265
+ result,
266
+ });
267
+ }
268
+ return result;
269
+ };
270
+ },
271
+ });
272
+
273
+ moduleProxyCache.set(namespace, proxy);
274
+ return proxy;
275
+ }
276
+
277
+ window.NativeModules = new Proxy(
278
+ {} as Record<
279
+ string,
280
+ Record<string, (...args: unknown[]) => Promise<unknown>>
281
+ >,
282
+ {
283
+ has: () => true,
284
+ get: (_target, namespace: string | symbol) => {
285
+ if (typeof namespace !== 'string') return undefined;
286
+ return createModuleProxy(namespace);
287
+ },
288
+ },
289
+ );
290
+
291
+ // ---- __FLUTTER_BRIDGE__ ----
292
+
293
+ window.__FLUTTER_BRIDGE__ = {
294
+ send: async (
295
+ type: string,
296
+ action: string,
297
+ payload: Record<string, unknown>,
298
+ ) => {
299
+ const result = config.onSend?.(type, action, payload) ?? null;
300
+ if (log) {
301
+ console.log(`[Mock Bridge] H5 → ${type}/${action}`, payload);
302
+ }
303
+ return result;
304
+ },
305
+ onMessage: (jsonString: string) => {
306
+ try {
307
+ const message = JSON.parse(jsonString) as BridgeMessage;
308
+ emit(message.type, message.action, message.payload);
309
+ } catch (e) {
310
+ console.error('[Mock Bridge] onMessage 解析失败:', e);
311
+ }
312
+ },
313
+ };
314
+ (window.__FLUTTER_BRIDGE__ as Record<string, unknown>)[MOCK_FLAG] = true;
315
+
316
+ // ---- 窗口尺寸联动 ----
317
+
318
+ const handleResize = () => {
319
+ adaptation = buildAdaptation({
320
+ ...config.adaptation,
321
+ screenWidth: window.innerWidth,
322
+ screenHeight: window.innerHeight,
323
+ scale: undefined,
324
+ });
325
+ emit(
326
+ 'adaptation',
327
+ 'changed',
328
+ adaptation as unknown as Record<string, unknown>,
329
+ );
330
+ };
331
+ const followResize = config.followWindowResize ?? true;
332
+ if (followResize) {
333
+ window.addEventListener('resize', handleResize);
334
+ }
335
+
336
+ // ---- 句柄 ----
337
+
338
+ const handle: MockBridgeHandle = {
339
+ emit,
340
+ setAdaptation(patch) {
341
+ adaptation = buildAdaptation({ ...adaptation, ...patch });
342
+ emit(
343
+ 'adaptation',
344
+ 'changed',
345
+ adaptation as unknown as Record<string, unknown>,
346
+ );
347
+ },
348
+ calls,
349
+ printMissing() {
350
+ const missing = calls.filter((c) => c.missing);
351
+ if (missing.length === 0) {
352
+ console.log('[Mock Bridge] 所有原生调用都已命中 mock 定义');
353
+ return;
354
+ }
355
+ console.table(
356
+ missing.map((c) => ({
357
+ 调用: `${c.namespace}.${c.method}`,
358
+ 参数: JSON.stringify(c.args),
359
+ })),
360
+ );
361
+ },
362
+ uninstall() {
363
+ if (followResize) {
364
+ window.removeEventListener('resize', handleResize);
365
+ }
366
+ delete window.__FLUTTER_BRIDGE__;
367
+ window.NativeModules = previousNativeModules;
368
+ delete window.__MRP_MOCK__;
369
+ },
370
+ };
371
+
372
+ window.__MRP_MOCK__ = handle;
373
+
374
+ // 唤醒可能已结束的就绪轮询,让 whenReady() 立即 resolve
375
+ bridge.refreshReady();
376
+
377
+ console.info(
378
+ `[Mock Bridge] 已安装(packageId=${config.packageId ?? 'mock_package'})。` +
379
+ '控制台可用 window.__MRP_MOCK__ 手工触发事件、查看调用记录。',
380
+ );
381
+
382
+ return handle;
383
+ }
384
+
385
+ /**
386
+ * 仅在非容器环境安装 mock,在真机/App 中自动跳过。
387
+ *
388
+ * 等价于 `installMockBridge`,语义更明确,适合直接写在入口文件里。
389
+ */
390
+ export const setupDevBridge = installMockBridge;