@dimina-kit/devtools 0.4.0-dev.20260728095034 → 0.4.0-dev.20260729062524
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 +68 -0
- package/dist/main/api.d.ts +1 -0
- package/dist/main/app/app.d.ts +3 -0
- package/dist/main/app/app.js +8 -0
- package/dist/main/index.bundle.js +185 -7
- package/dist/main/ipc/bridge-router.js +1 -2
- package/dist/main/services/simulator/ui-extensions.d.ts +13 -0
- package/dist/main/services/simulator/ui-extensions.js +166 -0
- package/dist/main/services/views/native-simulator-view.js +7 -0
- package/dist/main/services/views/view-manager.d.ts +5 -0
- package/dist/main/services/workbench-context.d.ts +6 -0
- package/dist/main/services/workbench-context.js +3 -0
- package/dist/preload/runtime/native-host.d.ts +8 -0
- package/dist/preload/runtime/native-host.js +2 -0
- package/dist/preload/windows/main.cjs +0 -96
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +1 -0
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +1 -0
- package/dist/render-host/preload.cjs +18 -0
- package/dist/renderer/assets/{index-BJlFbXHb.js → index-BBg3YI5b.js} +2 -2
- package/dist/renderer/entries/main/index.html +1 -1
- package/dist/shared/bridge-channels.d.ts +5 -2
- package/dist/shared/simulator-ui.d.ts +61 -0
- package/dist/shared/simulator-ui.js +12 -0
- package/dist/shared/types.d.ts +7 -0
- package/dist/simulator/assets/device-shell-BHz1v3Yo.css +1 -0
- package/dist/simulator/assets/device-shell-CH_DMYaz.js +2 -0
- package/dist/simulator/assets/{jsx-runtime-BDTY6fEq.js → jsx-runtime-hBv-rHqQ.js} +2 -2
- package/dist/simulator/assets/{simulator-BG-5CADK.js → simulator-DXraYerh.js} +6 -6
- package/dist/simulator/assets/{simulator-mini-app-DzfMfnVM.js → simulator-mini-app-w1Bhwkld.js} +2 -2
- package/dist/simulator/simulator.html +2 -2
- package/package.json +9 -5
- package/dist/simulator/assets/device-shell-Cs-rmP1f.js +0 -2
- package/dist/simulator/assets/device-shell-DOy53Y0s.css +0 -1
package/README.md
CHANGED
|
@@ -53,6 +53,13 @@ launch({
|
|
|
53
53
|
// simulator 自定义 API:per-context
|
|
54
54
|
instance.registerSimulatorApi('login', (params) => myLogin(params))
|
|
55
55
|
|
|
56
|
+
// simulator 下游 UI:框架管理 DeviceShell 挂载点与生命周期
|
|
57
|
+
const nativeUi = instance.registerSimulatorUiExtension({
|
|
58
|
+
id: 'my.native-ui',
|
|
59
|
+
rendererScriptPath: '/absolute/path/to/native-ui.js',
|
|
60
|
+
})
|
|
61
|
+
instance.registerSimulatorApi('share', params => nativeUi.invoke('share', params))
|
|
62
|
+
|
|
56
63
|
// 自定义 IPC:经 gated 的 IpcRegistry,不再裸 ipcMain.handle
|
|
57
64
|
instance.ipc.handle('my:action', () => collectStats())
|
|
58
65
|
|
|
@@ -207,6 +214,7 @@ src/
|
|
|
207
214
|
| 方法 | 说明 |
|
|
208
215
|
| --- | --- |
|
|
209
216
|
| `instance.registerSimulatorApi(name, handler)` | 注册 simulator 自定义 API,小程序里 `wx.<name>()` 调用(详见下方"Simulator 自定义 API")。返回 `Disposable` |
|
|
217
|
+
| `instance.registerSimulatorUiExtension({ id, rendererScriptPath })` | 注册可信 renderer bundle。框架负责注入当前 simulator、稳定 Overlay Root、soft reload 的 current/pending 切换和销毁;返回可 `invoke()` 的句柄 |
|
|
210
218
|
| `instance.ipc` | `IpcRegistry` 实例,`instance.ipc.handle(channel, fn)` 注册自定义 IPC;已绑定 `senderPolicy` 网关 |
|
|
211
219
|
| `instance.registerTrustedWindow(win)` | 把 host 自有弹窗 `BrowserWindow` 加入受信 sender 集,否则其发起的 `instance.ipc` 调用会被网关拒绝。窗口关闭即移除 |
|
|
212
220
|
|
|
@@ -482,6 +490,66 @@ launch({
|
|
|
482
490
|
|
|
483
491
|
---
|
|
484
492
|
|
|
493
|
+
## Simulator UI 扩展
|
|
494
|
+
|
|
495
|
+
需要分享面板、授权弹窗等产品 UI 的下游 host,应同时使用两个正交接口:
|
|
496
|
+
|
|
497
|
+
- `registerSimulatorApi` 定义小程序侧 `wx.*` 命令。
|
|
498
|
+
- `registerSimulatorUiExtension` 安装下游 renderer bundle,并通过返回句柄把命令送到 UI。
|
|
499
|
+
|
|
500
|
+
主进程只注册绝对路径指向的可信构建产物:
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const nativeUi = instance.registerSimulatorUiExtension({
|
|
504
|
+
id: 'my.native-ui',
|
|
505
|
+
rendererScriptPath: fileURLToPath(
|
|
506
|
+
new URL('../renderer/native-ui.js', import.meta.url),
|
|
507
|
+
),
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
instance.registerSimulatorApi('share', params =>
|
|
511
|
+
nativeUi.invoke('share', params),
|
|
512
|
+
)
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
renderer bundle 使用 `@dimina-kit/devtools/simulator-ui` 注册实现:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { registerSimulatorUiExtension } from '@dimina-kit/devtools/simulator-ui'
|
|
519
|
+
|
|
520
|
+
let runtime
|
|
521
|
+
|
|
522
|
+
registerSimulatorUiExtension({
|
|
523
|
+
id: 'my.native-ui',
|
|
524
|
+
mount({ overlayRoot, appId, signal }) {
|
|
525
|
+
runtime = createNativeUi({ root: overlayRoot, appId })
|
|
526
|
+
signal.addEventListener('abort', () => runtime.destroy(), { once: true })
|
|
527
|
+
return () => runtime.destroy()
|
|
528
|
+
},
|
|
529
|
+
invoke(method, params) {
|
|
530
|
+
return runtime.invoke(method, params)
|
|
531
|
+
},
|
|
532
|
+
onChromeAction(action) {
|
|
533
|
+
if (action.name !== 'capsule.more') return false
|
|
534
|
+
runtime.showMoreMenu(action)
|
|
535
|
+
return true
|
|
536
|
+
},
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
契约边界:
|
|
541
|
+
|
|
542
|
+
- 框架只提供设备内的稳定 Overlay Root,不接收 React Component,因而不绑定下游 React 版本。
|
|
543
|
+
- 只有可见的 current `DeviceShell` 持有挂载点;soft reload 提升 pending shell 时,旧 mount 先收到 abort/cleanup,再在新 root 上 mount。
|
|
544
|
+
- Overlay Root 默认不拦截页面点击;扩展实际创建的交互层需要设置 `pointer-events: auto`。
|
|
545
|
+
- 胶囊菜单通过语义动作 `capsule.more` 分发,不应查询 `.menu-capsule__more` 或拦截 DOM click。
|
|
546
|
+
- `id` 在一个 context 内唯一;script 必须用相同 `id` 注册 renderer 实现,否则安装失败。
|
|
547
|
+
- 每次 simulator 顶层文档完成加载时都会重新读取 `rendererScriptPath`,因此开发态重建
|
|
548
|
+
bundle 后执行硬 reload 即可加载新代码;soft reload 只切换 DeviceShell 挂载点,不重复执行 bundle。
|
|
549
|
+
- `dispose()` 会注销 renderer 实现;context 销毁时自动执行。`invoke()` 参数和返回值必须可跨 Electron IPC/`executeJavaScript` 序列化。
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
485
553
|
## Host Toolbar(宿主自定义工具栏)
|
|
486
554
|
|
|
487
555
|
下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。主进程保留最后一次下发的高度(`views.getHostToolbarHeight()`),项目视图的占位条挂载时会主动拉取并回放——广播器对已上报的高度去重不再重发,没有这一步,冷启动在项目列表期间的上报、以及关闭项目再打开后的高度都会永久丢失(工具栏条塌缩为 0)。
|
package/dist/main/api.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type { ViewManager } from './services/views/view-manager.js';
|
|
|
11
11
|
export type { WorkspaceService } from './services/workspace/workspace-service.js';
|
|
12
12
|
export type { Project, ProjectPages, ProjectSettings } from './services/projects/project-repository.js';
|
|
13
13
|
export type { SimulatorApiHandler } from './services/simulator/custom-apis.js';
|
|
14
|
+
export type { SimulatorUiExtensionHandle, SimulatorUiExtensionRegistration, } from '../shared/simulator-ui.js';
|
|
14
15
|
export { rendererDir, defaultPreloadPath, simulatorDir, getRendererDir, getPreloadDir, getRendererHtml, } from './utils/paths.js';
|
|
15
16
|
export { IpcRegistry } from './utils/ipc-registry.js';
|
|
16
17
|
export type { SenderPolicy } from './utils/ipc-registry.js';
|
package/dist/main/app/app.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BrowserWindow } from 'electron';
|
|
2
2
|
import type { WorkbenchAppConfig } from '../../shared/types.js';
|
|
3
|
+
import type { SimulatorUiExtensionHandle, SimulatorUiExtensionRegistration } from '../../shared/simulator-ui.js';
|
|
3
4
|
import type { SimulatorApiHandler } from '../services/simulator/custom-apis.js';
|
|
4
5
|
import { type WorkbenchContext } from '../services/workbench-context.js';
|
|
5
6
|
import { type AutomationServer } from '../services/automation/index.js';
|
|
@@ -15,6 +16,8 @@ export interface WorkbenchAppInstance {
|
|
|
15
16
|
registerTrustedWindow: (win: BrowserWindow) => Disposable;
|
|
16
17
|
/** Registers a simulator custom API into this context's registry. */
|
|
17
18
|
registerSimulatorApi: (name: string, handler: SimulatorApiHandler) => Disposable;
|
|
19
|
+
/** Registers a downstream renderer extension for the simulator device UI. */
|
|
20
|
+
registerSimulatorUiExtension: (registration: SimulatorUiExtensionRegistration) => SimulatorUiExtensionHandle;
|
|
18
21
|
automationServer?: AutomationServer;
|
|
19
22
|
updateManager?: UpdateManager;
|
|
20
23
|
dispose: () => Promise<void>;
|
package/dist/main/app/app.js
CHANGED
|
@@ -476,6 +476,14 @@ onInstanceCreated) {
|
|
|
476
476
|
// teardown, so a single dispose leaves no dead entry behind.
|
|
477
477
|
registerTrustedWindow: (win) => context.registry.add(registerTrustedWindow(context, win)),
|
|
478
478
|
registerSimulatorApi: (name, handler) => context.registry.add(toDisposable(context.simulatorApis.register(name, handler))),
|
|
479
|
+
registerSimulatorUiExtension: (registration) => {
|
|
480
|
+
const extension = context.simulatorUiExtensions.register(registration);
|
|
481
|
+
const owned = context.registry.add(extension);
|
|
482
|
+
return {
|
|
483
|
+
dispose: () => owned.dispose(),
|
|
484
|
+
invoke: (method, params) => extension.invoke(method, params),
|
|
485
|
+
};
|
|
486
|
+
},
|
|
479
487
|
dispose: () => disposeContext(context),
|
|
480
488
|
};
|
|
481
489
|
onInstanceCreated?.(instance);
|
|
@@ -5305,6 +5305,7 @@ function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
5305
5305
|
}
|
|
5306
5306
|
function tearDownNativeSimulatorView(label) {
|
|
5307
5307
|
if (!nativeSimulatorView) return;
|
|
5308
|
+
ctx.simulatorUiExtensions?.detach(nativeSimulatorView.webContents);
|
|
5308
5309
|
if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
5309
5310
|
try {
|
|
5310
5311
|
ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
|
|
@@ -5381,6 +5382,7 @@ function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
5381
5382
|
nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
|
|
5382
5383
|
view.setBackgroundColor(simDeskBg());
|
|
5383
5384
|
const simWc = view.webContents;
|
|
5385
|
+
ctx.simulatorUiExtensions?.attach(simWc);
|
|
5384
5386
|
const syncDeskBg = () => {
|
|
5385
5387
|
try {
|
|
5386
5388
|
if (!simWc.isDestroyed()) view.setBackgroundColor(simDeskBg());
|
|
@@ -6192,6 +6194,173 @@ function createLocalProjectsProvider() {
|
|
|
6192
6194
|
};
|
|
6193
6195
|
}
|
|
6194
6196
|
|
|
6197
|
+
// src/main/services/simulator/ui-extensions.ts
|
|
6198
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
6199
|
+
import { isAbsolute } from "node:path";
|
|
6200
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
6201
|
+
var EXTENSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
6202
|
+
function createSimulatorUiExtensionRegistry(options = {}) {
|
|
6203
|
+
const loadSource = options.loadSource ?? ((scriptPath) => readFile2(scriptPath, "utf8"));
|
|
6204
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6205
|
+
let active = null;
|
|
6206
|
+
function assertRegistration(registration) {
|
|
6207
|
+
if (!EXTENSION_ID_RE.test(registration.id)) {
|
|
6208
|
+
throw new Error(`Invalid simulator UI extension id: "${registration.id}"`);
|
|
6209
|
+
}
|
|
6210
|
+
if (!isAbsolute(registration.rendererScriptPath)) {
|
|
6211
|
+
throw new Error("Simulator UI extension rendererScriptPath must be absolute");
|
|
6212
|
+
}
|
|
6213
|
+
if (entries.has(registration.id)) {
|
|
6214
|
+
throw new Error(`Simulator UI extension "${registration.id}" is already registered`);
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
function activeExpression(method, args) {
|
|
6218
|
+
return `globalThis.__diminaSimulatorUi.${method}(${args.map((arg) => JSON.stringify(arg)).join(",")})`;
|
|
6219
|
+
}
|
|
6220
|
+
async function install(entry, target) {
|
|
6221
|
+
const { id, rendererScriptPath } = entry.registration;
|
|
6222
|
+
if (target.loaded.has(id)) return;
|
|
6223
|
+
const pending = target.loading.get(id);
|
|
6224
|
+
if (pending) return await pending;
|
|
6225
|
+
const generation = target.generation;
|
|
6226
|
+
const task = (async () => {
|
|
6227
|
+
const source = await loadSource(rendererScriptPath);
|
|
6228
|
+
if (entries.get(id) !== entry || active !== target || target.webContents.isDestroyed() || !target.ready || target.generation !== generation) {
|
|
6229
|
+
throw new Error("Simulator UI extension target is no longer available");
|
|
6230
|
+
}
|
|
6231
|
+
const sourceUrl = pathToFileURL2(rendererScriptPath).href;
|
|
6232
|
+
const wrapped = `(()=>{
|
|
6233
|
+
${source}
|
|
6234
|
+
;return undefined
|
|
6235
|
+
})()
|
|
6236
|
+
//# sourceURL=${sourceUrl}`;
|
|
6237
|
+
await target.webContents.executeJavaScript(wrapped, true);
|
|
6238
|
+
if (entries.get(id) !== entry || active !== target || target.webContents.isDestroyed() || !target.ready || target.generation !== generation) {
|
|
6239
|
+
throw new Error("Simulator UI extension target was destroyed during installation");
|
|
6240
|
+
}
|
|
6241
|
+
const registered = await target.webContents.executeJavaScript(
|
|
6242
|
+
`Boolean(globalThis.__diminaSimulatorUi?.has(${JSON.stringify(id)}))`,
|
|
6243
|
+
true
|
|
6244
|
+
);
|
|
6245
|
+
if (!registered) {
|
|
6246
|
+
throw new Error(
|
|
6247
|
+
`Simulator UI extension "${id}" did not register its renderer implementation`
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
target.loaded.add(id);
|
|
6251
|
+
})().finally(() => {
|
|
6252
|
+
if (target.loading.get(id) === task) target.loading.delete(id);
|
|
6253
|
+
});
|
|
6254
|
+
target.loading.set(id, task);
|
|
6255
|
+
return await task;
|
|
6256
|
+
}
|
|
6257
|
+
async function installAll(target) {
|
|
6258
|
+
for (const entry of entries.values()) {
|
|
6259
|
+
try {
|
|
6260
|
+
await install(entry, target);
|
|
6261
|
+
} catch (error) {
|
|
6262
|
+
console.warn(
|
|
6263
|
+
`[simulator-ui] failed to install "${entry.registration.id}":`,
|
|
6264
|
+
error instanceof Error ? error.message : String(error)
|
|
6265
|
+
);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
function uninstall(id, target = active) {
|
|
6270
|
+
if (!target) return;
|
|
6271
|
+
target.loaded.delete(id);
|
|
6272
|
+
target.loading.delete(id);
|
|
6273
|
+
if (!target.ready || target.webContents.isDestroyed()) return;
|
|
6274
|
+
void target.webContents.executeJavaScript(
|
|
6275
|
+
`globalThis.__diminaSimulatorUi?.unregister(${JSON.stringify(id)})`,
|
|
6276
|
+
true
|
|
6277
|
+
).catch(() => {
|
|
6278
|
+
});
|
|
6279
|
+
}
|
|
6280
|
+
function detach(webContents4) {
|
|
6281
|
+
const target = active;
|
|
6282
|
+
if (!target || webContents4 && target.webContents !== webContents4) return;
|
|
6283
|
+
active = null;
|
|
6284
|
+
if (!target.webContents.isDestroyed()) {
|
|
6285
|
+
target.webContents.removeListener("did-finish-load", target.onLoaded);
|
|
6286
|
+
}
|
|
6287
|
+
target.loaded.clear();
|
|
6288
|
+
target.loading.clear();
|
|
6289
|
+
}
|
|
6290
|
+
function attach(webContents4) {
|
|
6291
|
+
detach();
|
|
6292
|
+
const target = {
|
|
6293
|
+
webContents: webContents4,
|
|
6294
|
+
ready: false,
|
|
6295
|
+
generation: 0,
|
|
6296
|
+
loaded: /* @__PURE__ */ new Set(),
|
|
6297
|
+
loading: /* @__PURE__ */ new Map(),
|
|
6298
|
+
onLoaded: () => {
|
|
6299
|
+
}
|
|
6300
|
+
};
|
|
6301
|
+
target.onLoaded = () => {
|
|
6302
|
+
if (active !== target || webContents4.isDestroyed()) return;
|
|
6303
|
+
target.ready = true;
|
|
6304
|
+
target.generation += 1;
|
|
6305
|
+
target.loaded.clear();
|
|
6306
|
+
target.loading.clear();
|
|
6307
|
+
void installAll(target);
|
|
6308
|
+
};
|
|
6309
|
+
active = target;
|
|
6310
|
+
webContents4.on("did-finish-load", target.onLoaded);
|
|
6311
|
+
}
|
|
6312
|
+
function register(registration) {
|
|
6313
|
+
assertRegistration(registration);
|
|
6314
|
+
const stableRegistration = { ...registration };
|
|
6315
|
+
const entry = {
|
|
6316
|
+
registration: stableRegistration
|
|
6317
|
+
};
|
|
6318
|
+
entries.set(stableRegistration.id, entry);
|
|
6319
|
+
const target = active;
|
|
6320
|
+
if (target?.ready) {
|
|
6321
|
+
void install(entry, target).catch((error) => {
|
|
6322
|
+
console.warn(
|
|
6323
|
+
`[simulator-ui] failed to install "${stableRegistration.id}":`,
|
|
6324
|
+
error instanceof Error ? error.message : String(error)
|
|
6325
|
+
);
|
|
6326
|
+
});
|
|
6327
|
+
}
|
|
6328
|
+
let disposed = false;
|
|
6329
|
+
return {
|
|
6330
|
+
dispose() {
|
|
6331
|
+
if (disposed) return;
|
|
6332
|
+
disposed = true;
|
|
6333
|
+
if (entries.get(stableRegistration.id) !== entry) return;
|
|
6334
|
+
entries.delete(stableRegistration.id);
|
|
6335
|
+
uninstall(stableRegistration.id);
|
|
6336
|
+
},
|
|
6337
|
+
async invoke(method, params) {
|
|
6338
|
+
if (disposed || entries.get(stableRegistration.id) !== entry) {
|
|
6339
|
+
throw new Error(`Simulator UI extension "${stableRegistration.id}" is disposed`);
|
|
6340
|
+
}
|
|
6341
|
+
const current = active;
|
|
6342
|
+
if (!current || current.webContents.isDestroyed() || !current.ready) {
|
|
6343
|
+
throw new Error("Dimina simulator UI is not ready");
|
|
6344
|
+
}
|
|
6345
|
+
await install(entry, current);
|
|
6346
|
+
if (active !== current || current.webContents.isDestroyed()) {
|
|
6347
|
+
throw new Error("Dimina simulator UI was replaced during invocation");
|
|
6348
|
+
}
|
|
6349
|
+
return await current.webContents.executeJavaScript(
|
|
6350
|
+
activeExpression("invoke", [stableRegistration.id, method, params ?? null]),
|
|
6351
|
+
true
|
|
6352
|
+
);
|
|
6353
|
+
}
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6356
|
+
function clear() {
|
|
6357
|
+
for (const id of entries.keys()) uninstall(id);
|
|
6358
|
+
entries.clear();
|
|
6359
|
+
detach();
|
|
6360
|
+
}
|
|
6361
|
+
return { register, attach, detach, clear };
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6195
6364
|
// src/main/services/projects/templates.ts
|
|
6196
6365
|
function resolveTemplates(builtin, injected, mode) {
|
|
6197
6366
|
let kept;
|
|
@@ -6253,6 +6422,8 @@ function createWorkbenchContext(opts) {
|
|
|
6253
6422
|
ctx.registry.add(() => ctx.cdpSessionBroker.dispose());
|
|
6254
6423
|
ctx.trustedWindowSenderIds = /* @__PURE__ */ new Map();
|
|
6255
6424
|
ctx.simulatorApis = createSimulatorApiRegistry();
|
|
6425
|
+
ctx.simulatorUiExtensions = createSimulatorUiExtensionRegistry();
|
|
6426
|
+
ctx.registry.add(() => ctx.simulatorUiExtensions.clear());
|
|
6256
6427
|
ctx.windows = createWindowService(opts.mainWindow);
|
|
6257
6428
|
ctx.views = createViewManager(ctx);
|
|
6258
6429
|
ctx.registry.add(() => ctx.views.disposeAll());
|
|
@@ -6919,7 +7090,7 @@ import { DisposableRegistry as DisposableRegistry5 } from "@dimina-kit/electron-
|
|
|
6919
7090
|
// src/main/ipc/bridge-router.ts
|
|
6920
7091
|
import { app as app12, ipcMain as ipcMain3, protocol as protocol2, session as electronSession, webContents as webContents3 } from "electron";
|
|
6921
7092
|
import path17 from "node:path";
|
|
6922
|
-
import { pathToFileURL as
|
|
7093
|
+
import { pathToFileURL as pathToFileURL4 } from "node:url";
|
|
6923
7094
|
|
|
6924
7095
|
// src/shared/request-core.ts
|
|
6925
7096
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
@@ -7073,7 +7244,7 @@ function setCorsHeaders(res) {
|
|
|
7073
7244
|
// src/main/windows/service-host-window/create.ts
|
|
7074
7245
|
import { app as app10, BrowserWindow as BrowserWindow5 } from "electron";
|
|
7075
7246
|
import path16 from "node:path";
|
|
7076
|
-
import { pathToFileURL as
|
|
7247
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
7077
7248
|
var SERVICE_HOST_PARTITION = SHARED_MINIAPP_PARTITION;
|
|
7078
7249
|
var serviceHostPreloadPath = path16.join(devtoolsPackageRoot, "dist/service-host/preload.cjs");
|
|
7079
7250
|
var serviceHostHtmlPath = path16.join(devtoolsPackageRoot, "dist/service-host/service.html");
|
|
@@ -7096,7 +7267,7 @@ function constructServiceHostWindow(opts = {}) {
|
|
|
7096
7267
|
});
|
|
7097
7268
|
}
|
|
7098
7269
|
function buildServiceHostSpawnUrl(opts) {
|
|
7099
|
-
const url = new URL(
|
|
7270
|
+
const url = new URL(pathToFileURL3(serviceHostHtmlPath).toString());
|
|
7100
7271
|
url.searchParams.set("bridgeId", opts.bridgeId);
|
|
7101
7272
|
url.searchParams.set("appId", opts.appId);
|
|
7102
7273
|
url.searchParams.set("pagePath", opts.pagePath);
|
|
@@ -8297,8 +8468,8 @@ function installBridgeRouter(ctx) {
|
|
|
8297
8468
|
const onNativeHostQuery = (event) => {
|
|
8298
8469
|
const reply = {
|
|
8299
8470
|
enabled: true,
|
|
8300
|
-
renderHostHtmlUrl:
|
|
8301
|
-
renderPreloadUrl:
|
|
8471
|
+
renderHostHtmlUrl: pathToFileURL4(path17.join(devtoolsPackageRoot, "dist/render-host/pageFrame.html")).toString(),
|
|
8472
|
+
renderPreloadUrl: pathToFileURL4(path17.join(devtoolsPackageRoot, "dist/render-host/preload.cjs")).toString(),
|
|
8302
8473
|
device: currentDevice ?? void 0
|
|
8303
8474
|
};
|
|
8304
8475
|
event.returnValue = reply;
|
|
@@ -9618,8 +9789,7 @@ function resolveRootPagePath(manifest, requestedPagePath) {
|
|
|
9618
9789
|
function resolvePageWindowConfig(appConfig, pagePath) {
|
|
9619
9790
|
const normalized = normalizePagePath(pagePath);
|
|
9620
9791
|
const appWindow = appConfig.app?.window ?? {};
|
|
9621
|
-
const
|
|
9622
|
-
const pageWindow = pageEntry?.window ?? {};
|
|
9792
|
+
const pageWindow = appConfig.modules?.[normalized] ?? {};
|
|
9623
9793
|
return {
|
|
9624
9794
|
navigationBarTitleText: pageWindow.navigationBarTitleText ?? appWindow.navigationBarTitleText ?? "",
|
|
9625
9795
|
navigationBarBackgroundColor: pageWindow.navigationBarBackgroundColor ?? appWindow.navigationBarBackgroundColor ?? "#ffffff",
|
|
@@ -13169,6 +13339,14 @@ async function createDevtoolsRuntime(config = {}, onInstanceCreated) {
|
|
|
13169
13339
|
// teardown, so a single dispose leaves no dead entry behind.
|
|
13170
13340
|
registerTrustedWindow: (win) => context.registry.add(registerTrustedWindow(context, win)),
|
|
13171
13341
|
registerSimulatorApi: (name, handler) => context.registry.add(toDisposable8(context.simulatorApis.register(name, handler))),
|
|
13342
|
+
registerSimulatorUiExtension: (registration) => {
|
|
13343
|
+
const extension = context.simulatorUiExtensions.register(registration);
|
|
13344
|
+
const owned = context.registry.add(extension);
|
|
13345
|
+
return {
|
|
13346
|
+
dispose: () => owned.dispose(),
|
|
13347
|
+
invoke: (method, params) => extension.invoke(method, params)
|
|
13348
|
+
};
|
|
13349
|
+
},
|
|
13172
13350
|
dispose: () => disposeContext(context)
|
|
13173
13351
|
};
|
|
13174
13352
|
onInstanceCreated?.(instance);
|
|
@@ -2095,8 +2095,7 @@ function resolveRootPagePath(manifest, requestedPagePath) {
|
|
|
2095
2095
|
function resolvePageWindowConfig(appConfig, pagePath) {
|
|
2096
2096
|
const normalized = normalizePagePath(pagePath);
|
|
2097
2097
|
const appWindow = appConfig.app?.window ?? {};
|
|
2098
|
-
const
|
|
2099
|
-
const pageWindow = pageEntry?.window ?? {};
|
|
2098
|
+
const pageWindow = appConfig.modules?.[normalized] ?? {};
|
|
2100
2099
|
return {
|
|
2101
2100
|
navigationBarTitleText: pageWindow.navigationBarTitleText ?? appWindow.navigationBarTitleText ?? '',
|
|
2102
2101
|
navigationBarBackgroundColor: pageWindow.navigationBarBackgroundColor ?? appWindow.navigationBarBackgroundColor ?? '#ffffff',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { SimulatorUiExtensionHandle, SimulatorUiExtensionRegistration } from '../../../shared/simulator-ui.js';
|
|
3
|
+
export interface SimulatorUiExtensionRegistry {
|
|
4
|
+
register(registration: SimulatorUiExtensionRegistration): SimulatorUiExtensionHandle;
|
|
5
|
+
attach(webContents: WebContents): void;
|
|
6
|
+
detach(webContents?: WebContents): void;
|
|
7
|
+
clear(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface SimulatorUiExtensionRegistryOptions {
|
|
10
|
+
loadSource?: (scriptPath: string) => Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSimulatorUiExtensionRegistry(options?: SimulatorUiExtensionRegistryOptions): SimulatorUiExtensionRegistry;
|
|
13
|
+
//# sourceMappingURL=ui-extensions.d.ts.map
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
const EXTENSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
|
+
export function createSimulatorUiExtensionRegistry(options = {}) {
|
|
6
|
+
const loadSource = options.loadSource ?? (scriptPath => readFile(scriptPath, 'utf8'));
|
|
7
|
+
const entries = new Map();
|
|
8
|
+
let active = null;
|
|
9
|
+
function assertRegistration(registration) {
|
|
10
|
+
if (!EXTENSION_ID_RE.test(registration.id)) {
|
|
11
|
+
throw new Error(`Invalid simulator UI extension id: "${registration.id}"`);
|
|
12
|
+
}
|
|
13
|
+
if (!isAbsolute(registration.rendererScriptPath)) {
|
|
14
|
+
throw new Error('Simulator UI extension rendererScriptPath must be absolute');
|
|
15
|
+
}
|
|
16
|
+
if (entries.has(registration.id)) {
|
|
17
|
+
throw new Error(`Simulator UI extension "${registration.id}" is already registered`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function activeExpression(method, args) {
|
|
21
|
+
return `globalThis.__diminaSimulatorUi.${method}(${args.map(arg => JSON.stringify(arg)).join(',')})`;
|
|
22
|
+
}
|
|
23
|
+
async function install(entry, target) {
|
|
24
|
+
const { id, rendererScriptPath } = entry.registration;
|
|
25
|
+
if (target.loaded.has(id))
|
|
26
|
+
return;
|
|
27
|
+
const pending = target.loading.get(id);
|
|
28
|
+
if (pending)
|
|
29
|
+
return await pending;
|
|
30
|
+
const generation = target.generation;
|
|
31
|
+
const task = (async () => {
|
|
32
|
+
// Read once per simulator document generation so downstream watch builds
|
|
33
|
+
// become visible after a hard reload without restarting the workbench.
|
|
34
|
+
const source = await loadSource(rendererScriptPath);
|
|
35
|
+
if (entries.get(id) !== entry
|
|
36
|
+
||
|
|
37
|
+
active !== target
|
|
38
|
+
|| target.webContents.isDestroyed()
|
|
39
|
+
|| !target.ready
|
|
40
|
+
|| target.generation !== generation) {
|
|
41
|
+
throw new Error('Simulator UI extension target is no longer available');
|
|
42
|
+
}
|
|
43
|
+
const sourceUrl = pathToFileURL(rendererScriptPath).href;
|
|
44
|
+
const wrapped = `(()=>{\n${source}\n;return undefined\n})()\n//# sourceURL=${sourceUrl}`;
|
|
45
|
+
await target.webContents.executeJavaScript(wrapped, true);
|
|
46
|
+
if (entries.get(id) !== entry
|
|
47
|
+
||
|
|
48
|
+
active !== target
|
|
49
|
+
|| target.webContents.isDestroyed()
|
|
50
|
+
|| !target.ready
|
|
51
|
+
|| target.generation !== generation) {
|
|
52
|
+
throw new Error('Simulator UI extension target was destroyed during installation');
|
|
53
|
+
}
|
|
54
|
+
const registered = await target.webContents.executeJavaScript(`Boolean(globalThis.__diminaSimulatorUi?.has(${JSON.stringify(id)}))`, true);
|
|
55
|
+
if (!registered) {
|
|
56
|
+
throw new Error(`Simulator UI extension "${id}" did not register its renderer implementation`);
|
|
57
|
+
}
|
|
58
|
+
target.loaded.add(id);
|
|
59
|
+
})().finally(() => {
|
|
60
|
+
if (target.loading.get(id) === task)
|
|
61
|
+
target.loading.delete(id);
|
|
62
|
+
});
|
|
63
|
+
target.loading.set(id, task);
|
|
64
|
+
return await task;
|
|
65
|
+
}
|
|
66
|
+
async function installAll(target) {
|
|
67
|
+
for (const entry of entries.values()) {
|
|
68
|
+
try {
|
|
69
|
+
await install(entry, target);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.warn(`[simulator-ui] failed to install "${entry.registration.id}":`, error instanceof Error ? error.message : String(error));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function uninstall(id, target = active) {
|
|
77
|
+
if (!target)
|
|
78
|
+
return;
|
|
79
|
+
target.loaded.delete(id);
|
|
80
|
+
target.loading.delete(id);
|
|
81
|
+
if (!target.ready || target.webContents.isDestroyed())
|
|
82
|
+
return;
|
|
83
|
+
void target.webContents.executeJavaScript(`globalThis.__diminaSimulatorUi?.unregister(${JSON.stringify(id)})`, true).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
function detach(webContents) {
|
|
86
|
+
const target = active;
|
|
87
|
+
if (!target || (webContents && target.webContents !== webContents))
|
|
88
|
+
return;
|
|
89
|
+
active = null;
|
|
90
|
+
if (!target.webContents.isDestroyed()) {
|
|
91
|
+
target.webContents.removeListener('did-finish-load', target.onLoaded);
|
|
92
|
+
}
|
|
93
|
+
target.loaded.clear();
|
|
94
|
+
target.loading.clear();
|
|
95
|
+
}
|
|
96
|
+
function attach(webContents) {
|
|
97
|
+
detach();
|
|
98
|
+
const target = {
|
|
99
|
+
webContents,
|
|
100
|
+
ready: false,
|
|
101
|
+
generation: 0,
|
|
102
|
+
loaded: new Set(),
|
|
103
|
+
loading: new Map(),
|
|
104
|
+
onLoaded: () => { },
|
|
105
|
+
};
|
|
106
|
+
target.onLoaded = () => {
|
|
107
|
+
if (active !== target || webContents.isDestroyed())
|
|
108
|
+
return;
|
|
109
|
+
target.ready = true;
|
|
110
|
+
target.generation += 1;
|
|
111
|
+
target.loaded.clear();
|
|
112
|
+
target.loading.clear();
|
|
113
|
+
void installAll(target);
|
|
114
|
+
};
|
|
115
|
+
active = target;
|
|
116
|
+
webContents.on('did-finish-load', target.onLoaded);
|
|
117
|
+
}
|
|
118
|
+
function register(registration) {
|
|
119
|
+
assertRegistration(registration);
|
|
120
|
+
const stableRegistration = { ...registration };
|
|
121
|
+
const entry = {
|
|
122
|
+
registration: stableRegistration,
|
|
123
|
+
};
|
|
124
|
+
entries.set(stableRegistration.id, entry);
|
|
125
|
+
const target = active;
|
|
126
|
+
if (target?.ready) {
|
|
127
|
+
void install(entry, target).catch((error) => {
|
|
128
|
+
console.warn(`[simulator-ui] failed to install "${stableRegistration.id}":`, error instanceof Error ? error.message : String(error));
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
let disposed = false;
|
|
132
|
+
return {
|
|
133
|
+
dispose() {
|
|
134
|
+
if (disposed)
|
|
135
|
+
return;
|
|
136
|
+
disposed = true;
|
|
137
|
+
if (entries.get(stableRegistration.id) !== entry)
|
|
138
|
+
return;
|
|
139
|
+
entries.delete(stableRegistration.id);
|
|
140
|
+
uninstall(stableRegistration.id);
|
|
141
|
+
},
|
|
142
|
+
async invoke(method, params) {
|
|
143
|
+
if (disposed || entries.get(stableRegistration.id) !== entry) {
|
|
144
|
+
throw new Error(`Simulator UI extension "${stableRegistration.id}" is disposed`);
|
|
145
|
+
}
|
|
146
|
+
const current = active;
|
|
147
|
+
if (!current || current.webContents.isDestroyed() || !current.ready) {
|
|
148
|
+
throw new Error('Dimina simulator UI is not ready');
|
|
149
|
+
}
|
|
150
|
+
await install(entry, current);
|
|
151
|
+
if (active !== current || current.webContents.isDestroyed()) {
|
|
152
|
+
throw new Error('Dimina simulator UI was replaced during invocation');
|
|
153
|
+
}
|
|
154
|
+
return await current.webContents.executeJavaScript(activeExpression('invoke', [stableRegistration.id, method, params ?? null]), true);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function clear() {
|
|
159
|
+
for (const id of entries.keys())
|
|
160
|
+
uninstall(id);
|
|
161
|
+
entries.clear();
|
|
162
|
+
detach();
|
|
163
|
+
}
|
|
164
|
+
return { register, attach, detach, clear };
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=ui-extensions.js.map
|
|
@@ -70,6 +70,7 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
70
70
|
function tearDownNativeSimulatorView(label) {
|
|
71
71
|
if (!nativeSimulatorView)
|
|
72
72
|
return;
|
|
73
|
+
ctx.simulatorUiExtensions?.detach(nativeSimulatorView.webContents);
|
|
73
74
|
if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
74
75
|
try {
|
|
75
76
|
ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
|
|
@@ -219,6 +220,8 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
219
220
|
// and the renderer placeholder behind it are all the same color.
|
|
220
221
|
view.setBackgroundColor(simDeskBg());
|
|
221
222
|
const simWc = view.webContents;
|
|
223
|
+
// Keep downstream UI bundles bound to this WCV until shared teardown.
|
|
224
|
+
ctx.simulatorUiExtensions?.attach(simWc);
|
|
222
225
|
// Keep the WCV surface in sync with the active color scheme. The
|
|
223
226
|
// process-wide installThemeBackgroundSync() re-syncs BrowserWindows on a
|
|
224
227
|
// theme switch, but this top-level WebContentsView is not a window, so its
|
|
@@ -248,6 +251,10 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
248
251
|
// URL in will-attach (where `params.src` carries the full URL) and consumed
|
|
249
252
|
// FIFO in the matching did-attach — `guestWc.getURL()` is still empty there.
|
|
250
253
|
// Per-attach scope: a fresh simWc + handlers are built on every (re)attach.
|
|
254
|
+
// (The guest's `bgColor` query param — WeChat/Android/Harmony white-flash
|
|
255
|
+
// parity — is consumed entirely outside main: device-shell.tsx's `<webview>`
|
|
256
|
+
// CSS background and render-host/preload.cjs both read it directly, since
|
|
257
|
+
// `WebContents` has no `setBackgroundColor` for main to call here.)
|
|
251
258
|
const pendingGuestIsTab = [];
|
|
252
259
|
simWc.on('will-attach-webview', (_event, webPreferences, params) => {
|
|
253
260
|
;
|
|
@@ -64,6 +64,11 @@ export interface ViewManagerContext {
|
|
|
64
64
|
* Optional so partial test contexts compile.
|
|
65
65
|
*/
|
|
66
66
|
simulatorApis?: WorkbenchContext['simulatorApis'];
|
|
67
|
+
/**
|
|
68
|
+
* Host-registered UI extensions that follow the native simulator WCV.
|
|
69
|
+
* Optional only for focused view-manager test doubles.
|
|
70
|
+
*/
|
|
71
|
+
simulatorUiExtensions?: WorkbenchContext['simulatorUiExtensions'];
|
|
67
72
|
}
|
|
68
73
|
/**
|
|
69
74
|
* Unified lifecycle manager for Electron WebContentsView overlays.
|
|
@@ -18,6 +18,7 @@ import { type ViewManager } from './views/view-manager.js';
|
|
|
18
18
|
import { type WindowService } from './window-service.js';
|
|
19
19
|
import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
20
20
|
import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
|
|
21
|
+
import { type SimulatorUiExtensionRegistry } from './simulator/ui-extensions.js';
|
|
21
22
|
import { sanitizeTemplates } from './projects/templates.js';
|
|
22
23
|
import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
|
|
23
24
|
import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
|
|
@@ -123,6 +124,11 @@ export interface WorkbenchContext {
|
|
|
123
124
|
* One registry per context — no process-global crosstalk.
|
|
124
125
|
*/
|
|
125
126
|
simulatorApis: SimulatorApiRegistry;
|
|
127
|
+
/**
|
|
128
|
+
* Per-context downstream simulator UI extensions. It owns renderer bundle
|
|
129
|
+
* installation and follows the active native simulator WebContentsView.
|
|
130
|
+
*/
|
|
131
|
+
simulatorUiExtensions: SimulatorUiExtensionRegistry;
|
|
126
132
|
/** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
|
|
127
133
|
registry: DisposableRegistry;
|
|
128
134
|
/**
|
|
@@ -12,6 +12,7 @@ import { openSettingsWindow } from '../windows/settings-window/index.js';
|
|
|
12
12
|
import { createWorkspaceService, } from './workspace/workspace-service.js';
|
|
13
13
|
import { createLocalProjectsProvider } from './projects/local-provider.js';
|
|
14
14
|
import { createSimulatorApiRegistry, } from './simulator/custom-apis.js';
|
|
15
|
+
import { createSimulatorUiExtensionRegistry, } from './simulator/ui-extensions.js';
|
|
15
16
|
import { resolveTemplates, sanitizeTemplates } from './projects/templates.js';
|
|
16
17
|
import { BUILTIN_TEMPLATES } from './projects/builtin-templates.js';
|
|
17
18
|
export function createWorkbenchContext(opts) {
|
|
@@ -34,6 +35,8 @@ export function createWorkbenchContext(opts) {
|
|
|
34
35
|
ctx.registry.add(() => ctx.cdpSessionBroker.dispose());
|
|
35
36
|
ctx.trustedWindowSenderIds = new Map();
|
|
36
37
|
ctx.simulatorApis = createSimulatorApiRegistry();
|
|
38
|
+
ctx.simulatorUiExtensions = createSimulatorUiExtensionRegistry();
|
|
39
|
+
ctx.registry.add(() => ctx.simulatorUiExtensions.clear());
|
|
37
40
|
ctx.windows = createWindowService(opts.mainWindow);
|
|
38
41
|
ctx.views = createViewManager(ctx);
|
|
39
42
|
// Full view teardown belongs to the CONTEXT's life, not a project's:
|