@dimina-kit/devtools 0.4.0-dev.20260701155140 → 0.4.0-dev.20260702145840
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/main/app/max-listeners-diagnostic.js +5 -1
- package/dist/main/index.bundle.js +1532 -1236
- package/dist/main/ipc/bridge-router.d.ts +28 -0
- package/dist/main/ipc/bridge-router.js +175 -50
- package/dist/main/ipc/session-listener-bag.d.ts +41 -0
- package/dist/main/ipc/session-listener-bag.js +40 -0
- package/dist/main/ipc/simulator.js +5 -1
- package/dist/main/services/elements-forward/index.js +19 -0
- package/dist/main/services/network-forward/index.js +13 -0
- package/dist/main/services/views/destroy-child-view.d.ts +9 -0
- package/dist/main/services/views/destroy-child-view.js +23 -0
- package/dist/main/services/views/host-toolbar-view.d.ts +21 -0
- package/dist/main/services/views/host-toolbar-view.js +249 -0
- package/dist/main/services/views/inject-when-ready.d.ts +41 -0
- package/dist/main/services/views/inject-when-ready.js +62 -0
- package/dist/main/services/views/native-simulator-devtools-host.d.ts +26 -0
- package/dist/main/services/views/native-simulator-devtools-host.js +484 -0
- package/dist/main/services/views/native-simulator-view.d.ts +28 -0
- package/dist/main/services/views/native-simulator-view.js +428 -0
- package/dist/main/services/views/overlay-panels-view.d.ts +29 -0
- package/dist/main/services/views/overlay-panels-view.js +150 -0
- package/dist/main/services/views/placement-reconciler.d.ts +73 -0
- package/dist/main/services/views/placement-reconciler.js +120 -0
- package/dist/main/services/views/resolve-project-editor-target.d.ts +13 -0
- package/dist/main/services/views/resolve-project-editor-target.js +33 -0
- package/dist/main/services/views/view-manager.d.ts +20 -16
- package/dist/main/services/views/view-manager.js +66 -1545
- package/dist/main/services/views/workbench-view.d.ts +15 -0
- package/dist/main/services/views/workbench-view.js +170 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +10 -1
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +6 -0
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +6 -0
- package/dist/renderer/assets/index-ChF9V4r-.js +49 -0
- package/dist/renderer/assets/{input-Djg2NR4U.js → input-Cjk0wSEh.js} +2 -2
- package/dist/renderer/assets/ipc-transport-D5dKIti1.css +1 -0
- package/dist/renderer/assets/{ipc-transport-Be-AzCub.js → ipc-transport-OZS-KmOW.js} +2 -2
- package/dist/renderer/assets/{popover-eDUh4NYe.js → popover-CQVvOe90.js} +2 -2
- package/dist/renderer/assets/{select-DesFfkVV.js → select-BQDXi5ih.js} +2 -2
- package/dist/renderer/assets/{settings-api-HQ7ZFPWi.js → settings-api-B6x2mhaD.js} +2 -2
- package/dist/renderer/assets/{settings-CRmO7zUG.js → settings-tP3Px2KR.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CZBWsliT.js → workbenchSettings-Bim9ol9R.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/bridge-channels.d.ts +17 -0
- package/dist/shared/bridge-channels.js +9 -0
- package/dist/shared/ipc-channels.d.ts +1 -0
- package/dist/shared/ipc-channels.js +6 -0
- package/dist/shared/ipc-schemas.d.ts +6 -0
- package/dist/shared/ipc-schemas.js +10 -0
- package/dist/simulator/assets/{bridge-channels-BUQ5AbvJ.js → bridge-channels-zhsumfky.js} +2 -2
- package/dist/simulator/assets/{device-shell-CT3LRnTB.js → device-shell-CUl0ILfn.js} +2 -2
- package/dist/simulator/assets/{simulator-BizhGGE3.js → simulator-Dz8iGcgy.js} +5 -5
- package/dist/simulator/assets/{simulator-mini-app-BfNuDF3R.js → simulator-mini-app-Bt639XL_.js} +2 -2
- package/dist/simulator/simulator.html +2 -1
- package/package.json +5 -5
- package/dist/renderer/assets/index-jDrNHghH.js +0 -49
- package/dist/renderer/assets/ipc-transport-C6-KPMqw.css +0 -1
|
@@ -1,1567 +1,88 @@
|
|
|
1
|
-
import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { cjsSiblingPreloadPath, mainPreloadPath } from '../../utils/paths.js';
|
|
5
|
-
import { simDeskBg } from '../../utils/theme.js';
|
|
6
|
-
import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
|
|
7
|
-
import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
|
|
8
|
-
import { buildDevtoolsProjectSourceLinksScript, decodeOpenInEditorUrl, projectSourceContextFromServiceHostUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
|
|
9
1
|
import { createSafeAreaController } from '../safe-area/index.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
import { createHostToolbarPortChannel, } from './host-toolbar-port-channel.js';
|
|
18
|
-
import { parseRoute } from '../../../shared/simulator-route.js';
|
|
19
|
-
import { HEADER_H, HOST_TOOLBAR_RUNTIME_MARKER } from '../../../shared/constants.js';
|
|
20
|
-
import { reconcile, createInitialState } from '@dimina-kit/electron-deck/layout';
|
|
21
|
-
import { applyViewOps } from './apply-view-ops.js';
|
|
22
|
-
import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
|
|
23
|
-
/**
|
|
24
|
-
* Resolve a DevTools source request against the service-host URL that created
|
|
25
|
-
* the inspected app. Its pkgRoot is authoritative; the workspace is only a
|
|
26
|
-
* stale-session consistency guard.
|
|
27
|
-
*/
|
|
28
|
-
export function resolveProjectEditorTarget(serviceHostUrl, activeProjectRoot, req, isFile = (absolutePath) => fs.statSync(absolutePath).isFile()) {
|
|
29
|
-
const sourceContext = projectSourceContextFromServiceHostUrl(serviceHostUrl, activeProjectRoot);
|
|
30
|
-
if (!sourceContext)
|
|
31
|
-
return null;
|
|
32
|
-
const rel = resourceUrlToProjectRelativePath(req.url, sourceContext);
|
|
33
|
-
if (!rel)
|
|
34
|
-
return null;
|
|
35
|
-
const absolute = path.resolve(sourceContext.projectRoot, ...rel.split('/'));
|
|
36
|
-
const fromRoot = path.relative(path.resolve(sourceContext.projectRoot), absolute);
|
|
37
|
-
if (!fromRoot || fromRoot.startsWith('..') || path.isAbsolute(fromRoot))
|
|
38
|
-
return null;
|
|
39
|
-
try {
|
|
40
|
-
if (!isFile(absolute))
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
path: rel,
|
|
48
|
-
line: typeof req.line === 'number' ? req.line + 1 : undefined,
|
|
49
|
-
column: typeof req.column === 'number' ? req.column + 1 : undefined,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
// `webContents.executeJavaScript()` registers a transient `did-stop-loading`
|
|
53
|
-
// listener on the target wc whenever that wc is still loading (Electron's
|
|
54
|
-
// `waitTillCanExecuteJavaScript` defers the eval until load finishes, then
|
|
55
|
-
// removes the listener). The DevTools front-end host wc takes a burst of injects
|
|
56
|
-
// during its boot window — tab customization, default-panel selection, the 150ms
|
|
57
|
-
// Elements-forward reconcile, and Network-forward probing all call
|
|
58
|
-
// `executeJavaScript()` on it before the front-end has finished loading — so the
|
|
59
|
-
// pending listeners can transiently exceed Node's default EventEmitter ceiling
|
|
60
|
-
// of 10 and print a spurious `MaxListenersExceededWarning: 11 did-stop-loading
|
|
61
|
-
// listeners`. They drain once the wc stops loading; this is a boot-time
|
|
62
|
-
// concurrency spike, not a per-event leak. Raise the ceiling on the DevTools
|
|
63
|
-
// host wc, which we knowingly inject into repeatedly during its boot window, so
|
|
64
|
-
// the benign spike stays quiet. (Confirmed at runtime: the tripped emitter is
|
|
65
|
-
// this host wc's `did-stop-loading`, count 11 — see the `process.on('warning')`
|
|
66
|
-
// diagnostic in app bootstrap.)
|
|
67
|
-
const DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS = 50;
|
|
68
|
-
function raiseExecuteJavaScriptListenerCeiling(wc) {
|
|
69
|
-
try {
|
|
70
|
-
if (wc.isDestroyed())
|
|
71
|
-
return;
|
|
72
|
-
if (wc.getMaxListeners() >= DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS)
|
|
73
|
-
return;
|
|
74
|
-
wc.setMaxListeners(DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS);
|
|
75
|
-
}
|
|
76
|
-
catch { /* torn-down / stub wc — best effort, warning is non-fatal */ }
|
|
77
|
-
}
|
|
2
|
+
import { createPlacementReconciler } from './placement-reconciler.js';
|
|
3
|
+
import { createWorkbenchView } from './workbench-view.js';
|
|
4
|
+
import { createDevtoolsHost } from './native-simulator-devtools-host.js';
|
|
5
|
+
import { createHostToolbarView } from './host-toolbar-view.js';
|
|
6
|
+
import { createOverlayPanelsView } from './overlay-panels-view.js';
|
|
7
|
+
import { createNativeSimulatorView } from './native-simulator-view.js';
|
|
8
|
+
export { resolveProjectEditorTarget, } from './resolve-project-editor-target.js';
|
|
78
9
|
/**
|
|
79
10
|
* Build a ViewManager bound to the given context. The returned object is the
|
|
80
11
|
* only component allowed to instantiate or add/remove overlay WebContentsViews.
|
|
81
12
|
*
|
|
82
|
-
* All view-related mutable state lives inside
|
|
83
|
-
* on the context object.
|
|
13
|
+
* All view-related mutable state lives inside the per-domain factory closures
|
|
14
|
+
* (and the single placement reconciler) below, never on the context object.
|
|
84
15
|
*/
|
|
85
16
|
export function createViewManager(ctx) {
|
|
86
17
|
// CSS env(safe-area-inset-*) simulation for render-host guests (per device).
|
|
87
|
-
// Driven from did-attach-webview
|
|
88
|
-
// reapplySafeArea. Torn down in disposeAll.
|
|
18
|
+
// Driven from did-attach-webview in the simulator domain and re-pushed on
|
|
19
|
+
// device change via reapplySafeArea. Torn down in disposeAll.
|
|
89
20
|
const safeArea = createSafeAreaController({ connections: ctx.connections });
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let nativeSimulatorView = null;
|
|
98
|
-
let nativeSimulatorViewAdded = false;
|
|
99
|
-
let nativeSimulatorProjectPath = null;
|
|
100
|
-
let settleNativeSimulatorReady = null;
|
|
101
|
-
// NATIVE-HOST ONLY. The `ipcMain.on` listener that services the
|
|
102
|
-
// `__diminaCustomApis` bridge for the current native simulator webContents
|
|
103
|
-
// (see `attachNativeCustomApiBridge`). Tracked so we can remove it before
|
|
104
|
-
// tearing down / re-attaching the view (otherwise listeners leak across
|
|
105
|
-
// relaunch cycles).
|
|
106
|
-
let nativeCustomApiBridgeHandler = null;
|
|
107
|
-
// NATIVE-HOST ONLY. The current device zoom as a factor (zoomPercent/100),
|
|
108
|
-
// last reported by the renderer via setNativeSimulatorViewBounds. Stored so
|
|
109
|
-
// nested render-host `<webview>` guests attached AFTER a zoom change still
|
|
110
|
-
// pick up the correct scale in `did-attach-webview`. Defaults to 1 (100%).
|
|
111
|
-
let currentZoomFactor = 1;
|
|
112
|
-
let settingsView = null;
|
|
113
|
-
let popoverView = null;
|
|
114
|
-
let simulatorWebContentsId = null;
|
|
115
|
-
// NATIVE-HOST ONLY. The webContents the right-panel Chrome DevTools front-end
|
|
116
|
-
// currently inspects. We point it at the SERVICE HOST (logic layer) — the
|
|
117
|
-
// hidden BrowserWindow where the mini-app's page code runs (`console.log`,
|
|
118
|
-
// `wx.request`, Sources/Network(fetch) all live there). The UI/view layer
|
|
119
|
-
// (Elements/Styles/WXML tree) is served separately by the native WXML panel +
|
|
120
|
-
// render-guest highlight chain (`simulator-storage`/`simulator-wxml`), so a
|
|
121
|
-
// single DevTools front-end is enough. The service window can be swapped on
|
|
122
|
-
// respawn (pre-warm pool recycles it), so this is re-resolved fresh via
|
|
123
|
-
// `ctx.bridge.getServiceWc()` and re-pointed on every render-side event.
|
|
124
|
-
let nativeDevtoolsSourceWc = null;
|
|
125
|
-
let unsubscribeNativeRenderEvents = null;
|
|
126
|
-
// Disposer for the Elements-forward feature (routes the front-end's Elements
|
|
127
|
-
// CDP traffic at the active render guest). Installed in
|
|
128
|
-
// `attachNativeSimulatorDevtoolsHost`, stopped on detach / host destroyed.
|
|
129
|
-
let stopElementsForward = null;
|
|
130
|
-
// Disposer for the service-layer console capture (CDP `consoleAPICalled` on the
|
|
131
|
-
// service host wc → console fan-out). Installed when the DevTools is pointed at
|
|
132
|
-
// a service host, stopped when that source is closed / swapped.
|
|
133
|
-
let stopServiceConsole = null;
|
|
134
|
-
let nativeDevtoolsRetryTimer = null;
|
|
135
|
-
let nativeDevtoolsRetryToken = 0;
|
|
136
|
-
// ── Embedded workbench editor WebContentsView ────────────────────────
|
|
137
|
-
// The opt-in VS Code workbench hosting the 'editor' dock slot. Lazily created
|
|
138
|
-
// by `attachWorkbench` from the COI server URL; its bounds ride the renderer
|
|
139
|
-
// 'editor'-slot anchor (forward anchor, like the simulator DevTools overlay).
|
|
140
|
-
let workbenchView = null;
|
|
141
|
-
// Whether the devtools-theme → workbench-theme `nativeTheme` listener is live.
|
|
142
|
-
// Bound lazily on first workbench attach, removed on detach.
|
|
143
|
-
let workbenchThemeSyncBound = false;
|
|
144
|
-
// COI server base URL for the workbench, stored by `setWorkbenchSource`. The
|
|
145
|
-
// heavy WebContentsView load is deferred until the 'editor' slot first becomes
|
|
146
|
-
// visible (first non-zero `setWorkbenchBounds`) so it never sits on the app
|
|
147
|
-
// boot critical path (which would delay preload/window-ready and trip the e2e
|
|
148
|
-
// health check into a relaunch).
|
|
149
|
-
let workbenchUrl = null;
|
|
150
|
-
// ── Host-controllable toolbar WebContentsView ───────────────────────────
|
|
151
|
-
// A strip above the devtools header that the downstream host loads its own
|
|
152
|
-
// content into and fully controls. Bounds come from a renderer DOM anchor
|
|
153
|
-
// (forward anchor, like the simulator DevTools overlay); its height is
|
|
154
|
-
// dynamic via a reverse size-advertiser the toolbar's own renderer drives.
|
|
155
|
-
let hostToolbarView = null;
|
|
156
|
-
let hostToolbarPreloadOverride = null;
|
|
157
|
-
let hostToolbarViewAdded = false;
|
|
158
|
-
// Whether THIS manager holds a reference on the shared defaultSession
|
|
159
|
-
// registration of the toolbar-runtime preload (see
|
|
160
|
-
// host-toolbar-session-runtime.ts). Acquired on first toolbar need,
|
|
161
|
-
// released exactly once in disposeAll — a manager that never used the
|
|
162
|
-
// toolbar must not decrement a ref it never took.
|
|
163
|
-
let hostToolbarRuntimeAcquired = false;
|
|
164
|
-
// Placeholder height authority: 'auto' = advertiser reports forward to the
|
|
165
|
-
// renderer; { fixed } = host-pinned, advertiser reports are dropped.
|
|
166
|
-
let hostToolbarHeightMode = 'auto';
|
|
167
|
-
// Last toolbar height NOTIFIED to the main-window renderer — the replay
|
|
168
|
-
// source behind `getHostToolbarHeight()`. Updated ONLY inside
|
|
169
|
-
// `notifyHostToolbarHeight` so the retained value can never diverge from
|
|
170
|
-
// what the renderer was told (an advertiser report dropped by a `{ fixed }`
|
|
171
|
-
// pin must not pollute it, and a setHeightMode validation reject leaves it
|
|
172
|
-
// untouched).
|
|
173
|
-
let hostToolbarLastHeight = 0;
|
|
174
|
-
// Gated narrow channel to the toolbar PAGE (per-load MessagePort handshake;
|
|
175
|
-
// see host-toolbar-port-channel.ts). Control-level registry — created with
|
|
176
|
-
// the manager so onMessage() works before any toolbar view exists.
|
|
177
|
-
const hostToolbarPort = createHostToolbarPortChannel({
|
|
178
|
-
isCurrent: (wc) => liveHostToolbarWebContents() === wc,
|
|
21
|
+
// The single level-triggered placement reconciler every view domain shares —
|
|
22
|
+
// the sole owner of placement state (docs/view-placement-reconciler.md). Each
|
|
23
|
+
// domain registers exactly one view slot with it.
|
|
24
|
+
const reconciler = createPlacementReconciler(ctx);
|
|
25
|
+
const workbench = createWorkbenchView(ctx, reconciler);
|
|
26
|
+
const devtoolsHost = createDevtoolsHost(ctx, reconciler, {
|
|
27
|
+
openFileInWorkbench: workbench.openFileInWorkbench,
|
|
179
28
|
});
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// The main-process view tree is converged toward the renderer's declared
|
|
199
|
-
// placement by a level-triggered reconciler (docs/view-placement-reconciler.md).
|
|
200
|
-
// The renderer publishes a window-level snapshot into `baseDesired`;
|
|
201
|
-
// settings/popover are main-owned and live in `overlayDesired`. Any change
|
|
202
|
-
// merges the two, runs the pure reconcile core, and applies the ordered ops
|
|
203
|
-
// through `viewTarget`. `epochCounter` is a single monotonic tick — main is the
|
|
204
|
-
// only (serial) reconcile caller, so the core's stale guard passes by
|
|
205
|
-
// construction; `rendererGeneration` still drives the reset on renderer restart.
|
|
206
|
-
let placementState = createInitialState();
|
|
207
|
-
let epochCounter = 0;
|
|
208
|
-
let rendererGeneration = 0;
|
|
209
|
-
const baseDesired = new Map();
|
|
210
|
-
const overlayDesired = new Map();
|
|
211
|
-
// simulator / simulator-devtools WCVs are built by the project-open flow, not
|
|
212
|
-
// by an attach op; gate them hidden until they exist so the reconciler never
|
|
213
|
-
// records an attach that addChildView can't perform. The view-creation sites
|
|
214
|
-
// call reconcileNow() to re-open the gate. workbench is gated until it has a
|
|
215
|
-
// source or a live view (it lazy-creates from the URL in target.attach).
|
|
216
|
-
function gateReadiness(v) {
|
|
217
|
-
if (v.viewId === VIEW_ID.simulator && !nativeSimulatorView)
|
|
218
|
-
return { ...v, placement: { visible: false } };
|
|
219
|
-
if (v.viewId === VIEW_ID.simulatorDevtools && !simulatorView)
|
|
220
|
-
return { ...v, placement: { visible: false } };
|
|
221
|
-
if (v.viewId === VIEW_ID.workbench && !workbenchView && !workbenchUrl)
|
|
222
|
-
return { ...v, placement: { visible: false } };
|
|
223
|
-
return v;
|
|
224
|
-
}
|
|
225
|
-
function resolveView(viewId) {
|
|
226
|
-
switch (viewId) {
|
|
227
|
-
case VIEW_ID.simulator: return nativeSimulatorView;
|
|
228
|
-
case VIEW_ID.simulatorDevtools: return simulatorView;
|
|
229
|
-
case VIEW_ID.workbench: return workbenchView;
|
|
230
|
-
case VIEW_ID.hostToolbar: return hostToolbarView;
|
|
231
|
-
case VIEW_ID.settings: return settingsView;
|
|
232
|
-
case VIEW_ID.popover: return popoverView;
|
|
233
|
-
default: return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function markAdded(viewId, added) {
|
|
237
|
-
switch (viewId) {
|
|
238
|
-
case VIEW_ID.simulator:
|
|
239
|
-
nativeSimulatorViewAdded = added;
|
|
240
|
-
break;
|
|
241
|
-
case VIEW_ID.simulatorDevtools:
|
|
242
|
-
simulatorViewAdded = added;
|
|
243
|
-
break;
|
|
244
|
-
case VIEW_ID.hostToolbar:
|
|
245
|
-
hostToolbarViewAdded = added;
|
|
246
|
-
break;
|
|
247
|
-
// popover has no added-flag (create == added).
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// simulator zoom rides in `extra`: setBounds also drives the WCV zoomFactor and
|
|
251
|
-
// propagates it to nested render-host guests (from the old
|
|
252
|
-
// setNativeSimulatorViewBounds body).
|
|
253
|
-
function applyNativeSimulatorBounds(view, bounds, zoom) {
|
|
254
|
-
const p = layout.computeNativeSimulatorViewParams(bounds, zoom);
|
|
255
|
-
currentZoomFactor = p.zoomFactor;
|
|
256
|
-
view.setBounds(p.bounds);
|
|
257
|
-
const simWc = view.webContents;
|
|
258
|
-
if (!simWc.isDestroyed())
|
|
259
|
-
simWc.setZoomFactor(p.zoomFactor);
|
|
260
|
-
try {
|
|
261
|
-
for (const wc of webContents.getAllWebContents()) {
|
|
262
|
-
if (wc.isDestroyed())
|
|
263
|
-
continue;
|
|
264
|
-
if (wc.hostWebContents === simWc)
|
|
265
|
-
wc.setZoomFactor(p.zoomFactor);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
catch { /* hostWebContents unavailable; guests get zoom on attach */ }
|
|
269
|
-
}
|
|
270
|
-
const viewTarget = {
|
|
271
|
-
attach(viewId) {
|
|
272
|
-
if (ctx.windows.mainWindow.isDestroyed())
|
|
273
|
-
return;
|
|
274
|
-
if (viewId === VIEW_ID.workbench && !workbenchView && workbenchUrl) {
|
|
275
|
-
// Lazy-load the workbench; attachWorkbench adds it + sets the flag.
|
|
276
|
-
void attachWorkbench(workbenchUrl);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (viewId === VIEW_ID.hostToolbar) {
|
|
280
|
-
const view = ensureHostToolbarView();
|
|
281
|
-
ctx.windows.mainWindow.contentView.addChildView(view);
|
|
282
|
-
hostToolbarViewAdded = true;
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const view = resolveView(viewId);
|
|
286
|
-
if (!view)
|
|
287
|
-
return;
|
|
288
|
-
ctx.windows.mainWindow.contentView.addChildView(view);
|
|
289
|
-
markAdded(viewId, true);
|
|
290
|
-
},
|
|
291
|
-
detach(viewId) {
|
|
292
|
-
const view = resolveView(viewId);
|
|
293
|
-
if (view && !ctx.windows.mainWindow.isDestroyed()) {
|
|
294
|
-
try {
|
|
295
|
-
ctx.windows.mainWindow.contentView.removeChildView(view);
|
|
296
|
-
}
|
|
297
|
-
catch { /* already removed */ }
|
|
298
|
-
}
|
|
299
|
-
markAdded(viewId, false);
|
|
300
|
-
},
|
|
301
|
-
setBounds(viewId, bounds, extra) {
|
|
302
|
-
const view = resolveView(viewId);
|
|
303
|
-
if (!view || view.webContents.isDestroyed())
|
|
304
|
-
return;
|
|
305
|
-
if (viewId === VIEW_ID.simulator) {
|
|
306
|
-
applyNativeSimulatorBounds(view, bounds, extra?.zoom ?? 1);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
view.setBounds(bounds);
|
|
310
|
-
},
|
|
311
|
-
setVisible(viewId, visible) {
|
|
312
|
-
const view = resolveView(viewId);
|
|
313
|
-
if (!view)
|
|
314
|
-
return;
|
|
315
|
-
try {
|
|
316
|
-
view.setVisible(visible);
|
|
317
|
-
}
|
|
318
|
-
catch { /* stub may lack setVisible */ }
|
|
319
|
-
},
|
|
320
|
-
reorder(order) {
|
|
321
|
-
// A single attached view is already in place — nothing to reorder.
|
|
322
|
-
if (order.length <= 1 || ctx.windows.mainWindow.isDestroyed())
|
|
323
|
-
return;
|
|
324
|
-
const cv = ctx.windows.mainWindow.contentView;
|
|
325
|
-
for (const viewId of order) {
|
|
326
|
-
const view = resolveView(viewId);
|
|
327
|
-
if (view) {
|
|
328
|
-
try {
|
|
329
|
-
cv.addChildView(view);
|
|
330
|
-
}
|
|
331
|
-
catch { /* already attached */ }
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
// Create the lazily-built views (host-toolbar, workbench) BEFORE reconcile so
|
|
337
|
-
// the setBounds op — which the core emits before attach — lands on a live view
|
|
338
|
-
// rather than a not-yet-created one.
|
|
339
|
-
function ensureLazyViews() {
|
|
340
|
-
const ht = baseDesired.get(VIEW_ID.hostToolbar);
|
|
341
|
-
if (ht?.placement.visible && !liveHostToolbarWebContents())
|
|
342
|
-
ensureHostToolbarView();
|
|
343
|
-
const wb = baseDesired.get(VIEW_ID.workbench);
|
|
344
|
-
if (wb?.placement.visible && !workbenchView && workbenchUrl)
|
|
345
|
-
void attachWorkbench(workbenchUrl);
|
|
346
|
-
}
|
|
347
|
-
function reconcileNow() {
|
|
348
|
-
ensureLazyViews();
|
|
349
|
-
const views = [];
|
|
350
|
-
for (const v of baseDesired.values())
|
|
351
|
-
views.push(gateReadiness(v));
|
|
352
|
-
for (const v of overlayDesired.values())
|
|
353
|
-
views.push(v);
|
|
354
|
-
const result = reconcile(placementState, {
|
|
355
|
-
generation: rendererGeneration,
|
|
356
|
-
epoch: ++epochCounter,
|
|
357
|
-
views,
|
|
358
|
-
});
|
|
359
|
-
placementState = result.state;
|
|
360
|
-
applyViewOps(result.ops, viewTarget);
|
|
361
|
-
}
|
|
362
|
-
function setPlacementSnapshot(snapshot) {
|
|
363
|
-
rendererGeneration = snapshot.generation;
|
|
364
|
-
baseDesired.clear();
|
|
365
|
-
for (const v of snapshot.views)
|
|
366
|
-
baseDesired.set(v.viewId, v);
|
|
367
|
-
reconcileNow();
|
|
368
|
-
}
|
|
369
|
-
// ── Embedded workbench editor WebContentsView ────────────────────────
|
|
370
|
-
/** Current devtools color scheme, mirrored into the workbench's theme. */
|
|
371
|
-
function workbenchThemeScheme() {
|
|
372
|
-
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
|
373
|
-
}
|
|
374
|
-
// Push the live devtools scheme into the workbench whenever it flips. The
|
|
375
|
-
// workbench is a plain isolated http document, so drive its exposed
|
|
376
|
-
// `__WB_SET_THEME` setter over executeJavaScript (mirrors openFileInWorkbench).
|
|
377
|
-
// The setter only exists once the workbench's configuration service is
|
|
378
|
-
// initialized; before then the URL-query initial value already covers the
|
|
379
|
-
// current scheme, and a missing setter here is a tolerated no-op.
|
|
380
|
-
function pushWorkbenchTheme() {
|
|
381
|
-
if (!workbenchView || workbenchView.webContents.isDestroyed())
|
|
382
|
-
return;
|
|
383
|
-
const wc = workbenchView.webContents;
|
|
384
|
-
if (typeof wc.executeJavaScript !== 'function')
|
|
385
|
-
return;
|
|
386
|
-
const script = `window.__WB_SET_THEME && window.__WB_SET_THEME(${JSON.stringify(workbenchThemeScheme())})`;
|
|
387
|
-
wc.executeJavaScript(script, true).catch(() => { });
|
|
388
|
-
}
|
|
389
|
-
async function attachWorkbench(url) {
|
|
390
|
-
if (workbenchView)
|
|
391
|
-
return;
|
|
392
|
-
const view = new WebContentsView({
|
|
393
|
-
webPreferences: {
|
|
394
|
-
nodeIntegration: false,
|
|
395
|
-
contextIsolation: true,
|
|
396
|
-
sandbox: false,
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
workbenchView = view;
|
|
400
|
-
// Track devtools theme flips for the lifetime of the workbench view only —
|
|
401
|
-
// registered here (not at construction) so test electron mocks that omit
|
|
402
|
-
// `nativeTheme` and never open the editor stay unaffected. Removed in
|
|
403
|
-
// detachWorkbench.
|
|
404
|
-
if (!workbenchThemeSyncBound) {
|
|
405
|
-
nativeTheme.on('updated', pushWorkbenchTheme);
|
|
406
|
-
workbenchThemeSyncBound = true;
|
|
407
|
-
}
|
|
408
|
-
// The workbench bundle loads arbitrary URLs (docs links, etc.); route popups
|
|
409
|
-
// + cross-origin in-place navigation to the OS browser (mirror the host
|
|
410
|
-
// toolbar / native simulator hardening).
|
|
411
|
-
try {
|
|
412
|
-
view.webContents.setWindowOpenHandler(({ url: target }) => handleWindowOpenExternal(target));
|
|
413
|
-
}
|
|
414
|
-
catch { /* stub may lack it */ }
|
|
415
|
-
ctx.windows.mainWindow.contentView.addChildView(view);
|
|
416
|
-
// Hand the workbench the current devtools scheme as a URL query so its very
|
|
417
|
-
// first paint already matches (the runtime setter only exists post-init).
|
|
418
|
-
const loadUrl = `${url}index.html?theme=${workbenchThemeScheme()}`;
|
|
419
|
-
await view.webContents.loadURL(loadUrl).catch((err) => {
|
|
420
|
-
console.error('[workbench] attachWorkbench — loadURL failed', err);
|
|
421
|
-
});
|
|
422
|
-
}
|
|
423
|
-
/** Store the COI base URL; the heavy load happens lazily on first show. */
|
|
424
|
-
function setWorkbenchSource(url) {
|
|
425
|
-
workbenchUrl = url;
|
|
426
|
-
}
|
|
427
|
-
function detachWorkbench() {
|
|
428
|
-
if (workbenchThemeSyncBound) {
|
|
429
|
-
nativeTheme.removeListener('updated', pushWorkbenchTheme);
|
|
430
|
-
workbenchThemeSyncBound = false;
|
|
431
|
-
}
|
|
432
|
-
destroyViewInternal(workbenchView);
|
|
433
|
-
workbenchView = null;
|
|
434
|
-
baseDesired.delete(VIEW_ID.workbench);
|
|
435
|
-
reconcileNow();
|
|
436
|
-
}
|
|
437
|
-
// Build the `file:///workspace/<rel>` URI string with each path SEGMENT
|
|
438
|
-
// percent-encoded. A raw `rel` passed to `vscode.Uri.parse` mis-parses a
|
|
439
|
-
// filename containing `#` (treated as a fragment) or `?` (treated as a
|
|
440
|
-
// query), opening the wrong document; encoding each segment (but not the
|
|
441
|
-
// `/` separators) keeps the path structure while escaping the reserved
|
|
442
|
-
// characters. Leading slashes are already stripped by the caller.
|
|
443
|
-
function workspaceUriFor(rel) {
|
|
444
|
-
const encoded = rel.split('/').map(encodeURIComponent).join('/');
|
|
445
|
-
return `file:///workspace/${encoded}`;
|
|
446
|
-
}
|
|
447
|
-
// Single attempt to reveal `uri` at the 0-based position in the workbench.
|
|
448
|
-
// Resolves false when `__WB_PROBE` is not yet exposed (the workbench's
|
|
449
|
-
// configuration service has not initialized) OR the open throws, so the
|
|
450
|
-
// caller can retry; true once the document is shown.
|
|
451
|
-
function tryRevealInWorkbench(uri, zeroLine, zeroCol) {
|
|
452
|
-
if (!workbenchView || workbenchView.webContents.isDestroyed()) {
|
|
453
|
-
return Promise.resolve(false);
|
|
454
|
-
}
|
|
455
|
-
const script = `(async () => {
|
|
456
|
-
try {
|
|
457
|
-
const P = window.__WB_PROBE; if (!P) return false
|
|
458
|
-
const vscode = P.vscode
|
|
459
|
-
const uri = vscode.Uri.parse(${JSON.stringify(uri)})
|
|
460
|
-
const doc = await vscode.workspace.openTextDocument(uri)
|
|
461
|
-
const pos = new vscode.Position(${zeroLine}, ${zeroCol})
|
|
462
|
-
await vscode.window.showTextDocument(doc, { selection: new vscode.Range(pos, pos) })
|
|
463
|
-
return true
|
|
464
|
-
} catch (e) { return false }
|
|
465
|
-
})()`;
|
|
466
|
-
return workbenchView.webContents
|
|
467
|
-
.executeJavaScript(script, true)
|
|
468
|
-
.then((ok) => ok === true)
|
|
469
|
-
.catch(() => false);
|
|
470
|
-
}
|
|
471
|
-
// Reveal a project file in the embedded workbench, awaiting the real open
|
|
472
|
-
// result and retrying while the workbench finishes booting. The right-panel
|
|
473
|
-
// console redirect (`onOpenUrl`) fires open-in-editor clicks that can land
|
|
474
|
-
// during the workbench's lazy attach/boot window, when `__WB_PROBE` is not
|
|
475
|
-
// yet exposed; without the retry the click is silently dropped (the inner
|
|
476
|
-
// script returns false and the old code ignored it). Returns true only once
|
|
477
|
-
// the document is actually shown; false when there is no workbench view or
|
|
478
|
-
// every attempt failed.
|
|
479
|
-
function openFileInWorkbench(relPath, line, column) {
|
|
480
|
-
if (!workbenchView || workbenchView.webContents.isDestroyed())
|
|
481
|
-
return false;
|
|
482
|
-
// The workbench mirrors the active project under file:///workspace/<rel>; the
|
|
483
|
-
// open-in-editor target is 1-based (editor convention) while vscode.Position
|
|
484
|
-
// is 0-based, so clamp-convert. Drive the workbench's own vscode API rather
|
|
485
|
-
// than a preload bridge — the bundle is a plain isolated http document.
|
|
486
|
-
const uri = workspaceUriFor(relPath.replace(/^\/+/, ''));
|
|
487
|
-
const zeroLine = Math.max(0, Math.floor(line) - 1);
|
|
488
|
-
const zeroCol = Math.max(0, Math.floor(column) - 1);
|
|
489
|
-
void (async () => {
|
|
490
|
-
// Poll for workbench readiness: ~10 attempts × 150ms ≈ 1.5s, covering the
|
|
491
|
-
// first lazy attach + ext-host boot. Each attempt re-checks the live view
|
|
492
|
-
// so a teardown mid-retry bails cleanly.
|
|
493
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
494
|
-
if (!workbenchView || workbenchView.webContents.isDestroyed())
|
|
495
|
-
return;
|
|
496
|
-
if (await tryRevealInWorkbench(uri, zeroLine, zeroCol))
|
|
497
|
-
return;
|
|
498
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
499
|
-
}
|
|
500
|
-
console.error('[workbench] openFileInWorkbench: workbench never became ready for', uri);
|
|
501
|
-
})();
|
|
502
|
-
return true;
|
|
503
|
-
}
|
|
504
|
-
// ── Host-controllable toolbar WebContentsView ───────────────────────────
|
|
505
|
-
// The toolbar's webContents lifecycle belongs to the HOST, which may close
|
|
506
|
-
// it out from under us (the documented rebuild path). In real Electron a
|
|
507
|
-
// WebContentsView whose webContents was destroyed can report `webContents`
|
|
508
|
-
// as undefined — not merely a destroyed handle — so every access must
|
|
509
|
-
// tolerate BOTH (observed in the R1 e2e: `.isDestroyed()` on undefined threw
|
|
510
|
-
// inside the control surface after the host closed the wc).
|
|
511
|
-
function liveHostToolbarWebContents() {
|
|
512
|
-
const wc = hostToolbarView?.webContents;
|
|
513
|
-
if (!wc || wc.isDestroyed())
|
|
514
|
-
return null;
|
|
515
|
-
return wc;
|
|
516
|
-
}
|
|
517
|
-
// Lazily create the host-toolbar view. Mirrors `showSettings` for the
|
|
518
|
-
// webPreferences shape and the native simulator for nav hardening +
|
|
519
|
-
// background color (the host may load arbitrary URLs / content). Idempotent.
|
|
520
|
-
function ensureHostToolbarView() {
|
|
521
|
-
if (hostToolbarView && liveHostToolbarWebContents()) {
|
|
522
|
-
return hostToolbarView;
|
|
523
|
-
}
|
|
524
|
-
// Rebuilding after the host destroyed the underlying webContents: detach the
|
|
525
|
-
// dead view from the contentView and reset the added-flag so the new view
|
|
526
|
-
// gets re-mounted (otherwise the `hostToolbarViewAdded` guard would skip the
|
|
527
|
-
// addChildView and the toolbar would silently disappear).
|
|
528
|
-
if (hostToolbarView && hostToolbarViewAdded) {
|
|
529
|
-
try {
|
|
530
|
-
ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
|
|
531
|
-
}
|
|
532
|
-
catch { /* already removed */ }
|
|
533
|
-
hostToolbarViewAdded = false;
|
|
534
|
-
}
|
|
535
|
-
// The instance is being replaced — forget its reconciled mount state so the
|
|
536
|
-
// rebuilt view is treated as a fresh attach (not skipped as already-attached).
|
|
537
|
-
placementState.actual.delete(VIEW_ID.hostToolbar);
|
|
538
|
-
// The framework's height-advertiser runtime is SESSION-resident: register
|
|
539
|
-
// it on session.defaultSession (ref-counted across coexisting managers)
|
|
540
|
-
// BEFORE the view exists, so the very first load already runs it. The
|
|
541
|
-
// toolbar WCV stays on the defaultSession (no partition/session override)
|
|
542
|
-
// — moving it onto its own partition would silently detach it from this
|
|
543
|
-
// registration and height advertising would die with no error.
|
|
544
|
-
if (!hostToolbarRuntimeAcquired) {
|
|
545
|
-
acquireHostToolbarSessionRuntime();
|
|
546
|
-
hostToolbarRuntimeAcquired = true;
|
|
547
|
-
}
|
|
548
|
-
// `webPreferences.preload` is the HOST's alone (setPreloadPath); the
|
|
549
|
-
// built-in advertiser no longer rides it (it would execute twice — the
|
|
550
|
-
// session copy + the webPreferences copy). The additionalArguments marker
|
|
551
|
-
// is what the session runtime's guard keys on to activate here and stay a
|
|
552
|
-
// zero-footprint no-op in every other defaultSession renderer.
|
|
553
|
-
const webPreferences = {
|
|
554
|
-
nodeIntegration: false,
|
|
555
|
-
contextIsolation: true,
|
|
556
|
-
sandbox: false,
|
|
557
|
-
additionalArguments: [HOST_TOOLBAR_RUNTIME_MARKER],
|
|
558
|
-
};
|
|
559
|
-
if (hostToolbarPreloadOverride !== null) {
|
|
560
|
-
webPreferences.preload = hostToolbarPreloadOverride;
|
|
561
|
-
}
|
|
562
|
-
const view = new WebContentsView({ webPreferences });
|
|
563
|
-
hostToolbarView = view;
|
|
564
|
-
// Hook the per-load MessagePort handshake (did-finish-load) + dead-port
|
|
565
|
-
// cleanup (destroyed) on the fresh wc. AFTER the assignment above so the
|
|
566
|
-
// channel's isCurrent guard sees this wc as the live one.
|
|
567
|
-
hostToolbarPort.attach(view.webContents);
|
|
568
|
-
// Paint the surface a neutral color so growing the reserved strip never
|
|
569
|
-
// flashes white before the host content paints (mirrors the native
|
|
570
|
-
// simulator's setBackgroundColor anti-flash).
|
|
571
|
-
try {
|
|
572
|
-
view.setBackgroundColor('#121212');
|
|
573
|
-
}
|
|
574
|
-
catch { /* stub may lack it */ }
|
|
575
|
-
// The host may load arbitrary URLs; route popups + cross-origin in-place
|
|
576
|
-
// navigation to the OS browser (mirror the native simulator hardening).
|
|
577
|
-
try {
|
|
578
|
-
view.webContents.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
579
|
-
}
|
|
580
|
-
catch { /* stub may lack it */ }
|
|
581
|
-
return view;
|
|
582
|
-
}
|
|
583
|
-
// Single funnel for the height notify: retain-then-push, so the retained
|
|
584
|
-
// value is exactly the last value the renderer was told. Every height
|
|
585
|
-
// notify site MUST go through here — the renderer pulls the retained value
|
|
586
|
-
// on project-view mount to replay a push it missed (the toolbar's
|
|
587
|
-
// size-advertiser deduplicates and never re-reports).
|
|
588
|
-
function notifyHostToolbarHeight(height) {
|
|
589
|
-
hostToolbarLastHeight = height;
|
|
590
|
-
ctx.notify.hostToolbarHeightChanged(height);
|
|
591
|
-
if (overlayDesired.has(VIEW_ID.settings))
|
|
592
|
-
applySettingsBounds();
|
|
593
|
-
if (overlayDesired.has(VIEW_ID.popover))
|
|
594
|
-
applyPopoverBounds();
|
|
595
|
-
}
|
|
596
|
-
function setHostToolbarHeight(extent) {
|
|
597
|
-
// While the host pins a fixed height, drop advertiser reports entirely —
|
|
598
|
-
// the session-resident advertiser is always installed, so forwarding its
|
|
599
|
-
// reports would make the strip oscillate between the pinned and measured
|
|
600
|
-
// heights on every content resize. Dropped reports must not touch the
|
|
601
|
-
// retained value either: retention records what was NOTIFIED, not what
|
|
602
|
-
// was reported.
|
|
603
|
-
if (hostToolbarHeightMode !== 'auto')
|
|
604
|
-
return;
|
|
605
|
-
// Push the reserved height back to the main-window renderer so its
|
|
606
|
-
// placeholder div resizes (closing the dynamic-height loop). The notified
|
|
607
|
-
// height IS retained in main (`getHostToolbarHeight`) so a renderer that
|
|
608
|
-
// mounts later can pull/replay it; the renderer placeholder remains the
|
|
609
|
-
// geometry authority — the forward anchor re-reports bounds from it.
|
|
610
|
-
notifyHostToolbarHeight(extent);
|
|
611
|
-
}
|
|
612
|
-
function hideHostToolbar() {
|
|
613
|
-
baseDesired.set(VIEW_ID.hostToolbar, {
|
|
614
|
-
viewId: VIEW_ID.hostToolbar,
|
|
615
|
-
placement: { visible: false },
|
|
616
|
-
layer: VIEW_LAYER.hostToolbar,
|
|
617
|
-
});
|
|
618
|
-
reconcileNow();
|
|
619
|
-
// Collapse the renderer placeholder to 0 too. Otherwise its anchor keeps a
|
|
620
|
-
// non-zero reserved height and re-publishes bounds on the next window
|
|
621
|
-
// resize, silently re-adding the view we just hid (unstable hide). Zeroing
|
|
622
|
-
// the height flips the anchor to `present:false` so it stops re-publishing.
|
|
623
|
-
// Through the funnel so the retained value follows to 0 — a renderer
|
|
624
|
-
// mounting after the hide must replay 0, not the stale pre-hide height.
|
|
625
|
-
notifyHostToolbarHeight(0);
|
|
626
|
-
}
|
|
627
|
-
const hostToolbar = {
|
|
628
|
-
async loadURL(url) {
|
|
629
|
-
const view = ensureHostToolbarView();
|
|
630
|
-
// Invalidate SYNCHRONOUSLY at initiation, before the load is issued:
|
|
631
|
-
// the current document is about to be replaced, so a same-tick send()
|
|
632
|
-
// must report false instead of confirming delivery into it. The channel
|
|
633
|
-
// recovers on the new document's did-finish-load handshake. (Cannot
|
|
634
|
-
// rely on did-start-navigation here — that only covers page-initiated
|
|
635
|
-
// navigations once the load is actually under way.)
|
|
636
|
-
hostToolbarPort.invalidate();
|
|
637
|
-
await view.webContents.loadURL(url);
|
|
638
|
-
},
|
|
639
|
-
async loadFile(filePath) {
|
|
640
|
-
const view = ensureHostToolbarView();
|
|
641
|
-
// Same initiation-invalidates contract as loadURL above.
|
|
642
|
-
hostToolbarPort.invalidate();
|
|
643
|
-
await view.webContents.loadFile(filePath);
|
|
644
|
-
},
|
|
645
|
-
get webContents() {
|
|
646
|
-
return liveHostToolbarWebContents();
|
|
647
|
-
},
|
|
648
|
-
hide() {
|
|
649
|
-
hideHostToolbar();
|
|
650
|
-
},
|
|
651
|
-
setPreloadPath(path) {
|
|
652
|
-
// The HOST's own webPreferences.preload, applied when the view is next
|
|
653
|
-
// (re)created. `null` = no host preload. The framework advertiser is
|
|
654
|
-
// session-resident and unaffected either way (see ensureHostToolbarView).
|
|
655
|
-
hostToolbarPreloadOverride = path;
|
|
656
|
-
},
|
|
657
|
-
setHeightMode(mode) {
|
|
658
|
-
// Validate BEFORE touching any state: a poisoned `{ fixed }` (NaN /
|
|
659
|
-
// ±Infinity / negative) must neither reach the renderer placeholder
|
|
660
|
-
// (`height: NaNpx` corrupts the strip with no error anywhere) nor
|
|
661
|
-
// clobber the standing mode — fail-closed, not fail-corrupt.
|
|
662
|
-
if (mode !== 'auto' && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
|
|
663
|
-
throw new TypeError(`hostToolbar.setHeightMode: fixed height must be a finite, non-negative number (got ${mode.fixed})`);
|
|
664
|
-
}
|
|
665
|
-
hostToolbarHeightMode = mode;
|
|
666
|
-
if (mode !== 'auto') {
|
|
667
|
-
// Pin immediately: a preload-less/static toolbar never advertises, so
|
|
668
|
-
// waiting for the next report would leave the strip at height 0.
|
|
669
|
-
notifyHostToolbarHeight(mode.fixed);
|
|
670
|
-
}
|
|
671
|
-
// Switching back to 'auto' deliberately does NOT synthesize a notify —
|
|
672
|
-
// replaying a stale cached height would flash the old size; the NEXT
|
|
673
|
-
// advertiser report drives the placeholder again. The RETAINED value
|
|
674
|
-
// survives the switch though: a freshly-mounting renderer still needs
|
|
675
|
-
// the pinned height until that next report lands.
|
|
676
|
-
},
|
|
677
|
-
onMessage(channel, handler) {
|
|
678
|
-
return hostToolbarPort.onMessage(channel, handler);
|
|
679
|
-
},
|
|
680
|
-
onReady(handler) {
|
|
681
|
-
return hostToolbarPort.onReady(handler);
|
|
682
|
-
},
|
|
683
|
-
send(channel, payload) {
|
|
684
|
-
return hostToolbarPort.send(channel, payload);
|
|
685
|
-
},
|
|
686
|
-
};
|
|
687
|
-
function overlayHeaderHeight() {
|
|
688
|
-
return HEADER_H + hostToolbarLastHeight;
|
|
689
|
-
}
|
|
690
|
-
// settings/popover bounds are main-computed; publish them into overlayDesired
|
|
691
|
-
// and let the reconciler place them (they are top-tier layers, so a reorder
|
|
692
|
-
// keeps them above every base overlay — the old raiseTopOverlays is gone).
|
|
693
|
-
function applySettingsBounds() {
|
|
694
|
-
if (!settingsView || ctx.windows.mainWindow.isDestroyed())
|
|
695
|
-
return;
|
|
696
|
-
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
697
|
-
overlayDesired.set(VIEW_ID.settings, {
|
|
698
|
-
viewId: VIEW_ID.settings,
|
|
699
|
-
placement: {
|
|
700
|
-
visible: true,
|
|
701
|
-
bounds: layout.computeSettingsBounds(w, h, overlayHeaderHeight()),
|
|
702
|
-
},
|
|
703
|
-
layer: VIEW_LAYER.settings,
|
|
704
|
-
});
|
|
705
|
-
reconcileNow();
|
|
706
|
-
}
|
|
707
|
-
function applyPopoverBounds() {
|
|
708
|
-
if (!popoverView || ctx.windows.mainWindow.isDestroyed())
|
|
709
|
-
return;
|
|
710
|
-
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
711
|
-
overlayDesired.set(VIEW_ID.popover, {
|
|
712
|
-
viewId: VIEW_ID.popover,
|
|
713
|
-
placement: {
|
|
714
|
-
visible: true,
|
|
715
|
-
bounds: layout.computePopoverBounds(w, h, overlayHeaderHeight()),
|
|
716
|
-
},
|
|
717
|
-
layer: VIEW_LAYER.popover,
|
|
718
|
-
});
|
|
719
|
-
reconcileNow();
|
|
720
|
-
}
|
|
721
|
-
function clearNativeDevtoolsRetry() {
|
|
722
|
-
nativeDevtoolsRetryToken++;
|
|
723
|
-
if (nativeDevtoolsRetryTimer) {
|
|
724
|
-
clearTimeout(nativeDevtoolsRetryTimer);
|
|
725
|
-
nativeDevtoolsRetryTimer = null;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
function closeNativeDevtoolsSource() {
|
|
729
|
-
const source = nativeDevtoolsSourceWc;
|
|
730
|
-
nativeDevtoolsSourceWc = null;
|
|
731
|
-
try {
|
|
732
|
-
stopServiceConsole?.();
|
|
733
|
-
}
|
|
734
|
-
catch { /* already stopped */ }
|
|
735
|
-
stopServiceConsole = null;
|
|
736
|
-
if (!source || source.isDestroyed())
|
|
737
|
-
return;
|
|
738
|
-
try {
|
|
739
|
-
if (source.isDevToolsOpened()) {
|
|
740
|
-
source.closeDevTools();
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
catch { /* source may be mid-destroy */ }
|
|
744
|
-
}
|
|
745
|
-
function stopFollowingNativeServiceHost() {
|
|
746
|
-
clearNativeDevtoolsRetry();
|
|
747
|
-
if (unsubscribeNativeRenderEvents) {
|
|
748
|
-
try {
|
|
749
|
-
unsubscribeNativeRenderEvents();
|
|
750
|
-
}
|
|
751
|
-
catch { /* ignore */ }
|
|
752
|
-
unsubscribeNativeRenderEvents = null;
|
|
753
|
-
}
|
|
754
|
-
closeNativeDevtoolsSource();
|
|
755
|
-
}
|
|
756
|
-
// ── Open-in-editor: click a console file link → workbench ──────────────
|
|
757
|
-
// The right-panel console is the embedded Chromium DevTools front-end. Once a
|
|
758
|
-
// sourcemap maps a console frame back to source (restored by the service-host
|
|
759
|
-
// importScripts sourcemap rewrite), we redirect a source-link click to OUR
|
|
760
|
-
// workbench editor instead of the DevTools Sources panel: install an "open
|
|
761
|
-
// resource handler" in the front-end realm that encodes (url, line, col) into
|
|
762
|
-
// a sentinel URL and asks the front-end to open it; Electron surfaces that as
|
|
763
|
-
// `devtools-open-url` on the inspected (service-host) wc, which we decode,
|
|
764
|
-
// map to a project-relative path, and reveal in the workbench WCV.
|
|
765
|
-
const openInEditorWiredWcIds = new Set();
|
|
766
|
-
function injectOpenResourceHandler(serviceWc, devtoolsWc) {
|
|
767
|
-
// Inject the front-end glue that routes a project source-link click to our
|
|
768
|
-
// Monaco editor instead of the DevTools Sources panel: a capture-phase click
|
|
769
|
-
// interceptor re-emits an encoded sentinel via
|
|
770
|
-
// `InspectorFrontendHost.openInNewTab` → Electron `devtools-open-url`. (The
|
|
771
|
-
// legacy `setOpenResourceHandler` hook this used to rely on is gone in
|
|
772
|
-
// current Chromium, so the script keeps it only as a fallback.)
|
|
773
|
-
//
|
|
774
|
-
// Destroy-race chokepoint: this runs from a deferred `dom-ready` callback
|
|
775
|
-
// (see wireOpenInEditor), by which time the inspected service-host wc may
|
|
776
|
-
// already be torn down (pool swap / project close / relaunch). Reading
|
|
777
|
-
// `serviceWc.getURL()` on a destroyed wc throws "Object has been destroyed"
|
|
778
|
-
// synchronously, which would escape the event listener. Guard both wc and
|
|
779
|
-
// wrap the body so a teardown mid-call degrades silently (mirrors
|
|
780
|
-
// customizeDevtoolsTabs).
|
|
781
|
-
if (serviceWc.isDestroyed() || devtoolsWc.isDestroyed())
|
|
782
|
-
return;
|
|
783
|
-
try {
|
|
784
|
-
const sourceContext = projectSourceContextFromServiceHostUrl(serviceWc.getURL(), ctx.workspace?.getProjectPath?.());
|
|
785
|
-
if (!sourceContext)
|
|
786
|
-
return;
|
|
787
|
-
devtoolsWc.executeJavaScript(buildDevtoolsProjectSourceLinksScript(sourceContext)).catch(() => { });
|
|
788
|
-
}
|
|
789
|
-
catch { /* wc torn down mid-call — degrade silently */ }
|
|
790
|
-
}
|
|
791
|
-
// ── DevTools tab customization: keep only Elements/Console/Network ──────────
|
|
792
|
-
// Inject into the same DevTools front-end host wc that the console/network
|
|
793
|
-
// injectors target. Reorders Elements/Console/Network to the front and closes
|
|
794
|
-
// every other panel tab by driving the front-end's UI.ViewManager /
|
|
795
|
-
// InspectorView.tabbedLocation (see ./devtools-tabs.ts). Best-effort: the
|
|
796
|
-
// injected script bounded-polls for the lazily-registered panels, wraps
|
|
797
|
-
// everything in try/catch, and silently no-ops if the API never appears —
|
|
798
|
-
// DevTools default tab behaviour is preserved on any failure. Re-injected on
|
|
799
|
-
// every (re)point so a re-`openDevTools` (service-host pool swap) re-applies.
|
|
800
|
-
function customizeDevtoolsTabs(devtoolsWc) {
|
|
801
|
-
try {
|
|
802
|
-
if (devtoolsWc.isDestroyed())
|
|
803
|
-
return;
|
|
804
|
-
const inject = () => {
|
|
805
|
-
if (devtoolsWc.isDestroyed())
|
|
806
|
-
return;
|
|
807
|
-
try {
|
|
808
|
-
void devtoolsWc.executeJavaScript(buildCustomizeTabsScript()).catch(() => { });
|
|
809
|
-
}
|
|
810
|
-
catch { /* wc torn down mid-call */ }
|
|
811
|
-
};
|
|
812
|
-
if (devtoolsWc.isLoading()) {
|
|
813
|
-
devtoolsWc.once('dom-ready', inject);
|
|
814
|
-
}
|
|
815
|
-
else {
|
|
816
|
-
inject();
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
catch { /* wc surface incomplete / torn down; degrade silently */ }
|
|
820
|
-
}
|
|
821
|
-
function wireOpenInEditor(serviceWc, devtoolsWc) {
|
|
822
|
-
// Inject the front-end handler on every (re)attach — the DevTools front-end
|
|
823
|
-
// wc is recreated per simulatorView, so a fresh host needs the handler set.
|
|
824
|
-
if (!devtoolsWc.isDestroyed()) {
|
|
825
|
-
if (devtoolsWc.isLoading()) {
|
|
826
|
-
devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(serviceWc, devtoolsWc));
|
|
827
|
-
}
|
|
828
|
-
else {
|
|
829
|
-
injectOpenResourceHandler(serviceWc, devtoolsWc);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
// Attach the `devtools-open-url` decoder to the inspected service wc once
|
|
833
|
-
// (the listener is keyed off the encoded sentinel scheme, so it only acts on
|
|
834
|
-
// OUR redirected links; any other devtools "open in new tab" falls through).
|
|
835
|
-
if (openInEditorWiredWcIds.has(serviceWc.id))
|
|
836
|
-
return;
|
|
837
|
-
openInEditorWiredWcIds.add(serviceWc.id);
|
|
838
|
-
const onOpenUrl = (_event, url) => {
|
|
839
|
-
const req = decodeOpenInEditorUrl(url);
|
|
840
|
-
if (!req)
|
|
841
|
-
return; // not our sentinel — leave it to Electron's default path
|
|
842
|
-
const target = resolveProjectEditorTarget(serviceWc.getURL(), ctx.workspace?.getProjectPath?.(), req);
|
|
843
|
-
if (!target)
|
|
844
|
-
return;
|
|
845
|
-
// Drive the workbench WCV (the sole editor) when it is attached, AND
|
|
846
|
-
// always emit `editor:openFile` — it carries the editor-agnostic mapping
|
|
847
|
-
// (project-relative path + 1-based line) that downstream/consumers (and the
|
|
848
|
-
// open-in-editor contract test) observe; with Monaco gone it has no
|
|
849
|
-
// renderer subscriber, so emitting it is harmless when the workbench
|
|
850
|
-
// handles the actual reveal.
|
|
851
|
-
openFileInWorkbench(target.path, target.line ?? 1, target.column ?? 1);
|
|
852
|
-
ctx.notify.editorOpenFile(target);
|
|
853
|
-
};
|
|
854
|
-
serviceWc.on('devtools-open-url', onOpenUrl);
|
|
855
|
-
// Consolidate teardown onto the connection layer,
|
|
856
|
-
// but as a wc-LIFETIME resource: the open-in-editor wiring inspects this
|
|
857
|
-
// service-host wc and must SURVIVE pool reuse (`reset`) — the dedup set
|
|
858
|
-
// entry + listener stay valid across sessions reusing the same wc, and the
|
|
859
|
-
// early-return dedup above correctly skips re-wiring after a reset. So we
|
|
860
|
-
// register on `'closed'` (fires only on real wc destroy), NOT `own()`
|
|
861
|
-
// (which also fires on `reset` and would leave the wc un-wired after reuse
|
|
862
|
-
// because re-pointing the same wc.id early-returns). acquire() is
|
|
863
|
-
// idempotent — bridge-router owning session-scoped resources on the same
|
|
864
|
-
// serviceWc connection coexists cleanly.
|
|
865
|
-
const conn = ctx.connections.acquire(serviceWc);
|
|
866
|
-
conn.on('closed', () => {
|
|
867
|
-
openInEditorWiredWcIds.delete(serviceWc.id);
|
|
868
|
-
try {
|
|
869
|
-
serviceWc.removeListener('devtools-open-url', onOpenUrl);
|
|
870
|
-
}
|
|
871
|
-
catch { /* wc gone */ }
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
// Point the right-panel Chrome DevTools front-end at `next` — the SERVICE HOST
|
|
875
|
-
// webContents (logic layer). Idempotent: if we already inspect this wc, no-op.
|
|
876
|
-
function pointNativeDevtoolsAtServiceWc(next) {
|
|
877
|
-
if (!simulatorView || simulatorView.webContents.isDestroyed())
|
|
878
|
-
return true;
|
|
879
|
-
if (nativeDevtoolsSourceWc?.id === next.id && !nativeDevtoolsSourceWc.isDestroyed()) {
|
|
880
|
-
return true;
|
|
881
|
-
}
|
|
882
|
-
closeNativeDevtoolsSource();
|
|
883
|
-
nativeDevtoolsSourceWc = next;
|
|
884
|
-
try {
|
|
885
|
-
next.setDevToolsWebContents(simulatorView.webContents);
|
|
886
|
-
// DevTools renders into the right-panel host view (simulatorView); with a
|
|
887
|
-
// custom host the `mode` is overridden, and `activate:false` prevents it
|
|
888
|
-
// stealing focus — this re-points whenever the service window is swapped,
|
|
889
|
-
// so a focusing window would yank focus repeatedly (disrupting the user /
|
|
890
|
-
// e2e).
|
|
891
|
-
next.openDevTools({ mode: 'detach', activate: false });
|
|
892
|
-
// Redirect console source-link clicks to the workbench editor.
|
|
893
|
-
wireOpenInEditor(next, simulatorView.webContents);
|
|
894
|
-
// Keep only Elements/Console/Network tabs (front-most, in that order).
|
|
895
|
-
// Re-applied on every re-point so a service-host pool swap (fresh
|
|
896
|
-
// openDevTools) re-asserts the custom tab bar.
|
|
897
|
-
customizeDevtoolsTabs(simulatorView.webContents);
|
|
898
|
-
// Capture service-layer console via CDP (NOT a preload monkeypatch, which
|
|
899
|
-
// would clobber native source attribution) and feed it to the console
|
|
900
|
-
// fan-out (automation `App.logAdded`). Bound to THIS service wc; replaced
|
|
901
|
-
// on the next re-point via closeNativeDevtoolsSource.
|
|
902
|
-
try {
|
|
903
|
-
stopServiceConsole?.();
|
|
904
|
-
}
|
|
905
|
-
catch { /* already stopped */ }
|
|
906
|
-
stopServiceConsole = installServiceConsoleForward({
|
|
907
|
-
serviceWc: next,
|
|
908
|
-
connections: ctx.connections,
|
|
909
|
-
emit: (entry) => ctx.consoleForwarder?.emit(entry),
|
|
910
|
-
}).stop;
|
|
911
|
-
return true;
|
|
912
|
-
}
|
|
913
|
-
catch {
|
|
914
|
-
if (nativeDevtoolsSourceWc?.id === next.id) {
|
|
915
|
-
nativeDevtoolsSourceWc = null;
|
|
916
|
-
}
|
|
917
|
-
return false;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
// Resolve the SERVICE HOST webContents for the active (or named) app. This is
|
|
921
|
-
// the hidden service BrowserWindow's wc — a top-level wc that CAN host a
|
|
922
|
-
// Chrome DevTools front-end (unlike a `<webview>` guest's). Re-resolved fresh
|
|
923
|
-
// so a pre-warm-pool swap on respawn is tolerated.
|
|
924
|
-
function pointNativeDevtoolsAtActiveServiceHost(appId) {
|
|
925
|
-
if (!ctx.bridge?.isNativeHost())
|
|
926
|
-
return true;
|
|
927
|
-
if (!simulatorView || simulatorView.webContents.isDestroyed())
|
|
928
|
-
return true;
|
|
929
|
-
const wc = ctx.bridge.getServiceWc(appId);
|
|
930
|
-
if (!wc || wc.isDestroyed())
|
|
931
|
-
return false;
|
|
932
|
-
return pointNativeDevtoolsAtServiceWc(wc);
|
|
933
|
-
}
|
|
934
|
-
function scheduleNativeDevtoolsFollow(appId, attempt = 0) {
|
|
935
|
-
if (attempt >= 20)
|
|
936
|
-
return;
|
|
937
|
-
if (!ctx.bridge?.isNativeHost())
|
|
938
|
-
return;
|
|
939
|
-
const token = nativeDevtoolsRetryToken;
|
|
940
|
-
if (nativeDevtoolsRetryTimer)
|
|
941
|
-
clearTimeout(nativeDevtoolsRetryTimer);
|
|
942
|
-
nativeDevtoolsRetryTimer = setTimeout(() => {
|
|
943
|
-
nativeDevtoolsRetryTimer = null;
|
|
944
|
-
if (token !== nativeDevtoolsRetryToken)
|
|
945
|
-
return;
|
|
946
|
-
if (pointNativeDevtoolsAtActiveServiceHost(appId))
|
|
947
|
-
return;
|
|
948
|
-
scheduleNativeDevtoolsFollow(appId, attempt + 1);
|
|
949
|
-
}, 50);
|
|
950
|
-
}
|
|
951
|
-
function followNativeDevtoolsServiceHost(appId) {
|
|
952
|
-
clearNativeDevtoolsRetry();
|
|
953
|
-
if (pointNativeDevtoolsAtActiveServiceHost(appId))
|
|
954
|
-
return;
|
|
955
|
-
scheduleNativeDevtoolsFollow(appId);
|
|
956
|
-
}
|
|
957
|
-
// Render-side activity (a page DOM mounting / the visible page changing)
|
|
958
|
-
// always follows a spawn or respawn, by which point the service window exists
|
|
959
|
-
// (and may have been swapped by the pool). Re-resolve + re-point the DevTools
|
|
960
|
-
// at the now-current service host on every such event.
|
|
961
|
-
function onNativeRenderEvent(event) {
|
|
962
|
-
if (event.kind !== 'activePage' && event.kind !== 'domReady')
|
|
963
|
-
return;
|
|
964
|
-
followNativeDevtoolsServiceHost(event.appId);
|
|
965
|
-
}
|
|
966
|
-
// ── ViewManager methods ─────────────────────────────────────────────────
|
|
967
|
-
function attachNativeSimulatorDevtoolsHost() {
|
|
968
|
-
stopFollowingNativeServiceHost();
|
|
969
|
-
// Destroy old simulatorView to prevent WebContentsView leak
|
|
970
|
-
if (simulatorView) {
|
|
971
|
-
removeSimulatorDevtoolsView();
|
|
972
|
-
try {
|
|
973
|
-
if (!simulatorView.webContents.isDestroyed()) {
|
|
974
|
-
simulatorView.webContents.close();
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
catch { /* ignore */ }
|
|
978
|
-
simulatorView = null;
|
|
979
|
-
}
|
|
980
|
-
simulatorView = new WebContentsView();
|
|
981
|
-
// Default DevTools to Console panel (Chrome DevTools defaults to Elements).
|
|
982
|
-
// The DevTools UI lives inside closed shadow roots, so a light-DOM
|
|
983
|
-
// querySelector('[role="tab"]') cannot reach the tab bar to click it.
|
|
984
|
-
// Instead drive the front-end's own view manager: the bundled DevTools
|
|
985
|
-
// exposes `UI.ViewManager.instance().showView(id)` on `globalThis.UI`
|
|
986
|
-
// once the front-end has finished bootstrapping. We poll for it and
|
|
987
|
-
// request the `console` view, and also persist the choice via the
|
|
988
|
-
// `panel-selectedTab` localStorage key so subsequent reloads honor it.
|
|
989
|
-
const devtoolsWc = simulatorView.webContents;
|
|
990
|
-
// The boot-time burst of `executeJavaScript()` injects on this host wc (tab
|
|
991
|
-
// customization / console default / Elements+Network forwarding below) each
|
|
992
|
-
// queue a pending `did-stop-loading` waiter while the front-end is still
|
|
993
|
-
// loading (see raiseExecuteJavaScriptListenerCeiling).
|
|
994
|
-
raiseExecuteJavaScriptListenerCeiling(devtoolsWc);
|
|
995
|
-
// Hand the network forwarder THIS front-end host wc — it injects the
|
|
996
|
-
// simulator WCV's Network.* CDP events into `window.DevToolsAPI.dispatchMessage`
|
|
997
|
-
// here so the native Network tab renders them (falls back to the service-host
|
|
998
|
-
// console line when null / the API never appears). Cleared on detach.
|
|
999
|
-
ctx.networkForward?.setDevtoolsHost(devtoolsWc);
|
|
1000
|
-
// Elements forwarding (production, always on for the native simulator): route
|
|
1001
|
-
// the front-end's Elements-panel CDP traffic (DOM/CSS/Overlay/DOMSnapshot/
|
|
1002
|
-
// DOMDebugger) onto the ACTIVE RENDER GUEST so the panel reflects the page's
|
|
1003
|
-
// live DOM tree instead of the service host it natively inspects. Reuses the
|
|
1004
|
-
// safe-area-attached debugger session (never detaches one it doesn't own);
|
|
1005
|
-
// Emulation.* and every other domain stay on the service-host path. Degrades
|
|
1006
|
-
// to the native service-host DOM if the front-end hook is unavailable. The
|
|
1007
|
-
// disposer is stopped on detach / host destroyed.
|
|
1008
|
-
if (ctx.bridge) {
|
|
1009
|
-
try {
|
|
1010
|
-
stopElementsForward?.();
|
|
1011
|
-
}
|
|
1012
|
-
catch { /* prior disposer already gone */ }
|
|
1013
|
-
// Capture THIS instance's disposer in the destroy handler — NOT the mutable
|
|
1014
|
-
// `stopElementsForward`. A respawn creates a new simulatorView whose old
|
|
1015
|
-
// devtools host wc is destroyed AFTER the new instance is already installed;
|
|
1016
|
-
// a handler closing over `stopElementsForward` would then dispose the CURRENT
|
|
1017
|
-
// instance (its reconcile loop never gets to install the hook, so Elements
|
|
1018
|
-
// falls back to the natively-inspected service host). Stop only this wc's own
|
|
1019
|
-
// forward, and clear the module pointer only while it still points here.
|
|
1020
|
-
const thisForward = installElementsForward({ devtoolsWc, bridge: ctx.bridge, connections: ctx.connections });
|
|
1021
|
-
stopElementsForward = thisForward;
|
|
1022
|
-
devtoolsWc.once('destroyed', () => {
|
|
1023
|
-
try {
|
|
1024
|
-
thisForward();
|
|
1025
|
-
}
|
|
1026
|
-
catch { /* already stopped */ }
|
|
1027
|
-
if (stopElementsForward === thisForward)
|
|
1028
|
-
stopElementsForward = null;
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
devtoolsWc.once('dom-ready', () => {
|
|
1032
|
-
devtoolsWc.executeJavaScript(`
|
|
1033
|
-
(function() {
|
|
1034
|
-
try { localStorage.setItem('panel-selectedTab', '"console"') } catch {}
|
|
1035
|
-
let tries = 0
|
|
1036
|
-
const timer = setInterval(() => {
|
|
1037
|
-
tries++
|
|
1038
|
-
try {
|
|
1039
|
-
const UI = globalThis.UI
|
|
1040
|
-
const vm = UI && UI.ViewManager && typeof UI.ViewManager.instance === 'function'
|
|
1041
|
-
? UI.ViewManager.instance()
|
|
1042
|
-
: null
|
|
1043
|
-
if (vm && typeof vm.showView === 'function') {
|
|
1044
|
-
vm.showView('console')
|
|
1045
|
-
clearInterval(timer)
|
|
1046
|
-
return
|
|
1047
|
-
}
|
|
1048
|
-
} catch {}
|
|
1049
|
-
if (tries > 80) clearInterval(timer)
|
|
1050
|
-
}, 50)
|
|
1051
|
-
})()
|
|
1052
|
-
`).catch(() => { });
|
|
1053
|
-
});
|
|
1054
|
-
// Anchor-only mount: the renderer's published rect is the SOLE authority.
|
|
1055
|
-
// If a non-zero rect was already published (it can land before this attach
|
|
1056
|
-
// on the project-open ordering), replay it; otherwise the view stays
|
|
1057
|
-
// unadded and unsized until the first publish arrives. No static-layout
|
|
1058
|
-
// fallback — an attach-time computed rect raced the precise anchor rect
|
|
1059
|
-
// and flashed the overlay at the wrong rectangle.
|
|
1060
|
-
// The view now exists: re-run the reconciler so a placement published before
|
|
1061
|
-
// this attach (project-open ordering) attaches it once the gate re-opens.
|
|
1062
|
-
reconcileNow();
|
|
1063
|
-
if (ctx.bridge?.isNativeHost()) {
|
|
1064
|
-
unsubscribeNativeRenderEvents = ctx.bridge.onRenderEvent(onNativeRenderEvent);
|
|
1065
|
-
followNativeDevtoolsServiceHost();
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Remove the custom-apis bridge `ipcMain.on` listener, if any. Idempotent.
|
|
1070
|
-
*/
|
|
1071
|
-
function detachNativeCustomApiBridge() {
|
|
1072
|
-
if (nativeCustomApiBridgeHandler) {
|
|
1073
|
-
ipcMain.removeListener(SimulatorCustomApiBridgeChannel.Request, nativeCustomApiBridgeHandler);
|
|
1074
|
-
nativeCustomApiBridgeHandler = null;
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* The single teardown path for the live native-host simulator WCV, shared by
|
|
1079
|
-
* relaunch (attachNativeSimulator replacing the view) and project close
|
|
1080
|
-
* (detachSimulator). Detaches the view from the window, then synchronously
|
|
1081
|
-
* clears its bridge sessions (render guests + service host + mappings) BEFORE
|
|
1082
|
-
* the WCV's own async close(), so the next project never re-resolves,
|
|
1083
|
-
* re-renders, or screenshots the outgoing guest. The sync prefix clears the
|
|
1084
|
-
* maps immediately; the async tail (pool / resource-server release) is observed
|
|
1085
|
-
* so a rejection is logged rather than swallowed. The simulatorWc 'destroyed'
|
|
1086
|
-
* hook in bridge-router stays as an idempotent fallback. No-op when no view is
|
|
1087
|
-
* live; idempotent (disposeSessionsForSimulator early-returns once a session is
|
|
1088
|
-
* gone), so the eager teardown here and the 'destroyed' fallback never
|
|
1089
|
-
* double-dispose. `label` only tags the diagnostic on the async-tail failure.
|
|
1090
|
-
*/
|
|
1091
|
-
function tearDownNativeSimulatorView(label) {
|
|
1092
|
-
if (!nativeSimulatorView)
|
|
1093
|
-
return;
|
|
1094
|
-
if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
1095
|
-
try {
|
|
1096
|
-
ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
|
|
1097
|
-
}
|
|
1098
|
-
catch { /* already removed */ }
|
|
1099
|
-
}
|
|
1100
|
-
try {
|
|
1101
|
-
if (!nativeSimulatorView.webContents.isDestroyed()) {
|
|
1102
|
-
ctx.bridge?.disposeSessionsForSimulator?.(nativeSimulatorView.webContents.id)
|
|
1103
|
-
?.catch((err) => console.warn(`[view-manager] dispose sessions (${label}) failed:`, err));
|
|
1104
|
-
nativeSimulatorView.webContents.close();
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
catch { /* ignore */ }
|
|
1108
|
-
nativeSimulatorView = null;
|
|
1109
|
-
nativeSimulatorViewAdded = false;
|
|
1110
|
-
// The view is gone: gateReadiness now hides it, so reconcile detaches it.
|
|
1111
|
-
reconcileNow();
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
|
|
1115
|
-
* NO embedder renderer, so the simulator-side `__diminaCustomApis` bridge
|
|
1116
|
-
* (`src/preload/runtime/custom-apis.ts`) cannot reach the host via
|
|
1117
|
-
* `ipcRenderer.sendToHost` — that only delivers to a `<webview>`'s embedder,
|
|
1118
|
-
* which is how the default path's `useCustomApiProxy` answered it. A
|
|
1119
|
-
* top-level WebContentsView has no embedder, and `sendToHost` does NOT loop
|
|
1120
|
-
* back as `ipc-message-host` on itself, so that channel never fires here.
|
|
1121
|
-
*
|
|
1122
|
-
* Under native-host the bridge instead talks to `ipcMain` directly (the same
|
|
1123
|
-
* way `installNativeHostBridge` issues SPAWN/PAGE_OPEN): the preload sends
|
|
1124
|
-
* `SimulatorCustomApiBridgeChannel.Request` via `ipcRenderer.send`, this
|
|
1125
|
-
* `ipcMain.on` listener answers it. We do NOT route through `IpcRegistry` /
|
|
1126
|
-
* the sender-policy white-list (the simulator is deliberately kept off it);
|
|
1127
|
-
* instead we accept the message ONLY when `event.sender` is THIS precise
|
|
1128
|
-
* simWc — the same trust model bridge-router uses for the simulator's own
|
|
1129
|
-
* SPAWN messages. The result is dispatched through the shared
|
|
1130
|
-
* `ctx.simulatorApis` registry and the id-correlated `Response` is posted
|
|
1131
|
-
* back via `simWc.send`, which the preload's `ipcRenderer.on(Response)`
|
|
1132
|
-
* listener settles.
|
|
1133
|
-
*/
|
|
1134
|
-
function attachNativeCustomApiBridge(simWc) {
|
|
1135
|
-
// Re-attach defensively: a stale listener from a prior simWc must never
|
|
1136
|
-
// linger. (attachNativeSimulator already tears the old view down first.)
|
|
1137
|
-
detachNativeCustomApiBridge();
|
|
1138
|
-
const apis = ctx.simulatorApis;
|
|
1139
|
-
if (!apis)
|
|
1140
|
-
return;
|
|
1141
|
-
const simWcId = simWc.id;
|
|
1142
|
-
const handler = (event, req) => {
|
|
1143
|
-
// Trust gate: only the exact native simulator webContents may drive this.
|
|
1144
|
-
if (event.sender.id !== simWcId)
|
|
1145
|
-
return;
|
|
1146
|
-
const r = req;
|
|
1147
|
-
if (!r || typeof r.id !== 'number')
|
|
1148
|
-
return;
|
|
1149
|
-
void handleCustomApiBridgeRequest(apis, r).then((response) => {
|
|
1150
|
-
if (simWc.isDestroyed())
|
|
1151
|
-
return;
|
|
1152
|
-
simWc.send(SimulatorCustomApiBridgeChannel.Response, response);
|
|
1153
|
-
}).catch(() => { });
|
|
1154
|
-
};
|
|
1155
|
-
nativeCustomApiBridgeHandler = handler;
|
|
1156
|
-
ipcMain.on(SimulatorCustomApiBridgeChannel.Request, handler);
|
|
1157
|
-
// Consolidate this per-webContents teardown onto the connection layer:
|
|
1158
|
-
// acquiring the simWc connection makes the
|
|
1159
|
-
// registry track this trusted webContents, and `own()`-ing the detach ties
|
|
1160
|
-
// the ipcMain listener's lifetime to the connection so it's torn down
|
|
1161
|
-
// deterministically when the wc is destroyed (or the connection is reset),
|
|
1162
|
-
// instead of a bespoke `once('destroyed')`. The detach stays idempotent, so
|
|
1163
|
-
// the defensive re-attach path calling it directly remains safe.
|
|
1164
|
-
ctx.connections.acquire(simWc).own(detachNativeCustomApiBridge);
|
|
1165
|
-
}
|
|
1166
|
-
function attachNativeSimulator(simulatorUrl, _simWidth) {
|
|
1167
|
-
if (!ctx.preloadPath) {
|
|
1168
|
-
console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
|
|
1169
|
-
return Promise.resolve();
|
|
1170
|
-
}
|
|
1171
|
-
// Unblock a superseded IPC invocation. Its renderer effect cleanup marks
|
|
1172
|
-
// that generation cancelled, so this cannot schedule a stale capture.
|
|
1173
|
-
settleNativeSimulatorReady?.();
|
|
1174
|
-
settleNativeSimulatorReady = null;
|
|
1175
|
-
// Tear down any previous native simulator view (relaunch / re-open) through
|
|
1176
|
-
// the shared single teardown path. detachNativeCustomApiBridge stays here
|
|
1177
|
-
// (the bridge dispatcher is re-installed per attach below).
|
|
1178
|
-
if (nativeSimulatorView) {
|
|
1179
|
-
detachNativeCustomApiBridge();
|
|
1180
|
-
tearDownNativeSimulatorView('relaunch');
|
|
1181
|
-
}
|
|
1182
|
-
nativeSimulatorProjectPath = null;
|
|
1183
|
-
const ready = new Promise((resolve) => {
|
|
1184
|
-
settleNativeSimulatorReady = resolve;
|
|
1185
|
-
});
|
|
1186
|
-
// Derive THIS project's session partition from the simulator URL's appId so
|
|
1187
|
-
// its cookies/localStorage/cache are isolated from every other project.
|
|
1188
|
-
// Same project → same partition (storage survives a relaunch);
|
|
1189
|
-
// unknown appId → the shared fallback. Configure the partition's session
|
|
1190
|
-
// (protocol handlers + CORS/referer policy) before any project content loads
|
|
1191
|
-
// on it — idempotent per partition.
|
|
1192
|
-
const route = parseRoute(simulatorUrl);
|
|
1193
|
-
// Include the project path so two projects that declare the same appId at
|
|
1194
|
-
// different paths get isolated partitions. The service-host window for THIS
|
|
1195
|
-
// project derives its partition from the same (appId, projectPath) pair so
|
|
1196
|
-
// render guests + service host still share one session.
|
|
1197
|
-
const partition = miniappPartition(route?.appId, ctx.workspace?.getProjectPath());
|
|
1198
|
-
configureMiniappSession(partition);
|
|
1199
|
-
// The simulator preload is a CJS bundle; webPreferences.preload obeys the
|
|
1200
|
-
// `.js` + "type":"module" ESM rule (require would be undefined), so hand the
|
|
1201
|
-
// top-level WebContentsView the `.cjs` sibling. contextIsolation:false +
|
|
1202
|
-
// sandbox:false + webviewTag:true mirror what the default `<webview>` guest
|
|
1203
|
-
// runs with, and the per-project `persist:miniapp-<key>` partition shares
|
|
1204
|
-
// storage + the session-registered preload/CORS rules with the rest of THIS
|
|
1205
|
-
// project's simulator (render guests + service host), never other projects'.
|
|
1206
|
-
const view = new WebContentsView({
|
|
1207
|
-
webPreferences: {
|
|
1208
|
-
nodeIntegration: false,
|
|
1209
|
-
contextIsolation: false,
|
|
1210
|
-
sandbox: false,
|
|
1211
|
-
webviewTag: true,
|
|
1212
|
-
preload: cjsSiblingPreloadPath(ctx.preloadPath),
|
|
1213
|
-
partition,
|
|
1214
|
-
},
|
|
1215
|
-
});
|
|
1216
|
-
nativeSimulatorView = view;
|
|
1217
|
-
nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
|
|
1218
|
-
// Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
|
|
1219
|
-
// light #e8e8e8) so a height-resize that grows the region never flashes a
|
|
1220
|
-
// mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
|
|
1221
|
-
// and the renderer placeholder behind it are all the same color.
|
|
1222
|
-
view.setBackgroundColor(simDeskBg());
|
|
1223
|
-
const simWc = view.webContents;
|
|
1224
|
-
// Keep the WCV surface in sync with the active color scheme. The
|
|
1225
|
-
// process-wide installThemeBackgroundSync() re-syncs BrowserWindows on a
|
|
1226
|
-
// theme switch, but this top-level WebContentsView is not a window, so its
|
|
1227
|
-
// creation-time backgroundColor would otherwise freeze. Mirror simDeskBg()
|
|
1228
|
-
// here on every nativeTheme `updated`; the listener is owned by the wc's
|
|
1229
|
-
// connection so it detaches when the simulator view is torn down.
|
|
1230
|
-
const syncDeskBg = () => {
|
|
1231
|
-
try {
|
|
1232
|
-
if (!simWc.isDestroyed())
|
|
1233
|
-
view.setBackgroundColor(simDeskBg());
|
|
1234
|
-
}
|
|
1235
|
-
catch { /* view/wc gone */ }
|
|
1236
|
-
};
|
|
1237
|
-
nativeTheme.on('updated', syncDeskBg);
|
|
1238
|
-
ctx.connections.acquire(simWc).own(() => nativeTheme.removeListener('updated', syncDeskBg));
|
|
1239
|
-
// Service the simulator-side `__diminaCustomApis` bridge: this top-level
|
|
1240
|
-
// WebContentsView has no embedder renderer to proxy through, so dispatch its
|
|
1241
|
-
// `sendToHost` requests straight to `ctx.simulatorApis` from main.
|
|
1242
|
-
attachNativeCustomApiBridge(simWc);
|
|
1243
|
-
// DeviceShell mounts per-page render-host `<webview>`s INSIDE this view.
|
|
1244
|
-
// Pin them onto the SAME per-project partition as their host WCV (so render
|
|
1245
|
-
// and the rest of this project share one localStorage/cookie jar) and run
|
|
1246
|
-
// them with contextIsolation/sandbox off so the render runtime + its preload
|
|
1247
|
-
// share the page realm. (A top-level WebContentsView can host these guests; a
|
|
1248
|
-
// `<webview>` guest cannot — that's the whole point of Option A.)
|
|
1249
|
-
// Page type (`isTab`) of each attaching guest, captured from its render-host
|
|
1250
|
-
// URL in will-attach (where `params.src` carries the full URL) and consumed
|
|
1251
|
-
// FIFO in the matching did-attach — `guestWc.getURL()` is still empty there.
|
|
1252
|
-
// Per-attach scope: a fresh simWc + handlers are built on every (re)attach.
|
|
1253
|
-
const pendingGuestIsTab = [];
|
|
1254
|
-
simWc.on('will-attach-webview', (_event, webPreferences, params) => {
|
|
1255
|
-
;
|
|
1256
|
-
webPreferences.partition = partition;
|
|
1257
|
-
params.partition = partition;
|
|
1258
|
-
webPreferences.contextIsolation = false;
|
|
1259
|
-
webPreferences.sandbox = false;
|
|
1260
|
-
let isTab = false;
|
|
1261
|
-
try {
|
|
1262
|
-
isTab = new URL(params.src).searchParams.get('isTab') === '1';
|
|
1263
|
-
}
|
|
1264
|
-
catch { /* keep false */ }
|
|
1265
|
-
pendingGuestIsTab.push(isTab);
|
|
1266
|
-
});
|
|
1267
|
-
simWc.on('did-attach-webview', (_event, guestWc) => {
|
|
1268
|
-
// Scale the nested render-host page with the device zoom. The host WCV is
|
|
1269
|
-
// sized to the SCALED bezel rect and runs at `currentZoomFactor`, so the
|
|
1270
|
-
// guest must run at the same factor to lay the page out at the logical
|
|
1271
|
-
// device width and paint at the right scale. At 100% (default) this is a
|
|
1272
|
-
// no-op identity (factor 1). Newly-attached guests pick up the latest
|
|
1273
|
-
// zoom here; live zoom changes re-apply via setNativeSimulatorViewBounds.
|
|
1274
|
-
try {
|
|
1275
|
-
guestWc.setZoomFactor(currentZoomFactor);
|
|
1276
|
-
}
|
|
1277
|
-
catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ }
|
|
1278
|
-
// Simulate this device's CSS env(safe-area-inset-*) on the fresh guest
|
|
1279
|
-
// before it paints, so notch-aware page layout resolves correctly. The
|
|
1280
|
-
// bottom inset is page-type-dependent (see services/safe-area): a tab
|
|
1281
|
-
// page's content sits above the shell tabBar (bottom 0); a non-tab page
|
|
1282
|
-
// is full-bleed (real bottom inset). The page type was captured from the
|
|
1283
|
-
// render-host URL in will-attach (FIFO).
|
|
1284
|
-
const isTabGuest = pendingGuestIsTab.shift() ?? false;
|
|
1285
|
-
safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
|
|
1286
|
-
guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
1287
|
-
guestWc.on('will-navigate', (e, url) => {
|
|
1288
|
-
try {
|
|
1289
|
-
const u = new URL(url);
|
|
1290
|
-
if (u.protocol === 'about:')
|
|
1291
|
-
return;
|
|
1292
|
-
if ((u.protocol === 'http:' || u.protocol === 'https:')
|
|
1293
|
-
&& (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
if (u.protocol === 'file:')
|
|
1297
|
-
return;
|
|
1298
|
-
e.preventDefault();
|
|
1299
|
-
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
1300
|
-
void shell.openExternal(url);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
catch {
|
|
1304
|
-
e.preventDefault();
|
|
1305
|
-
}
|
|
1306
|
-
});
|
|
1307
|
-
// The outer DeviceShell loading is insufficient: slow projects attach a
|
|
1308
|
-
// render guest later. Resolve the renderer's attach IPC only after that
|
|
1309
|
-
// first mini-app page has completed its own document load.
|
|
1310
|
-
guestWc.once('did-finish-load', () => {
|
|
1311
|
-
if (nativeSimulatorView !== view || simWc.isDestroyed())
|
|
1312
|
-
return;
|
|
1313
|
-
settleNativeSimulatorReady?.();
|
|
1314
|
-
settleNativeSimulatorReady = null;
|
|
1315
|
-
});
|
|
1316
|
-
// A render-host guest only attaches after a spawn, so the service window
|
|
1317
|
-
// exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
|
|
1318
|
-
// with the `onRenderEvent` path in case its emit lost the attach race.
|
|
1319
|
-
followNativeDevtoolsServiceHost();
|
|
1320
|
-
});
|
|
1321
|
-
// The simulator loads http://localhost:<port>/simulator.html. Harden popups
|
|
1322
|
-
// + in-place navigation: allow the dev server origin (and about:blank /
|
|
1323
|
-
// file:// render hosts), route everything else to the OS browser.
|
|
1324
|
-
simWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
1325
|
-
simWc.on('will-navigate', (e, url) => {
|
|
1326
|
-
try {
|
|
1327
|
-
const u = new URL(url);
|
|
1328
|
-
if (u.protocol === 'about:')
|
|
1329
|
-
return;
|
|
1330
|
-
if (u.protocol === 'file:')
|
|
1331
|
-
return;
|
|
1332
|
-
if ((u.protocol === 'http:' || u.protocol === 'https:')
|
|
1333
|
-
&& (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
e.preventDefault();
|
|
1337
|
-
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
1338
|
-
void shell.openExternal(url);
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
catch {
|
|
1342
|
-
e.preventDefault();
|
|
1343
|
-
}
|
|
1344
|
-
});
|
|
1345
|
-
void simWc.loadURL(simulatorUrl).catch((err) => {
|
|
1346
|
-
console.error('[workbench] attachNativeSimulator — loadURL failed', err);
|
|
1347
|
-
});
|
|
1348
|
-
// This WebContentsView's webContents IS the simulator: the native-host
|
|
1349
|
-
// preload runs here and issues SPAWN, so bridge-router resolves
|
|
1350
|
-
// ap.simulatorWc = event.sender = this wc, and SIMULATOR_EVENTS flow back.
|
|
1351
|
-
simulatorWebContentsId = simWc.id;
|
|
1352
|
-
// Forward this WCV's network requests (wx.request/download/upload run here,
|
|
1353
|
-
// not in the service host) into the service-host console so they show in the
|
|
1354
|
-
// embedded DevTools the right panel hosts. Attaches the CDP debugger; no-op
|
|
1355
|
-
// if the forwarder is unwired (partial test ctx) or the debugger is claimed.
|
|
1356
|
-
ctx.networkForward?.attachSimulator(simWc);
|
|
1357
|
-
// Model A: bounds come ONLY from the renderer's window-level placement
|
|
1358
|
-
// snapshot. The view now exists, so re-reconcile: gateReadiness admits the
|
|
1359
|
-
// simulator (gated hidden while its WCV was absent) and the latest desired
|
|
1360
|
-
// placement attaches + sizes it. Until the next snapshot it stays unadded.
|
|
1361
|
-
reconcileNow();
|
|
1362
|
-
// Native-host page code (console.log / wx.request / Sources) runs in the
|
|
1363
|
-
// hidden SERVICE HOST window, not this DeviceShell host nor the render-host
|
|
1364
|
-
// guests. Keep the right-panel DevTools host view stable and point it at the
|
|
1365
|
-
// service host so its Console/Network(fetch)/Sources reflect the logic layer.
|
|
1366
|
-
// (The UI/view layer's Elements equivalent is the native WXML panel +
|
|
1367
|
-
// render-guest highlight chain, which targets the active render guest.)
|
|
1368
|
-
attachNativeSimulatorDevtoolsHost();
|
|
1369
|
-
return ready;
|
|
1370
|
-
}
|
|
1371
|
-
function detachSimulator() {
|
|
1372
|
-
settleNativeSimulatorReady?.();
|
|
1373
|
-
settleNativeSimulatorReady = null;
|
|
1374
|
-
stopFollowingNativeServiceHost();
|
|
1375
|
-
detachNativeCustomApiBridge();
|
|
1376
|
-
// Stop Elements forwarding (detaches only the debugger sessions IT attached;
|
|
1377
|
-
// safe-area's sessions are untouched). Idempotent — the host 'destroyed'
|
|
1378
|
-
// handler may have already run.
|
|
1379
|
-
try {
|
|
1380
|
-
stopElementsForward?.();
|
|
1381
|
-
}
|
|
1382
|
-
catch { /* already stopped */ }
|
|
1383
|
-
stopElementsForward = null;
|
|
1384
|
-
// Stop forwarding the simulator WCV's network (its debugger is detached as
|
|
1385
|
-
// the view is closed below, but drop our session deterministically first).
|
|
1386
|
-
// Also drop the DevTools front-end host — its wc is destroyed below; a stale
|
|
1387
|
-
// ref would make the forwarder dispatch into a dead wc instead of falling
|
|
1388
|
-
// back to the console.
|
|
1389
|
-
ctx.networkForward?.detachSimulator();
|
|
1390
|
-
ctx.networkForward?.setDevtoolsHost(null);
|
|
1391
|
-
// Native-host simulator content view (no-op in the default path) — same
|
|
1392
|
-
// single teardown path as relaunch. detachNativeCustomApiBridge was already
|
|
1393
|
-
// called above.
|
|
1394
|
-
tearDownNativeSimulatorView('close');
|
|
1395
|
-
hidePopover();
|
|
1396
|
-
// Drop the settings view too — the previous detachAllViews() did.
|
|
1397
|
-
destroyViewInternal(settingsView);
|
|
1398
|
-
settingsView = null;
|
|
1399
|
-
destroyViewInternal(simulatorView);
|
|
1400
|
-
simulatorView = null;
|
|
1401
|
-
simulatorViewAdded = false;
|
|
1402
|
-
simulatorWebContentsId = null;
|
|
1403
|
-
nativeSimulatorProjectPath = null;
|
|
1404
|
-
}
|
|
1405
|
-
// Remove (but do not destroy) the DevTools overlay from the contentView.
|
|
1406
|
-
// Internal teardown helper for re-attach; user-facing visibility is the
|
|
1407
|
-
// anchor 0×0 single path (`setSimulatorDevtoolsBounds`).
|
|
1408
|
-
function removeSimulatorDevtoolsView() {
|
|
1409
|
-
if (simulatorView && simulatorViewAdded) {
|
|
1410
|
-
try {
|
|
1411
|
-
ctx.windows.mainWindow.contentView.removeChildView(simulatorView);
|
|
1412
|
-
}
|
|
1413
|
-
catch (e) {
|
|
1414
|
-
console.error('[workbench] removeSimulatorDevtoolsView error', e);
|
|
1415
|
-
}
|
|
1416
|
-
simulatorViewAdded = false;
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
async function showSettings() {
|
|
1420
|
-
if (!settingsView) {
|
|
1421
|
-
settingsView = new WebContentsView({
|
|
1422
|
-
webPreferences: {
|
|
1423
|
-
nodeIntegration: false,
|
|
1424
|
-
contextIsolation: true,
|
|
1425
|
-
sandbox: false,
|
|
1426
|
-
preload: mainPreloadPath,
|
|
1427
|
-
},
|
|
1428
|
-
});
|
|
1429
|
-
// Overlay loads mainPreloadPath, so the same navigation rules as the
|
|
1430
|
-
// main window apply — see navigation-hardening.ts.
|
|
1431
|
-
applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
|
|
1432
|
-
// Transparent backing: the settings view now spans the whole content area
|
|
1433
|
-
// (computeSettingsBounds) and its renderer paints a transparent backdrop +
|
|
1434
|
-
// an opaque right-side panel, so the underlying editor/simulator show
|
|
1435
|
-
// through and a backdrop click closes the overlay (mirrors the popover).
|
|
1436
|
-
settingsView.setBackgroundColor('#00000000');
|
|
1437
|
-
await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
|
|
1438
|
-
}
|
|
1439
|
-
applySettingsBounds();
|
|
1440
|
-
}
|
|
1441
|
-
function hideSettings() {
|
|
1442
|
-
overlayDesired.delete(VIEW_ID.settings);
|
|
1443
|
-
reconcileNow();
|
|
1444
|
-
}
|
|
1445
|
-
function showPopover(data) {
|
|
1446
|
-
hidePopover();
|
|
1447
|
-
const popover = new WebContentsView({
|
|
1448
|
-
webPreferences: {
|
|
1449
|
-
nodeIntegration: false,
|
|
1450
|
-
contextIsolation: true,
|
|
1451
|
-
sandbox: false,
|
|
1452
|
-
preload: mainPreloadPath,
|
|
1453
|
-
},
|
|
1454
|
-
});
|
|
1455
|
-
// Popover overlay loads mainPreloadPath — same navigation rules apply.
|
|
1456
|
-
applyNavigationHardening(popover.webContents, ctx.rendererDir);
|
|
1457
|
-
popoverView = popover;
|
|
1458
|
-
popover.setBackgroundColor('#00000000');
|
|
1459
|
-
applyPopoverBounds();
|
|
1460
|
-
popover.webContents.once('did-finish-load', () => {
|
|
1461
|
-
ctx.notify.popoverInit(popover, data);
|
|
1462
|
-
});
|
|
1463
|
-
popover.webContents.loadFile(path.join(ctx.rendererDir, 'entries/popover/index.html'));
|
|
1464
|
-
}
|
|
1465
|
-
function hidePopover() {
|
|
1466
|
-
if (!popoverView)
|
|
1467
|
-
return;
|
|
1468
|
-
overlayDesired.delete(VIEW_ID.popover);
|
|
1469
|
-
// Destroy the WCV first (removeChildView + close), THEN reconcile: the detach
|
|
1470
|
-
// op then finds the view already gone and does not double-removeChildView.
|
|
1471
|
-
destroyViewInternal(popoverView);
|
|
1472
|
-
popoverView = null;
|
|
1473
|
-
reconcileNow();
|
|
1474
|
-
ctx.notify.popoverClosed();
|
|
1475
|
-
}
|
|
1476
|
-
function repositionAll() {
|
|
1477
|
-
// Simulator + DevTools overlay: bounds owned by the renderer's anchor
|
|
1478
|
-
// publishes; their ResizeObserver/window-resize listeners re-measure, so
|
|
1479
|
-
// no static re-apply here.
|
|
1480
|
-
if (overlayDesired.has(VIEW_ID.settings))
|
|
1481
|
-
applySettingsBounds();
|
|
1482
|
-
if (overlayDesired.has(VIEW_ID.popover))
|
|
1483
|
-
applyPopoverBounds();
|
|
29
|
+
// hostToolbar and the overlay panels form a height cycle: the toolbar height
|
|
30
|
+
// offsets the overlays' top edge, and a toolbar height change re-applies the
|
|
31
|
+
// present overlays. The toolbar receives a thunk into the (hoisted) function
|
|
32
|
+
// declaration below so the reference resolves at runtime — after the overlay
|
|
33
|
+
// panels exist — while both domains stay plain const bindings.
|
|
34
|
+
const hostToolbar = createHostToolbarView(ctx, reconciler, {
|
|
35
|
+
reapplyPresentOverlays: () => reapplyToolbarDependentOverlays(),
|
|
36
|
+
});
|
|
37
|
+
const overlayPanels = createOverlayPanelsView(ctx, reconciler, {
|
|
38
|
+
getHostToolbarHeight: hostToolbar.getHostToolbarHeight,
|
|
39
|
+
});
|
|
40
|
+
const nativeSimulator = createNativeSimulatorView(ctx, reconciler, {
|
|
41
|
+
safeArea,
|
|
42
|
+
devtoolsHost,
|
|
43
|
+
overlayPanels,
|
|
44
|
+
});
|
|
45
|
+
function reapplyToolbarDependentOverlays() {
|
|
46
|
+
overlayPanels.reapplyPresentOverlays();
|
|
1484
47
|
}
|
|
1485
48
|
function disposeAll() {
|
|
1486
|
-
|
|
49
|
+
// Aggregate simulator detach first (native simulator + devtools host +
|
|
50
|
+
// settings/popover), then the workbench, the host toolbar, and the
|
|
51
|
+
// per-guest safe-area sessions.
|
|
52
|
+
nativeSimulator.detachSimulator();
|
|
1487
53
|
// Embedded workbench editor view (no-op when the host never opted in;
|
|
1488
54
|
// also removes the devtools-theme sync listener).
|
|
1489
|
-
detachWorkbench();
|
|
1490
|
-
|
|
1491
|
-
// registry, so a send() racing teardown reports false instead of posting
|
|
1492
|
-
// into a wc that is about to be closed.
|
|
1493
|
-
hostToolbarPort.dispose();
|
|
1494
|
-
// Host-controllable toolbar view: removed from the contentView + its
|
|
1495
|
-
// WebContents closed (the host's loaded content is torn down on app exit).
|
|
1496
|
-
destroyViewInternal(hostToolbarView);
|
|
1497
|
-
hostToolbarView = null;
|
|
1498
|
-
hostToolbarViewAdded = false;
|
|
1499
|
-
// Release this manager's reference on the shared defaultSession
|
|
1500
|
-
// toolbar-runtime registration (only if it ever acquired one — a manager
|
|
1501
|
-
// that never used the toolbar must not drive the shared count to zero).
|
|
1502
|
-
// The LAST release unregisters; other coexisting managers keep theirs.
|
|
1503
|
-
if (hostToolbarRuntimeAcquired) {
|
|
1504
|
-
releaseHostToolbarSessionRuntime();
|
|
1505
|
-
hostToolbarRuntimeAcquired = false;
|
|
1506
|
-
}
|
|
55
|
+
workbench.detachWorkbench();
|
|
56
|
+
hostToolbar.dispose();
|
|
1507
57
|
safeArea.dispose();
|
|
1508
58
|
}
|
|
1509
|
-
function resize(_simWidth) {
|
|
1510
|
-
// Simulator + DevTools overlay bounds are owned solely by the renderer's
|
|
1511
|
-
// anchor publishes — its ResizeObserver/window-resize listeners re-measure
|
|
1512
|
-
// on this same resize. No static re-apply (it raced the precise rect).
|
|
1513
|
-
if (overlayDesired.has(VIEW_ID.settings))
|
|
1514
|
-
applySettingsBounds();
|
|
1515
|
-
}
|
|
1516
59
|
return {
|
|
1517
|
-
attachNativeSimulator,
|
|
1518
|
-
|
|
60
|
+
attachNativeSimulator: nativeSimulator.attachNativeSimulator,
|
|
61
|
+
softReloadNativeSimulator: nativeSimulator.softReloadNativeSimulator,
|
|
62
|
+
detachSimulator: nativeSimulator.detachSimulator,
|
|
1519
63
|
reapplySafeArea: (device) => safeArea.reapplyAll(device),
|
|
1520
|
-
showSettings,
|
|
1521
|
-
hideSettings,
|
|
1522
|
-
showPopover,
|
|
1523
|
-
hidePopover,
|
|
1524
|
-
repositionAll,
|
|
64
|
+
showSettings: overlayPanels.showSettings,
|
|
65
|
+
hideSettings: overlayPanels.hideSettings,
|
|
66
|
+
showPopover: overlayPanels.showPopover,
|
|
67
|
+
hidePopover: overlayPanels.hidePopover,
|
|
68
|
+
repositionAll: () => overlayPanels.reapplyPresentOverlays(),
|
|
1525
69
|
disposeAll,
|
|
1526
|
-
getSimulatorWebContentsId:
|
|
1527
|
-
getSimulatorWebContents:
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (!settingsView)
|
|
1543
|
-
return null;
|
|
1544
|
-
if (settingsView.webContents.isDestroyed())
|
|
1545
|
-
return null;
|
|
1546
|
-
return settingsView.webContents.id;
|
|
1547
|
-
},
|
|
1548
|
-
getPopoverWebContentsId: () => {
|
|
1549
|
-
if (!popoverView)
|
|
1550
|
-
return null;
|
|
1551
|
-
if (popoverView.webContents.isDestroyed())
|
|
1552
|
-
return null;
|
|
1553
|
-
return popoverView.webContents.id;
|
|
1554
|
-
},
|
|
1555
|
-
getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
|
|
1556
|
-
getHostToolbarHeight: () => hostToolbarLastHeight,
|
|
1557
|
-
resize,
|
|
1558
|
-
setPlacementSnapshot,
|
|
1559
|
-
attachWorkbench,
|
|
1560
|
-
setWorkbenchSource,
|
|
1561
|
-
detachWorkbench,
|
|
1562
|
-
openFileInWorkbench,
|
|
1563
|
-
setHostToolbarHeight,
|
|
1564
|
-
hostToolbar,
|
|
70
|
+
getSimulatorWebContentsId: nativeSimulator.getSimulatorWebContentsId,
|
|
71
|
+
getSimulatorWebContents: nativeSimulator.getSimulatorWebContents,
|
|
72
|
+
getSimulatorProjectPath: nativeSimulator.getSimulatorProjectPath,
|
|
73
|
+
getSettingsWebContents: overlayPanels.getSettingsWebContents,
|
|
74
|
+
getSettingsWebContentsId: overlayPanels.getSettingsWebContentsId,
|
|
75
|
+
getPopoverWebContentsId: overlayPanels.getPopoverWebContentsId,
|
|
76
|
+
getHostToolbarWebContentsId: hostToolbar.getHostToolbarWebContentsId,
|
|
77
|
+
getHostToolbarHeight: hostToolbar.getHostToolbarHeight,
|
|
78
|
+
resize: () => overlayPanels.applySettingsBoundsIfPresent(),
|
|
79
|
+
setPlacementSnapshot: reconciler.setPlacementSnapshot,
|
|
80
|
+
attachWorkbench: workbench.attachWorkbench,
|
|
81
|
+
setWorkbenchSource: workbench.setWorkbenchSource,
|
|
82
|
+
detachWorkbench: workbench.detachWorkbench,
|
|
83
|
+
openFileInWorkbench: workbench.openFileInWorkbench,
|
|
84
|
+
setHostToolbarHeight: hostToolbar.setHostToolbarHeight,
|
|
85
|
+
hostToolbar: hostToolbar.control,
|
|
1565
86
|
};
|
|
1566
87
|
}
|
|
1567
88
|
//# sourceMappingURL=view-manager.js.map
|