@dimina-kit/devtools 0.4.0-dev.20260612025610 → 0.4.0-dev.20260612152115
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 +4 -4
- package/dist/main/index.bundle.js +63 -12
- package/dist/main/ipc/bridge-router.js +35 -8
- package/dist/main/ipc/views.js +10 -1
- package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
- package/dist/main/services/notifications/renderer-notifier.js +3 -0
- package/dist/main/services/views/view-manager.d.ts +16 -4
- package/dist/main/services/views/view-manager.js +32 -8
- package/dist/main/services/workspace/workspace-service.js +39 -0
- package/dist/preload/index.d.ts +2 -2
- package/dist/preload/index.js +2 -2
- package/dist/preload/windows/host-toolbar-runtime.cjs +11 -1
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +9 -7
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-Bi_6SS-L.js +50 -0
- package/dist/renderer/assets/{input-6fpCXV-n.js → input-DgqX2UvN.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-D22e4dv7.js → ipc-transport-CsFhDJ7q.js} +2 -2
- package/dist/renderer/assets/ipc-transport-SnOAk0Jq.css +1 -0
- package/dist/renderer/assets/{popover-CdGdKkkc.js → popover-D_2o_52M.js} +2 -2
- package/dist/renderer/assets/{select-X0sbjwdP.js → select-BJYsdghn.js} +2 -2
- package/dist/renderer/assets/{settings-CS4jYaCw.js → settings-CivkfizP.js} +2 -2
- package/dist/renderer/assets/{settings-api-CCWTAao_.js → settings-api-7mXkBy5-.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-Bpzaj3bm.js → workbenchSettings-BwpwxuzM.js} +2 -2
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/shared/ipc-channels.d.ts +11 -0
- package/dist/shared/ipc-channels.js +14 -0
- package/dist/simulator/assets/bridge-channels-BUQ5AbvJ.js +2 -0
- package/dist/simulator/assets/device-shell-BEnc2k-W.js +2 -0
- package/dist/simulator/assets/{simulator-DHylZf9Z.js → simulator-cV1PERi6.js} +3 -3
- package/dist/simulator/assets/simulator-mini-app-CYtW6bMd.js +2 -0
- package/dist/simulator/simulator.html +1 -1
- package/package.json +4 -4
- package/dist/renderer/assets/index-DLzjz1wv.js +0 -50
- package/dist/renderer/assets/ipc-transport-Bs8Sf1B2.css +0 -1
- package/dist/simulator/assets/device-shell-fH2cI-3q.js +0 -2
- package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +0 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dimina DevTools
|
|
2
2
|
|
|
3
|
-
基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage 面板、编译配置等功能。
|
|
3
|
+
基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage/编译 面板、编译配置等功能。
|
|
4
4
|
|
|
5
5
|
下游 host 通过 `launch(config)` 集成并定制 devtools(零配置直接 `launch()`,配置驱动 `launch({...})`;见下方「两种用法」)。两种用法都经领域中立的 [`@dimina-kit/electron-deck`](../electron-deck) 框架编排——框架接管 Electron 进程生命周期(whenReady / will-quit)、wire/trust 原语,devtools 作为 `RuntimeBackend` 注入完整运行时(见 [`framework-extraction-v2.md`](../electron-deck/docs/framework-extraction-v2.md))。
|
|
6
6
|
|
|
@@ -147,7 +147,7 @@ src/
|
|
|
147
147
|
main.tsx # 主窗口 React 根
|
|
148
148
|
features/ # 主窗口内的业务区域
|
|
149
149
|
project-runtime/ # 项目视图 + 工具栏 + 右侧面板切换
|
|
150
|
-
right-panel/ # WXML / AppData / Storage 面板
|
|
150
|
+
right-panel/ # WXML / AppData / Storage / 编译 面板
|
|
151
151
|
popover/ settings/ workbench-settings/
|
|
152
152
|
shared/
|
|
153
153
|
components/ # UI 组件(ui / layout / json-viewer / ...)
|
|
@@ -177,7 +177,7 @@ src/
|
|
|
177
177
|
| ------------------ | -------------------- | ------------------- | ---------------------------------- |
|
|
178
178
|
| `appName` | `string` | `'Dimina DevTools'` | 窗口标题 |
|
|
179
179
|
| `adapter` | `CompilationAdapter` | 内置 | 项目编译适配器 |
|
|
180
|
-
| `panels` | `BuiltinPanelId[]` | — |
|
|
180
|
+
| `panels` | `BuiltinPanelId[]` | — | **已废弃,运行时忽略**:界面恒显示全部内置面板(WXML / AppData / Storage / Console / 编译);保留字段仅为兼容仍传它的 host |
|
|
181
181
|
| `preloadPath` | `string` | 内置 | 统一的 host 级 preload 入口;native-host simulator(WCV)自动跑其 `.cjs` sibling(`cjsSiblingPreloadPath`) |
|
|
182
182
|
| `apiNamespaces` | `string[]` | `[]` | 自定义 API 命名空间(如 `['qd']`) |
|
|
183
183
|
| `brandingProvider` | `() => { appName }` | — | 品牌信息 provider |
|
|
@@ -483,7 +483,7 @@ launch({
|
|
|
483
483
|
|
|
484
484
|
## Host Toolbar(宿主自定义工具栏)
|
|
485
485
|
|
|
486
|
-
下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError`
|
|
486
|
+
下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。主进程保留最后一次下发的高度(`views.getHostToolbarHeight()`),项目视图的占位条挂载时会主动拉取并回放——广播器对已上报的高度去重不再重发,没有这一步,冷启动在项目列表期间的上报、以及关闭项目再打开后的高度都会永久丢失(工具栏条塌缩为 0)。
|
|
487
487
|
|
|
488
488
|
### 双向消息:onMessage / send
|
|
489
489
|
|
|
@@ -374,6 +374,10 @@ var ProjectChannel = {
|
|
|
374
374
|
GetCompileConfig: "project:getCompileConfig",
|
|
375
375
|
SaveCompileConfig: "project:saveCompileConfig",
|
|
376
376
|
Status: "project:status",
|
|
377
|
+
// Main → renderer push of per-line dmcc compile logs (devkit `onLog`).
|
|
378
|
+
// Dedicated channel: `project:status` keeps its one-event-per-payload
|
|
379
|
+
// contract (compileEvents), this one carries the line stream (compileLogs).
|
|
380
|
+
CompileLog: "project:compileLog",
|
|
377
381
|
CaptureThumbnail: "project:captureThumbnail",
|
|
378
382
|
GetThumbnail: "project:getThumbnail"
|
|
379
383
|
};
|
|
@@ -440,7 +444,17 @@ var ViewChannel = {
|
|
|
440
444
|
* main → main-window renderer: push the reserved host-toolbar height so the
|
|
441
445
|
* renderer placeholder div resizes (closing the dynamic-height loop).
|
|
442
446
|
*/
|
|
443
|
-
HostToolbarHeightChanged: "view:host-toolbar:height-changed"
|
|
447
|
+
HostToolbarHeightChanged: "view:host-toolbar:height-changed",
|
|
448
|
+
/**
|
|
449
|
+
* main ← main-window renderer (invoke): pull the last NOTIFIED toolbar
|
|
450
|
+
* height retained in main. Mount-time replay companion to
|
|
451
|
+
* `HostToolbarHeightChanged`: the push listener mounts with the project
|
|
452
|
+
* view and the toolbar's size-advertiser deduplicates (never re-reports),
|
|
453
|
+
* so a height pushed while no project view was mounted would otherwise be
|
|
454
|
+
* lost forever (cold start on the project list races it; close-project →
|
|
455
|
+
* reopen hits it deterministically). No payload; resolves a number.
|
|
456
|
+
*/
|
|
457
|
+
HostToolbarGetHeight: "view:host-toolbar:get-height"
|
|
444
458
|
};
|
|
445
459
|
var PopoverChannel = {
|
|
446
460
|
Show: "popover:show",
|
|
@@ -966,6 +980,9 @@ function createRendererNotifier(ctx) {
|
|
|
966
980
|
projectStatus(payload) {
|
|
967
981
|
sendToMain(ProjectChannel.Status, payload);
|
|
968
982
|
},
|
|
983
|
+
compileLog(payload) {
|
|
984
|
+
sendToMain(ProjectChannel.CompileLog, payload);
|
|
985
|
+
},
|
|
969
986
|
windowNavigateBack() {
|
|
970
987
|
sendToMain(WindowChannel.NavigateBack);
|
|
971
988
|
},
|
|
@@ -1841,6 +1858,7 @@ function createViewManager(ctx) {
|
|
|
1841
1858
|
let hostToolbarViewAdded = false;
|
|
1842
1859
|
let hostToolbarRuntimeAcquired = false;
|
|
1843
1860
|
let hostToolbarHeightMode = "auto";
|
|
1861
|
+
let hostToolbarLastHeight = 0;
|
|
1844
1862
|
const hostToolbarPort = createHostToolbarPortChannel({
|
|
1845
1863
|
isCurrent: (wc) => liveHostToolbarWebContents() === wc
|
|
1846
1864
|
});
|
|
@@ -1943,9 +1961,13 @@ function createViewManager(ctx) {
|
|
|
1943
1961
|
}
|
|
1944
1962
|
view.setBounds(bounds);
|
|
1945
1963
|
}
|
|
1964
|
+
function notifyHostToolbarHeight(height) {
|
|
1965
|
+
hostToolbarLastHeight = height;
|
|
1966
|
+
ctx.notify.hostToolbarHeightChanged(height);
|
|
1967
|
+
}
|
|
1946
1968
|
function setHostToolbarHeight(extent) {
|
|
1947
1969
|
if (hostToolbarHeightMode !== "auto") return;
|
|
1948
|
-
|
|
1970
|
+
notifyHostToolbarHeight(extent);
|
|
1949
1971
|
}
|
|
1950
1972
|
function hideHostToolbar() {
|
|
1951
1973
|
if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
@@ -1955,7 +1977,7 @@ function createViewManager(ctx) {
|
|
|
1955
1977
|
}
|
|
1956
1978
|
}
|
|
1957
1979
|
hostToolbarViewAdded = false;
|
|
1958
|
-
|
|
1980
|
+
notifyHostToolbarHeight(0);
|
|
1959
1981
|
}
|
|
1960
1982
|
const hostToolbar = {
|
|
1961
1983
|
async loadURL(url) {
|
|
@@ -1985,7 +2007,7 @@ function createViewManager(ctx) {
|
|
|
1985
2007
|
}
|
|
1986
2008
|
hostToolbarHeightMode = mode;
|
|
1987
2009
|
if (mode !== "auto") {
|
|
1988
|
-
|
|
2010
|
+
notifyHostToolbarHeight(mode.fixed);
|
|
1989
2011
|
}
|
|
1990
2012
|
},
|
|
1991
2013
|
onMessage(channel, handler) {
|
|
@@ -2546,6 +2568,7 @@ function createViewManager(ctx) {
|
|
|
2546
2568
|
return popoverView.webContents.id;
|
|
2547
2569
|
},
|
|
2548
2570
|
getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
|
|
2571
|
+
getHostToolbarHeight: () => hostToolbarLastHeight,
|
|
2549
2572
|
setNativeSimulatorViewBounds,
|
|
2550
2573
|
resize,
|
|
2551
2574
|
setSimulatorDevtoolsBounds,
|
|
@@ -2823,6 +2846,7 @@ function createWorkspaceService(ctx) {
|
|
|
2823
2846
|
let currentSession = null;
|
|
2824
2847
|
let currentProjectPath = "";
|
|
2825
2848
|
let lastClosedProjectPath = "";
|
|
2849
|
+
let logGeneration = 0;
|
|
2826
2850
|
function sendStatus(status2, message, hotReload) {
|
|
2827
2851
|
ctx.notify.projectStatus(hotReload ? { status: status2, message, hotReload: true } : { status: status2, message });
|
|
2828
2852
|
}
|
|
@@ -2871,6 +2895,7 @@ function createWorkspaceService(ctx) {
|
|
|
2871
2895
|
validateProjectDir: async (dirPath) => provider.validateProjectDir ? provider.validateProjectDir(dirPath) : null,
|
|
2872
2896
|
async openProject(projectPath) {
|
|
2873
2897
|
clearSimulatorServicewechatReferer();
|
|
2898
|
+
logGeneration++;
|
|
2874
2899
|
await disposeSession();
|
|
2875
2900
|
currentProjectPath = "";
|
|
2876
2901
|
lastClosedProjectPath = "";
|
|
@@ -2881,6 +2906,7 @@ function createWorkspaceService(ctx) {
|
|
|
2881
2906
|
}
|
|
2882
2907
|
sendStatus("compiling", "\u6B63\u5728\u7F16\u8BD1...");
|
|
2883
2908
|
const { compile } = loadWorkbenchSettings();
|
|
2909
|
+
const sessionGeneration = ++logGeneration;
|
|
2884
2910
|
let session5;
|
|
2885
2911
|
try {
|
|
2886
2912
|
session5 = await ctx.adapter.openProject({
|
|
@@ -2888,7 +2914,21 @@ function createWorkspaceService(ctx) {
|
|
|
2888
2914
|
sourcemap: true,
|
|
2889
2915
|
watch: compile.watch,
|
|
2890
2916
|
onRebuild: () => sendStatus("ready", "\u7F16\u8BD1\u5B8C\u6210\uFF0C\u5DF2\u70ED\u66F4\u65B0", true),
|
|
2891
|
-
onBuildError: (err) => sendStatus("error", String(err))
|
|
2917
|
+
onBuildError: (err) => sendStatus("error", String(err)),
|
|
2918
|
+
// Per-line dmcc log (already filtered in devkit). Stamp the
|
|
2919
|
+
// wall-clock capture time here and push verbatim on the dedicated
|
|
2920
|
+
// compile-log channel — never through projectStatus, whose
|
|
2921
|
+
// one-event-per-payload contract feeds compileEvents. Stale lines
|
|
2922
|
+
// (a closed/replaced session's worker flushing its buffers) are
|
|
2923
|
+
// dropped via the generation check.
|
|
2924
|
+
onLog: (entry) => {
|
|
2925
|
+
if (sessionGeneration !== logGeneration) return;
|
|
2926
|
+
ctx.notify.compileLog({
|
|
2927
|
+
stream: entry.stream,
|
|
2928
|
+
text: entry.text,
|
|
2929
|
+
at: Date.now()
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2892
2932
|
});
|
|
2893
2933
|
} catch (err) {
|
|
2894
2934
|
clearSimulatorServicewechatReferer();
|
|
@@ -2922,6 +2962,7 @@ function createWorkspaceService(ctx) {
|
|
|
2922
2962
|
};
|
|
2923
2963
|
},
|
|
2924
2964
|
async closeProject() {
|
|
2965
|
+
logGeneration++;
|
|
2925
2966
|
clearSimulatorServicewechatReferer();
|
|
2926
2967
|
await disposeSession();
|
|
2927
2968
|
if (currentProjectPath !== "") lastClosedProjectPath = currentProjectPath;
|
|
@@ -4756,12 +4797,16 @@ function summarizeBridgeMsg(payload) {
|
|
|
4756
4797
|
var API_CALL_TIMEOUT_MS = 5e3;
|
|
4757
4798
|
function resolveCurrentApp(state, ctx, appId) {
|
|
4758
4799
|
if (appId) {
|
|
4759
|
-
|
|
4800
|
+
let match;
|
|
4801
|
+
for (const ap of state.appSessions.values()) if (ap.appId === appId) match = ap;
|
|
4802
|
+
if (match) return match;
|
|
4760
4803
|
}
|
|
4761
4804
|
const appInfo = ctx.workspace?.getSession?.()?.appInfo;
|
|
4762
4805
|
const activeAppId = appInfo?.appId;
|
|
4763
4806
|
if (activeAppId) {
|
|
4764
|
-
|
|
4807
|
+
let match;
|
|
4808
|
+
for (const ap of state.appSessions.values()) if (ap.appId === activeAppId) match = ap;
|
|
4809
|
+
if (match) return match;
|
|
4765
4810
|
}
|
|
4766
4811
|
let last;
|
|
4767
4812
|
for (const ap of state.appSessions.values()) last = ap;
|
|
@@ -4778,7 +4823,11 @@ function installBridgeRouter(ctx) {
|
|
|
4778
4823
|
emitRenderEvent: () => {
|
|
4779
4824
|
},
|
|
4780
4825
|
connections: ctx.connections,
|
|
4781
|
-
debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() })
|
|
4826
|
+
debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
|
|
4827
|
+
evictAppDataBridges: (ap) => {
|
|
4828
|
+
if (!ctx.appData) return;
|
|
4829
|
+
for (const page of ap.pages.values()) ctx.appData.evictBridge(ap.appId, page.bridgeId);
|
|
4830
|
+
}
|
|
4782
4831
|
};
|
|
4783
4832
|
const prewarmPoolSize = resolvePrewarmPoolSize();
|
|
4784
4833
|
if (prewarmPoolSize > 0) {
|
|
@@ -4950,9 +4999,6 @@ function installBridgeRouter(ctx) {
|
|
|
4950
4999
|
console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
|
|
4951
5000
|
return;
|
|
4952
5001
|
}
|
|
4953
|
-
if (ctx.appData) {
|
|
4954
|
-
for (const page of target.pages.values()) ctx.appData.evictBridge(target.appId, page.bridgeId);
|
|
4955
|
-
}
|
|
4956
5002
|
void disposeAppSession(state, target.appSessionId);
|
|
4957
5003
|
};
|
|
4958
5004
|
ipcMain3.on(BRIDGE_CHANNELS.DISPOSE, onDispose);
|
|
@@ -5135,6 +5181,10 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
5135
5181
|
};
|
|
5136
5182
|
appSession.onServiceClosed = onServiceClosed;
|
|
5137
5183
|
serviceWindow.once("closed", onServiceClosed);
|
|
5184
|
+
const onSimulatorDestroyed = () => {
|
|
5185
|
+
void disposeAppSession(state, appSessionId);
|
|
5186
|
+
};
|
|
5187
|
+
simulatorWc.once("destroyed", onSimulatorDestroyed);
|
|
5138
5188
|
if (usedPool) {
|
|
5139
5189
|
const bootOnServiceLoad = () => {
|
|
5140
5190
|
if (serviceWindow.isDestroyed()) return;
|
|
@@ -5639,6 +5689,7 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
5639
5689
|
const ap = state.appSessions.get(appSessionId);
|
|
5640
5690
|
if (!ap) return;
|
|
5641
5691
|
state.appSessions.delete(appSessionId);
|
|
5692
|
+
state.evictAppDataBridges(ap);
|
|
5642
5693
|
for (const [requestId, pending] of state.pendingApiCalls) {
|
|
5643
5694
|
if (pending.appSessionId !== appSessionId) continue;
|
|
5644
5695
|
clearTimeout(pending.timer);
|
|
@@ -5804,7 +5855,7 @@ function registerViewsIpc(ctx) {
|
|
|
5804
5855
|
args
|
|
5805
5856
|
);
|
|
5806
5857
|
ctx.views.setHostToolbarBounds(bounds);
|
|
5807
|
-
});
|
|
5858
|
+
}).handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
|
|
5808
5859
|
const onAdvertiseHeight = (event, ...args) => {
|
|
5809
5860
|
if (event.sender.id !== ctx.views.getHostToolbarWebContentsId()) return;
|
|
5810
5861
|
let extent;
|
|
@@ -58,17 +58,26 @@ const API_CALL_TIMEOUT_MS = 5_000;
|
|
|
58
58
|
* rather than assuming a single session.
|
|
59
59
|
*/
|
|
60
60
|
function resolveCurrentApp(state, ctx, appId) {
|
|
61
|
+
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
62
|
+
// order): after a respawn/reopen the newest session is the live one — the
|
|
63
|
+
// first match could be a just-superseded session mid-teardown.
|
|
61
64
|
if (appId) {
|
|
65
|
+
let match;
|
|
62
66
|
for (const ap of state.appSessions.values())
|
|
63
67
|
if (ap.appId === appId)
|
|
64
|
-
|
|
68
|
+
match = ap;
|
|
69
|
+
if (match)
|
|
70
|
+
return match;
|
|
65
71
|
}
|
|
66
72
|
const appInfo = ctx.workspace?.getSession?.()?.appInfo;
|
|
67
73
|
const activeAppId = appInfo?.appId;
|
|
68
74
|
if (activeAppId) {
|
|
75
|
+
let match;
|
|
69
76
|
for (const ap of state.appSessions.values())
|
|
70
77
|
if (ap.appId === activeAppId)
|
|
71
|
-
|
|
78
|
+
match = ap;
|
|
79
|
+
if (match)
|
|
80
|
+
return match;
|
|
72
81
|
}
|
|
73
82
|
// Maps preserve insertion order; the last entry is the most recent spawn.
|
|
74
83
|
let last;
|
|
@@ -87,6 +96,12 @@ export function installBridgeRouter(ctx) {
|
|
|
87
96
|
emitRenderEvent: () => { },
|
|
88
97
|
connections: ctx.connections,
|
|
89
98
|
debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
|
|
99
|
+
evictAppDataBridges: (ap) => {
|
|
100
|
+
if (!ctx.appData)
|
|
101
|
+
return;
|
|
102
|
+
for (const page of ap.pages.values())
|
|
103
|
+
ctx.appData.evictBridge(ap.appId, page.bridgeId);
|
|
104
|
+
},
|
|
90
105
|
};
|
|
91
106
|
// Opt-in (default OFF) pre-warm pool for service-host windows. When enabled,
|
|
92
107
|
// handleSpawn acquires a warm window instead of constructing one per spawn.
|
|
@@ -298,12 +313,8 @@ export function installBridgeRouter(ctx) {
|
|
|
298
313
|
console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
|
|
299
314
|
return;
|
|
300
315
|
}
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
if (ctx.appData) {
|
|
304
|
-
for (const page of target.pages.values())
|
|
305
|
-
ctx.appData.evictBridge(target.appId, page.bridgeId);
|
|
306
|
-
}
|
|
316
|
+
// AppData bridge eviction happens inside disposeAppSession (single
|
|
317
|
+
// chokepoint shared with the simulator-WCV 'destroyed' path).
|
|
307
318
|
void disposeAppSession(state, target.appSessionId);
|
|
308
319
|
};
|
|
309
320
|
ipcMain.on(C.DISPOSE, onDispose);
|
|
@@ -517,6 +528,19 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
517
528
|
};
|
|
518
529
|
appSession.onServiceClosed = onServiceClosed;
|
|
519
530
|
serviceWindow.once('closed', onServiceClosed);
|
|
531
|
+
// The simulator WCV owns the app's UI lifetime. When it is destroyed —
|
|
532
|
+
// project close (`views.disposeAll`/detach) or a DeviceShell respawn
|
|
533
|
+
// (`attachNativeSimulator`, e.g. watcher hot reload) — the guest never gets
|
|
534
|
+
// to send its graceful `C.DISPOSE`, so without this hook the app session and
|
|
535
|
+
// its hidden service-host window leak, and `resolveCurrentApp` keeps
|
|
536
|
+
// resolving the STALE session for the same appId: `getActiveRenderWc` then
|
|
537
|
+
// dereferences dead pages and every panel pull (WXML/elements) returns null
|
|
538
|
+
// for the whole next session. Disposing here is idempotent with the graceful
|
|
539
|
+
// path (`disposeAppSession` early-returns once the session is gone).
|
|
540
|
+
const onSimulatorDestroyed = () => {
|
|
541
|
+
void disposeAppSession(state, appSessionId);
|
|
542
|
+
};
|
|
543
|
+
simulatorWc.once('destroyed', onSimulatorDestroyed);
|
|
520
544
|
if (usedPool) {
|
|
521
545
|
// A pooled/fallback window passes through about:blank (warm load or the
|
|
522
546
|
// fallback's initial load). Boot only once the REAL service.html navigation
|
|
@@ -1144,6 +1168,9 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1144
1168
|
if (!ap)
|
|
1145
1169
|
return;
|
|
1146
1170
|
state.appSessions.delete(appSessionId);
|
|
1171
|
+
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1172
|
+
// page teardown below progressively empties (and finally clears).
|
|
1173
|
+
state.evictAppDataBridges(ap);
|
|
1147
1174
|
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1148
1175
|
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1149
1176
|
// subscriptions (e.g. audioListen) live with their timer cleared until
|
package/dist/main/ipc/views.js
CHANGED
|
@@ -30,7 +30,16 @@ export function registerViewsIpc(ctx) {
|
|
|
30
30
|
.handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
|
|
31
31
|
const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
|
|
32
32
|
ctx.views.setHostToolbarBounds(bounds);
|
|
33
|
-
})
|
|
33
|
+
})
|
|
34
|
+
// Height replay pull: a freshly-mounted main-renderer placeholder asks for
|
|
35
|
+
// the last NOTIFIED toolbar height (main retains it — the toolbar's
|
|
36
|
+
// size-advertiser deduplicates and never re-pushes, so a push that fired
|
|
37
|
+
// while no project view was mounted is otherwise lost: cold start races
|
|
38
|
+
// it, close-project → reopen hits it always). Rides the SAME
|
|
39
|
+
// senderPolicy-gated registry as HostToolbarBounds: the toolbar WCV's
|
|
40
|
+
// arbitrary host content must not reach this — only the trusted main
|
|
41
|
+
// renderer pulls. Live delegation, not a registration-time snapshot.
|
|
42
|
+
.handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
|
|
34
43
|
// Reverse size-advertiser: the toolbar WCV's OWN renderer sends this, and the
|
|
35
44
|
// host loads ARBITRARY content into that WCV. We DELIBERATELY do NOT add the
|
|
36
45
|
// toolbar wc to the global sender policy — that would trust it for ALL ~72
|
|
@@ -12,6 +12,15 @@ export interface ProjectStatusPayload {
|
|
|
12
12
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
13
13
|
hotReload?: boolean;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Payload for the `project:compileLog` push — one filtered dmcc log line.
|
|
17
|
+
* `at` is stamped in the main process when the line is captured.
|
|
18
|
+
*/
|
|
19
|
+
export interface CompileLogPayload {
|
|
20
|
+
at: number;
|
|
21
|
+
stream: 'stdout' | 'stderr';
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
15
24
|
/**
|
|
16
25
|
* Payload for the `settings:init` event sent into the embedded settings overlay
|
|
17
26
|
* right after it is shown.
|
|
@@ -43,6 +52,8 @@ export interface WorkbenchSettingsInitPayload {
|
|
|
43
52
|
export interface RendererNotifier {
|
|
44
53
|
/** Broadcast project compile status transitions to the main renderer. */
|
|
45
54
|
projectStatus(payload: ProjectStatusPayload): void;
|
|
55
|
+
/** Push one per-line dmcc compile-log entry to the main renderer. */
|
|
56
|
+
compileLog(payload: CompileLogPayload): void;
|
|
46
57
|
/** Ask the main renderer to navigate back to its landing screen. */
|
|
47
58
|
windowNavigateBack(): void;
|
|
48
59
|
/** Tell the main renderer the compile popover has been closed. */
|
|
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
|
|
|
24
24
|
projectStatus(payload) {
|
|
25
25
|
sendToMain(ProjectChannel.Status, payload);
|
|
26
26
|
},
|
|
27
|
+
compileLog(payload) {
|
|
28
|
+
sendToMain(ProjectChannel.CompileLog, payload);
|
|
29
|
+
},
|
|
27
30
|
windowNavigateBack() {
|
|
28
31
|
sendToMain(WindowChannel.NavigateBack);
|
|
29
32
|
},
|
|
@@ -112,6 +112,16 @@ export interface ViewManager {
|
|
|
112
112
|
* must trust its id — see `createWorkbenchSenderPolicy`.
|
|
113
113
|
*/
|
|
114
114
|
getHostToolbarWebContentsId(): number | null;
|
|
115
|
+
/**
|
|
116
|
+
* Return the last host-toolbar height NOTIFIED to the main-window renderer
|
|
117
|
+
* (an advertiser report in `'auto'` mode, a `setHeightMode({ fixed })` pin,
|
|
118
|
+
* or 0 after `hostToolbar.hide()`); 0 before any notify. The renderer pulls
|
|
119
|
+
* this on project-view mount to REPLAY a height whose push it missed — the
|
|
120
|
+
* toolbar's size-advertiser deduplicates and never re-reports, so a notify
|
|
121
|
+
* fired while no project view is mounted (cold start on the project list;
|
|
122
|
+
* always on close-project → reopen) would otherwise be lost forever.
|
|
123
|
+
*/
|
|
124
|
+
getHostToolbarHeight(): number;
|
|
115
125
|
/**
|
|
116
126
|
* NATIVE-HOST ONLY. Position the simulator content WebContentsView over the
|
|
117
127
|
* renderer-measured simulator panel REGION rect (the flex:1 placeholder slot,
|
|
@@ -168,10 +178,12 @@ export interface ViewManager {
|
|
|
168
178
|
}): void;
|
|
169
179
|
/**
|
|
170
180
|
* Reverse size-advertiser sink: the toolbar WCV's own renderer advertises
|
|
171
|
-
* its intrinsic content height (block-axis extent); we
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
181
|
+
* its intrinsic content height (block-axis extent); we retain it as the
|
|
182
|
+
* last-notified height (`getHostToolbarHeight`) and push it to the
|
|
183
|
+
* main-window renderer so the placeholder div resizes (closing the
|
|
184
|
+
* dynamic-height loop). Ignored ENTIRELY while a `{ fixed }` height mode is
|
|
185
|
+
* pinned via `hostToolbar.setHeightMode` — dropped reports neither notify
|
|
186
|
+
* nor touch the retained value (the session-resident advertiser always
|
|
175
187
|
* runs, so its reports must not fight a host-pinned height).
|
|
176
188
|
*/
|
|
177
189
|
setHostToolbarHeight(extent: number): void;
|
|
@@ -95,6 +95,13 @@ export function createViewManager(ctx) {
|
|
|
95
95
|
// Placeholder height authority: 'auto' = advertiser reports forward to the
|
|
96
96
|
// renderer; { fixed } = host-pinned, advertiser reports are dropped.
|
|
97
97
|
let hostToolbarHeightMode = 'auto';
|
|
98
|
+
// Last toolbar height NOTIFIED to the main-window renderer — the replay
|
|
99
|
+
// source behind `getHostToolbarHeight()`. Updated ONLY inside
|
|
100
|
+
// `notifyHostToolbarHeight` so the retained value can never diverge from
|
|
101
|
+
// what the renderer was told (an advertiser report dropped by a `{ fixed }`
|
|
102
|
+
// pin must not pollute it, and a setHeightMode validation reject leaves it
|
|
103
|
+
// untouched).
|
|
104
|
+
let hostToolbarLastHeight = 0;
|
|
98
105
|
// Gated narrow channel to the toolbar PAGE (per-load MessagePort handshake;
|
|
99
106
|
// see host-toolbar-port-channel.ts). Control-level registry — created with
|
|
100
107
|
// the manager so onMessage() works before any toolbar view exists.
|
|
@@ -248,18 +255,30 @@ export function createViewManager(ctx) {
|
|
|
248
255
|
}
|
|
249
256
|
view.setBounds(bounds);
|
|
250
257
|
}
|
|
258
|
+
// Single funnel for the height notify: retain-then-push, so the retained
|
|
259
|
+
// value is exactly the last value the renderer was told. Every height
|
|
260
|
+
// notify site MUST go through here — the renderer pulls the retained value
|
|
261
|
+
// on project-view mount to replay a push it missed (the toolbar's
|
|
262
|
+
// size-advertiser deduplicates and never re-reports).
|
|
263
|
+
function notifyHostToolbarHeight(height) {
|
|
264
|
+
hostToolbarLastHeight = height;
|
|
265
|
+
ctx.notify.hostToolbarHeightChanged(height);
|
|
266
|
+
}
|
|
251
267
|
function setHostToolbarHeight(extent) {
|
|
252
268
|
// While the host pins a fixed height, drop advertiser reports entirely —
|
|
253
269
|
// the session-resident advertiser is always installed, so forwarding its
|
|
254
270
|
// reports would make the strip oscillate between the pinned and measured
|
|
255
|
-
// heights on every content resize.
|
|
271
|
+
// heights on every content resize. Dropped reports must not touch the
|
|
272
|
+
// retained value either: retention records what was NOTIFIED, not what
|
|
273
|
+
// was reported.
|
|
256
274
|
if (hostToolbarHeightMode !== 'auto')
|
|
257
275
|
return;
|
|
258
276
|
// Push the reserved height back to the main-window renderer so its
|
|
259
|
-
// placeholder div resizes (closing the dynamic-height loop). The
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
277
|
+
// placeholder div resizes (closing the dynamic-height loop). The notified
|
|
278
|
+
// height IS retained in main (`getHostToolbarHeight`) so a renderer that
|
|
279
|
+
// mounts later can pull/replay it; the renderer placeholder remains the
|
|
280
|
+
// geometry authority — the forward anchor re-reports bounds from it.
|
|
281
|
+
notifyHostToolbarHeight(extent);
|
|
263
282
|
}
|
|
264
283
|
function hideHostToolbar() {
|
|
265
284
|
if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
@@ -273,7 +292,9 @@ export function createViewManager(ctx) {
|
|
|
273
292
|
// non-zero reserved height and re-publishes bounds on the next window
|
|
274
293
|
// resize, silently re-adding the view we just hid (unstable hide). Zeroing
|
|
275
294
|
// the height flips the anchor to `present:false` so it stops re-publishing.
|
|
276
|
-
|
|
295
|
+
// Through the funnel so the retained value follows to 0 — a renderer
|
|
296
|
+
// mounting after the hide must replay 0, not the stale pre-hide height.
|
|
297
|
+
notifyHostToolbarHeight(0);
|
|
277
298
|
}
|
|
278
299
|
const hostToolbar = {
|
|
279
300
|
async loadURL(url) {
|
|
@@ -317,11 +338,13 @@ export function createViewManager(ctx) {
|
|
|
317
338
|
if (mode !== 'auto') {
|
|
318
339
|
// Pin immediately: a preload-less/static toolbar never advertises, so
|
|
319
340
|
// waiting for the next report would leave the strip at height 0.
|
|
320
|
-
|
|
341
|
+
notifyHostToolbarHeight(mode.fixed);
|
|
321
342
|
}
|
|
322
343
|
// Switching back to 'auto' deliberately does NOT synthesize a notify —
|
|
323
344
|
// replaying a stale cached height would flash the old size; the NEXT
|
|
324
|
-
// advertiser report drives the placeholder again.
|
|
345
|
+
// advertiser report drives the placeholder again. The RETAINED value
|
|
346
|
+
// survives the switch though: a freshly-mounting renderer still needs
|
|
347
|
+
// the pinned height until that next report lands.
|
|
325
348
|
},
|
|
326
349
|
onMessage(channel, handler) {
|
|
327
350
|
return hostToolbarPort.onMessage(channel, handler);
|
|
@@ -1146,6 +1169,7 @@ export function createViewManager(ctx) {
|
|
|
1146
1169
|
return popoverView.webContents.id;
|
|
1147
1170
|
},
|
|
1148
1171
|
getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
|
|
1172
|
+
getHostToolbarHeight: () => hostToolbarLastHeight,
|
|
1149
1173
|
setNativeSimulatorViewBounds,
|
|
1150
1174
|
resize,
|
|
1151
1175
|
setSimulatorDevtoolsBounds,
|
|
@@ -18,6 +18,16 @@ export function createWorkspaceService(ctx) {
|
|
|
18
18
|
// accept an in-flight write that targets the just-closed project. Reset at
|
|
19
19
|
// the start of every `openProject` so it never accumulates a stale root.
|
|
20
20
|
let lastClosedProjectPath = '';
|
|
21
|
+
// Session generation for the onLog closure handed to the adapter: a closed
|
|
22
|
+
// (or switched-away-from) session's compile worker dies asynchronously, so
|
|
23
|
+
// buffered log lines can still arrive through the OLD closure after
|
|
24
|
+
// closeProject/openProject. Each openProject claims a new generation and
|
|
25
|
+
// closeProject invalidates the current one; a stale closure sees the
|
|
26
|
+
// mismatch and drops the line instead of polluting the next project's
|
|
27
|
+
// compile panel. (Deliberately NOT a `currentSession !== null` check: the
|
|
28
|
+
// first compile's log lines arrive while the adapter promise is still
|
|
29
|
+
// pending, before currentSession is assigned.)
|
|
30
|
+
let logGeneration = 0;
|
|
21
31
|
function sendStatus(status, message, hotReload) {
|
|
22
32
|
ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
|
|
23
33
|
}
|
|
@@ -71,6 +81,14 @@ export function createWorkspaceService(ctx) {
|
|
|
71
81
|
: null,
|
|
72
82
|
async openProject(projectPath) {
|
|
73
83
|
clearSimulatorServicewechatReferer();
|
|
84
|
+
// Invalidate the outgoing session's onLog BEFORE teardown starts (same
|
|
85
|
+
// order as closeProject below): the dying compile worker flushes
|
|
86
|
+
// buffered lines DURING disposeSession(), and with the old generation
|
|
87
|
+
// still current they would pass the staleness guard and pollute the
|
|
88
|
+
// incoming project's compile-log timeline. The new session claims its
|
|
89
|
+
// own fresh generation further down (after dispose), so its onLog
|
|
90
|
+
// forwards normally.
|
|
91
|
+
logGeneration++;
|
|
74
92
|
await disposeSession();
|
|
75
93
|
currentProjectPath = '';
|
|
76
94
|
// A fresh open starts a clean sandbox window — drop any previously
|
|
@@ -92,6 +110,9 @@ export function createWorkspaceService(ctx) {
|
|
|
92
110
|
}
|
|
93
111
|
sendStatus('compiling', '正在编译...');
|
|
94
112
|
const { compile } = loadWorkbenchSettings();
|
|
113
|
+
// This open owns the log channel until the next open/close bumps the
|
|
114
|
+
// generation — the onLog closure below checks it per line.
|
|
115
|
+
const sessionGeneration = ++logGeneration;
|
|
95
116
|
let session;
|
|
96
117
|
try {
|
|
97
118
|
session = await ctx.adapter.openProject({
|
|
@@ -100,6 +121,21 @@ export function createWorkspaceService(ctx) {
|
|
|
100
121
|
watch: compile.watch,
|
|
101
122
|
onRebuild: () => sendStatus('ready', '编译完成,已热更新', true),
|
|
102
123
|
onBuildError: (err) => sendStatus('error', String(err)),
|
|
124
|
+
// Per-line dmcc log (already filtered in devkit). Stamp the
|
|
125
|
+
// wall-clock capture time here and push verbatim on the dedicated
|
|
126
|
+
// compile-log channel — never through projectStatus, whose
|
|
127
|
+
// one-event-per-payload contract feeds compileEvents. Stale lines
|
|
128
|
+
// (a closed/replaced session's worker flushing its buffers) are
|
|
129
|
+
// dropped via the generation check.
|
|
130
|
+
onLog: (entry) => {
|
|
131
|
+
if (sessionGeneration !== logGeneration)
|
|
132
|
+
return;
|
|
133
|
+
ctx.notify.compileLog({
|
|
134
|
+
stream: entry.stream,
|
|
135
|
+
text: entry.text,
|
|
136
|
+
at: Date.now(),
|
|
137
|
+
});
|
|
138
|
+
},
|
|
103
139
|
});
|
|
104
140
|
}
|
|
105
141
|
catch (err) {
|
|
@@ -141,6 +177,9 @@ export function createWorkspaceService(ctx) {
|
|
|
141
177
|
};
|
|
142
178
|
},
|
|
143
179
|
async closeProject() {
|
|
180
|
+
// Invalidate the active session's onLog BEFORE teardown starts — lines
|
|
181
|
+
// flushed by the dying compile worker must not reach the panel.
|
|
182
|
+
logGeneration++;
|
|
144
183
|
clearSimulatorServicewechatReferer();
|
|
145
184
|
await disposeSession();
|
|
146
185
|
// Record the root being torn down BEFORE clearing it, so a teardown/
|
package/dist/preload/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { installConsoleInstrumentation } from './instrumentation/console.js';
|
|
|
13
13
|
export { createAppDataSource } from './instrumentation/app-data.js';
|
|
14
14
|
export type { AppDataSnapshot } from './instrumentation/app-data.js';
|
|
15
15
|
/**
|
|
16
|
-
* @deprecated Will be removed in
|
|
16
|
+
* @deprecated Will be removed in 0.5.0. Under the native-host (sole)
|
|
17
17
|
* runtime the simulator top frame is a top-level WebContentsView — the page DOM
|
|
18
18
|
* lives in child render-host `<webview>` guests, so this top-frame DOM observer
|
|
19
19
|
* only ever publishes `null`. Panel WXML flows over the main-process
|
|
@@ -21,7 +21,7 @@ export type { AppDataSnapshot } from './instrumentation/app-data.js';
|
|
|
21
21
|
*/
|
|
22
22
|
export { createWxmlSource } from './instrumentation/wxml.js';
|
|
23
23
|
/**
|
|
24
|
-
* @deprecated Will be removed in
|
|
24
|
+
* @deprecated Will be removed in 0.5.0. The host's
|
|
25
25
|
* `miniapp-snapshot:push/pull` IPC has no receiving end under the native-host
|
|
26
26
|
* (sole) runtime: the simulator is a top-level WebContentsView without an
|
|
27
27
|
* embedder (its `sendToHost` fires into the void) and the renderer-side puller
|
package/dist/preload/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export { installConsoleInstrumentation } from './instrumentation/console.js';
|
|
13
13
|
export { createAppDataSource } from './instrumentation/app-data.js';
|
|
14
14
|
/**
|
|
15
|
-
* @deprecated Will be removed in
|
|
15
|
+
* @deprecated Will be removed in 0.5.0. Under the native-host (sole)
|
|
16
16
|
* runtime the simulator top frame is a top-level WebContentsView — the page DOM
|
|
17
17
|
* lives in child render-host `<webview>` guests, so this top-frame DOM observer
|
|
18
18
|
* only ever publishes `null`. Panel WXML flows over the main-process
|
|
@@ -20,7 +20,7 @@ export { createAppDataSource } from './instrumentation/app-data.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export { createWxmlSource } from './instrumentation/wxml.js';
|
|
22
22
|
/**
|
|
23
|
-
* @deprecated Will be removed in
|
|
23
|
+
* @deprecated Will be removed in 0.5.0. The host's
|
|
24
24
|
* `miniapp-snapshot:push/pull` IPC has no receiving end under the native-host
|
|
25
25
|
* (sole) runtime: the simulator is a top-level WebContentsView without an
|
|
26
26
|
* embedder (its `sendToHost` fires into the void) and the renderer-side puller
|
|
@@ -134,7 +134,17 @@ var ViewChannel = {
|
|
|
134
134
|
* main → main-window renderer: push the reserved host-toolbar height so the
|
|
135
135
|
* renderer placeholder div resizes (closing the dynamic-height loop).
|
|
136
136
|
*/
|
|
137
|
-
HostToolbarHeightChanged: "view:host-toolbar:height-changed"
|
|
137
|
+
HostToolbarHeightChanged: "view:host-toolbar:height-changed",
|
|
138
|
+
/**
|
|
139
|
+
* main ← main-window renderer (invoke): pull the last NOTIFIED toolbar
|
|
140
|
+
* height retained in main. Mount-time replay companion to
|
|
141
|
+
* `HostToolbarHeightChanged`: the push listener mounts with the project
|
|
142
|
+
* view and the toolbar's size-advertiser deduplicates (never re-reports),
|
|
143
|
+
* so a height pushed while no project view was mounted would otherwise be
|
|
144
|
+
* lost forever (cold start on the project list races it; close-project →
|
|
145
|
+
* reopen hits it deterministically). No payload; resolves a number.
|
|
146
|
+
*/
|
|
147
|
+
HostToolbarGetHeight: "view:host-toolbar:get-height"
|
|
138
148
|
};
|
|
139
149
|
|
|
140
150
|
// src/preload/runtime/host-toolbar-advertiser.ts
|