@dimina-kit/devtools 0.4.0-dev.20260701155140 → 0.4.0-dev.20260702164903
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 +1533 -1237
- 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/main/services/workspace/workspace-service.js +1 -1
- 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-B-Dqb7G0.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
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
|
|
2
|
+
import { cjsSiblingPreloadPath } from '../../utils/paths.js';
|
|
3
|
+
import { simDeskBg } from '../../utils/theme.js';
|
|
4
|
+
import { handleWindowOpenExternal } from '../../windows/navigation-hardening.js';
|
|
5
|
+
import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
|
|
6
|
+
import * as layout from '../layout/index.js';
|
|
7
|
+
import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
|
|
8
|
+
import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
|
|
9
|
+
import { parseRoute } from '../../../shared/simulator-route.js';
|
|
10
|
+
import { SIMULATOR_EVENTS } from '../../../shared/bridge-channels.js';
|
|
11
|
+
import { VIEW_ID } from '../../../shared/view-ids.js';
|
|
12
|
+
export function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
13
|
+
const { safeArea, devtoolsHost, overlayPanels } = deps;
|
|
14
|
+
let nativeSimulatorView = null;
|
|
15
|
+
let nativeSimulatorViewAdded = false;
|
|
16
|
+
let nativeSimulatorProjectPath = null;
|
|
17
|
+
let settleNativeSimulatorReady = null;
|
|
18
|
+
// Whether the CURRENT simulator view's shell finished its first boot (first
|
|
19
|
+
// render guest did-finish-load) — the precondition for a soft reload: only a
|
|
20
|
+
// fully-booted shell has the RELAUNCH listener installed, so sending earlier
|
|
21
|
+
// would silently drop the event and strand the renderer (it was told
|
|
22
|
+
// "accepted" but nothing reloads). Reset on teardown/re-attach — readiness
|
|
23
|
+
// never carries over to a rebuilt view.
|
|
24
|
+
let nativeSimulatorShellReady = false;
|
|
25
|
+
// The `ipcMain.on` listener that services the `__diminaCustomApis` bridge for
|
|
26
|
+
// the current native simulator webContents (see `attachNativeCustomApiBridge`).
|
|
27
|
+
// Tracked so we can remove it before tearing down / re-attaching the view
|
|
28
|
+
// (otherwise listeners leak across relaunch cycles).
|
|
29
|
+
let nativeCustomApiBridgeHandler = null;
|
|
30
|
+
// The current device zoom as a factor (zoomPercent/100), last reported by the
|
|
31
|
+
// renderer via setNativeSimulatorViewBounds. Stored so nested render-host
|
|
32
|
+
// `<webview>` guests attached AFTER a zoom change still pick up the correct
|
|
33
|
+
// scale in `did-attach-webview`. Defaults to 1 (100%).
|
|
34
|
+
let currentZoomFactor = 1;
|
|
35
|
+
let simulatorWebContentsId = null;
|
|
36
|
+
// simulator zoom rides in `extra`: setBounds also drives the WCV zoomFactor and
|
|
37
|
+
// propagates it to nested render-host guests.
|
|
38
|
+
function applyNativeSimulatorBounds(view, bounds, zoom) {
|
|
39
|
+
const p = layout.computeNativeSimulatorViewParams(bounds, zoom);
|
|
40
|
+
currentZoomFactor = p.zoomFactor;
|
|
41
|
+
view.setBounds(p.bounds);
|
|
42
|
+
const simWc = view.webContents;
|
|
43
|
+
if (!simWc.isDestroyed())
|
|
44
|
+
simWc.setZoomFactor(p.zoomFactor);
|
|
45
|
+
try {
|
|
46
|
+
for (const wc of webContents.getAllWebContents()) {
|
|
47
|
+
if (wc.isDestroyed())
|
|
48
|
+
continue;
|
|
49
|
+
if (wc.hostWebContents === simWc)
|
|
50
|
+
wc.setZoomFactor(p.zoomFactor);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* hostWebContents unavailable; guests get zoom on attach */ }
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The single teardown path for the live native-host simulator WCV, shared by
|
|
57
|
+
* relaunch (attachNativeSimulator replacing the view) and project close
|
|
58
|
+
* (detachSimulator). Detaches the view from the window, then synchronously
|
|
59
|
+
* clears its bridge sessions (render guests + service host + mappings) BEFORE
|
|
60
|
+
* the WCV's own async close(), so the next project never re-resolves,
|
|
61
|
+
* re-renders, or screenshots the outgoing guest. The sync prefix clears the
|
|
62
|
+
* maps immediately; the async tail (pool / resource-server release) is observed
|
|
63
|
+
* so a rejection is logged rather than swallowed. The simulatorWc 'destroyed'
|
|
64
|
+
* hook in bridge-router stays as an idempotent fallback. No-op when no view is
|
|
65
|
+
* live; idempotent (disposeSessionsForSimulator early-returns once a session is
|
|
66
|
+
* gone), so the eager teardown here and the 'destroyed' fallback never
|
|
67
|
+
* double-dispose. `label` only tags the diagnostic on the async-tail failure.
|
|
68
|
+
*/
|
|
69
|
+
function tearDownNativeSimulatorView(label) {
|
|
70
|
+
if (!nativeSimulatorView)
|
|
71
|
+
return;
|
|
72
|
+
if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
|
|
73
|
+
try {
|
|
74
|
+
ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
|
|
75
|
+
}
|
|
76
|
+
catch { /* already removed */ }
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
if (!nativeSimulatorView.webContents.isDestroyed()) {
|
|
80
|
+
ctx.bridge?.disposeSessionsForSimulator?.(nativeSimulatorView.webContents.id)
|
|
81
|
+
?.catch((err) => console.warn(`[view-manager] dispose sessions (${label}) failed:`, err));
|
|
82
|
+
nativeSimulatorView.webContents.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch { /* ignore */ }
|
|
86
|
+
nativeSimulatorView = null;
|
|
87
|
+
nativeSimulatorViewAdded = false;
|
|
88
|
+
nativeSimulatorShellReady = false;
|
|
89
|
+
// The instance is being replaced/destroyed — forget its reconciled mount
|
|
90
|
+
// state so the NEXT rebuilt view is treated as a fresh attach. Without this,
|
|
91
|
+
// the level-triggered reconciler still records `simulator` as `attached`
|
|
92
|
+
// (the manual removeChildView above bypasses it), so the next reconcile
|
|
93
|
+
// classifies the freshly-built WebContentsView as already-attached, never
|
|
94
|
+
// emits the `attach` op, and the new view is never addChildView'd — a sticky
|
|
95
|
+
// 100%-invisible simulator after every recompile. Mirrors the hostToolbar
|
|
96
|
+
// instance-replacement handling in ensureHostToolbarView.
|
|
97
|
+
reconciler.forgetActual(VIEW_ID.simulator);
|
|
98
|
+
// The view is gone: gateReadiness now hides it, so reconcile detaches it.
|
|
99
|
+
reconciler.reconcileNow();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Remove the custom-apis bridge `ipcMain.on` listener, if any. Idempotent.
|
|
103
|
+
*/
|
|
104
|
+
function detachNativeCustomApiBridge() {
|
|
105
|
+
if (nativeCustomApiBridgeHandler) {
|
|
106
|
+
ipcMain.removeListener(SimulatorCustomApiBridgeChannel.Request, nativeCustomApiBridgeHandler);
|
|
107
|
+
nativeCustomApiBridgeHandler = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
|
|
112
|
+
* NO embedder renderer, so the simulator-side `__diminaCustomApis` bridge
|
|
113
|
+
* (`src/preload/runtime/custom-apis.ts`) cannot reach the host via
|
|
114
|
+
* `ipcRenderer.sendToHost` — that only delivers to a `<webview>`'s embedder,
|
|
115
|
+
* which is how the default path's `useCustomApiProxy` answered it. A
|
|
116
|
+
* top-level WebContentsView has no embedder, and `sendToHost` does NOT loop
|
|
117
|
+
* back as `ipc-message-host` on itself, so that channel never fires here.
|
|
118
|
+
*
|
|
119
|
+
* Under native-host the bridge instead talks to `ipcMain` directly (the same
|
|
120
|
+
* way `installNativeHostBridge` issues SPAWN/PAGE_OPEN): the preload sends
|
|
121
|
+
* `SimulatorCustomApiBridgeChannel.Request` via `ipcRenderer.send`, this
|
|
122
|
+
* `ipcMain.on` listener answers it. We do NOT route through `IpcRegistry` /
|
|
123
|
+
* the sender-policy white-list (the simulator is deliberately kept off it);
|
|
124
|
+
* instead we accept the message ONLY when `event.sender` is THIS precise
|
|
125
|
+
* simWc — the same trust model bridge-router uses for the simulator's own
|
|
126
|
+
* SPAWN messages. The result is dispatched through the shared
|
|
127
|
+
* `ctx.simulatorApis` registry and the id-correlated `Response` is posted
|
|
128
|
+
* back via `simWc.send`, which the preload's `ipcRenderer.on(Response)`
|
|
129
|
+
* listener settles.
|
|
130
|
+
*/
|
|
131
|
+
function attachNativeCustomApiBridge(simWc) {
|
|
132
|
+
// Re-attach defensively: a stale listener from a prior simWc must never
|
|
133
|
+
// linger. (attachNativeSimulator already tears the old view down first.)
|
|
134
|
+
detachNativeCustomApiBridge();
|
|
135
|
+
const apis = ctx.simulatorApis;
|
|
136
|
+
if (!apis)
|
|
137
|
+
return;
|
|
138
|
+
const simWcId = simWc.id;
|
|
139
|
+
const handler = (event, req) => {
|
|
140
|
+
// Trust gate: only the exact native simulator webContents may drive this.
|
|
141
|
+
if (event.sender.id !== simWcId)
|
|
142
|
+
return;
|
|
143
|
+
const r = req;
|
|
144
|
+
if (!r || typeof r.id !== 'number')
|
|
145
|
+
return;
|
|
146
|
+
void handleCustomApiBridgeRequest(apis, r).then((response) => {
|
|
147
|
+
if (simWc.isDestroyed())
|
|
148
|
+
return;
|
|
149
|
+
simWc.send(SimulatorCustomApiBridgeChannel.Response, response);
|
|
150
|
+
}).catch(() => { });
|
|
151
|
+
};
|
|
152
|
+
nativeCustomApiBridgeHandler = handler;
|
|
153
|
+
ipcMain.on(SimulatorCustomApiBridgeChannel.Request, handler);
|
|
154
|
+
// Consolidate this per-webContents teardown onto the connection layer:
|
|
155
|
+
// acquiring the simWc connection makes the
|
|
156
|
+
// registry track this trusted webContents, and `own()`-ing the detach ties
|
|
157
|
+
// the ipcMain listener's lifetime to the connection so it's torn down
|
|
158
|
+
// deterministically when the wc is destroyed (or the connection is reset),
|
|
159
|
+
// instead of a bespoke `once('destroyed')`. The detach stays idempotent, so
|
|
160
|
+
// the defensive re-attach path calling it directly remains safe.
|
|
161
|
+
ctx.connections.acquire(simWc).own(detachNativeCustomApiBridge);
|
|
162
|
+
}
|
|
163
|
+
function attachNativeSimulator(simulatorUrl, _simWidth) {
|
|
164
|
+
if (!ctx.preloadPath) {
|
|
165
|
+
console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
|
|
166
|
+
return Promise.resolve();
|
|
167
|
+
}
|
|
168
|
+
// Unblock a superseded IPC invocation. Its renderer effect cleanup marks
|
|
169
|
+
// that generation cancelled, so this cannot schedule a stale capture.
|
|
170
|
+
settleNativeSimulatorReady?.();
|
|
171
|
+
settleNativeSimulatorReady = null;
|
|
172
|
+
// Tear down any previous native simulator view (relaunch / re-open) through
|
|
173
|
+
// the shared single teardown path. detachNativeCustomApiBridge stays here
|
|
174
|
+
// (the bridge dispatcher is re-installed per attach below).
|
|
175
|
+
if (nativeSimulatorView) {
|
|
176
|
+
detachNativeCustomApiBridge();
|
|
177
|
+
tearDownNativeSimulatorView('relaunch');
|
|
178
|
+
}
|
|
179
|
+
nativeSimulatorProjectPath = null;
|
|
180
|
+
const ready = new Promise((resolve) => {
|
|
181
|
+
settleNativeSimulatorReady = resolve;
|
|
182
|
+
});
|
|
183
|
+
// Derive THIS project's session partition from the simulator URL's appId so
|
|
184
|
+
// its cookies/localStorage/cache are isolated from every other project.
|
|
185
|
+
// Same project → same partition (storage survives a relaunch);
|
|
186
|
+
// unknown appId → the shared fallback. Configure the partition's session
|
|
187
|
+
// (protocol handlers + CORS/referer policy) before any project content loads
|
|
188
|
+
// on it — idempotent per partition.
|
|
189
|
+
const route = parseRoute(simulatorUrl);
|
|
190
|
+
// Include the project path so two projects that declare the same appId at
|
|
191
|
+
// different paths get isolated partitions. The service-host window for THIS
|
|
192
|
+
// project derives its partition from the same (appId, projectPath) pair so
|
|
193
|
+
// render guests + service host still share one session.
|
|
194
|
+
const partition = miniappPartition(route?.appId, ctx.workspace?.getProjectPath());
|
|
195
|
+
configureMiniappSession(partition);
|
|
196
|
+
// The simulator preload is a CJS bundle; webPreferences.preload obeys the
|
|
197
|
+
// `.js` + "type":"module" ESM rule (require would be undefined), so hand the
|
|
198
|
+
// top-level WebContentsView the `.cjs` sibling. contextIsolation:false +
|
|
199
|
+
// sandbox:false + webviewTag:true mirror what the default `<webview>` guest
|
|
200
|
+
// runs with, and the per-project `persist:miniapp-<key>` partition shares
|
|
201
|
+
// storage + the session-registered preload/CORS rules with the rest of THIS
|
|
202
|
+
// project's simulator (render guests + service host), never other projects'.
|
|
203
|
+
const view = new WebContentsView({
|
|
204
|
+
webPreferences: {
|
|
205
|
+
nodeIntegration: false,
|
|
206
|
+
contextIsolation: false,
|
|
207
|
+
sandbox: false,
|
|
208
|
+
webviewTag: true,
|
|
209
|
+
preload: cjsSiblingPreloadPath(ctx.preloadPath),
|
|
210
|
+
partition,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
nativeSimulatorView = view;
|
|
214
|
+
nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
|
|
215
|
+
// Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
|
|
216
|
+
// light #e8e8e8) so a height-resize that grows the region never flashes a
|
|
217
|
+
// mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
|
|
218
|
+
// and the renderer placeholder behind it are all the same color.
|
|
219
|
+
view.setBackgroundColor(simDeskBg());
|
|
220
|
+
const simWc = view.webContents;
|
|
221
|
+
// Keep the WCV surface in sync with the active color scheme. The
|
|
222
|
+
// process-wide installThemeBackgroundSync() re-syncs BrowserWindows on a
|
|
223
|
+
// theme switch, but this top-level WebContentsView is not a window, so its
|
|
224
|
+
// creation-time backgroundColor would otherwise freeze. Mirror simDeskBg()
|
|
225
|
+
// here on every nativeTheme `updated`; the listener is owned by the wc's
|
|
226
|
+
// connection so it detaches when the simulator view is torn down.
|
|
227
|
+
const syncDeskBg = () => {
|
|
228
|
+
try {
|
|
229
|
+
if (!simWc.isDestroyed())
|
|
230
|
+
view.setBackgroundColor(simDeskBg());
|
|
231
|
+
}
|
|
232
|
+
catch { /* view/wc gone */ }
|
|
233
|
+
};
|
|
234
|
+
nativeTheme.on('updated', syncDeskBg);
|
|
235
|
+
ctx.connections.acquire(simWc).own(() => nativeTheme.removeListener('updated', syncDeskBg));
|
|
236
|
+
// Service the simulator-side `__diminaCustomApis` bridge: this top-level
|
|
237
|
+
// WebContentsView has no embedder renderer to proxy through, so dispatch its
|
|
238
|
+
// `sendToHost` requests straight to `ctx.simulatorApis` from main.
|
|
239
|
+
attachNativeCustomApiBridge(simWc);
|
|
240
|
+
// DeviceShell mounts per-page render-host `<webview>`s INSIDE this view.
|
|
241
|
+
// Pin them onto the SAME per-project partition as their host WCV (so render
|
|
242
|
+
// and the rest of this project share one localStorage/cookie jar) and run
|
|
243
|
+
// them with contextIsolation/sandbox off so the render runtime + its preload
|
|
244
|
+
// share the page realm. (A top-level WebContentsView can host these guests; a
|
|
245
|
+
// `<webview>` guest cannot — that's the whole point of Option A.)
|
|
246
|
+
// Page type (`isTab`) of each attaching guest, captured from its render-host
|
|
247
|
+
// URL in will-attach (where `params.src` carries the full URL) and consumed
|
|
248
|
+
// FIFO in the matching did-attach — `guestWc.getURL()` is still empty there.
|
|
249
|
+
// Per-attach scope: a fresh simWc + handlers are built on every (re)attach.
|
|
250
|
+
const pendingGuestIsTab = [];
|
|
251
|
+
simWc.on('will-attach-webview', (_event, webPreferences, params) => {
|
|
252
|
+
;
|
|
253
|
+
webPreferences.partition = partition;
|
|
254
|
+
params.partition = partition;
|
|
255
|
+
webPreferences.contextIsolation = false;
|
|
256
|
+
webPreferences.sandbox = false;
|
|
257
|
+
let isTab = false;
|
|
258
|
+
try {
|
|
259
|
+
isTab = new URL(params.src).searchParams.get('isTab') === '1';
|
|
260
|
+
}
|
|
261
|
+
catch { /* keep false */ }
|
|
262
|
+
pendingGuestIsTab.push(isTab);
|
|
263
|
+
});
|
|
264
|
+
simWc.on('did-attach-webview', (_event, guestWc) => {
|
|
265
|
+
// Scale the nested render-host page with the device zoom. The host WCV is
|
|
266
|
+
// sized to the SCALED bezel rect and runs at `currentZoomFactor`, so the
|
|
267
|
+
// guest must run at the same factor to lay the page out at the logical
|
|
268
|
+
// device width and paint at the right scale. At 100% (default) this is a
|
|
269
|
+
// no-op identity (factor 1). Newly-attached guests pick up the latest
|
|
270
|
+
// zoom here; live zoom changes re-apply via setNativeSimulatorViewBounds.
|
|
271
|
+
try {
|
|
272
|
+
guestWc.setZoomFactor(currentZoomFactor);
|
|
273
|
+
}
|
|
274
|
+
catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ }
|
|
275
|
+
// Simulate this device's CSS env(safe-area-inset-*) on the fresh guest
|
|
276
|
+
// before it paints, so notch-aware page layout resolves correctly. The
|
|
277
|
+
// bottom inset is page-type-dependent (see services/safe-area): a tab
|
|
278
|
+
// page's content sits above the shell tabBar (bottom 0); a non-tab page
|
|
279
|
+
// is full-bleed (real bottom inset). The page type was captured from the
|
|
280
|
+
// render-host URL in will-attach (FIFO).
|
|
281
|
+
const isTabGuest = pendingGuestIsTab.shift() ?? false;
|
|
282
|
+
safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
|
|
283
|
+
guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
284
|
+
guestWc.on('will-navigate', (e, url) => {
|
|
285
|
+
try {
|
|
286
|
+
const u = new URL(url);
|
|
287
|
+
if (u.protocol === 'about:')
|
|
288
|
+
return;
|
|
289
|
+
if ((u.protocol === 'http:' || u.protocol === 'https:')
|
|
290
|
+
&& (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (u.protocol === 'file:')
|
|
294
|
+
return;
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
297
|
+
void shell.openExternal(url);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
e.preventDefault();
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// The outer DeviceShell loading is insufficient: slow projects attach a
|
|
305
|
+
// render guest later. Resolve the renderer's attach IPC only after that
|
|
306
|
+
// first mini-app page has completed its own document load.
|
|
307
|
+
guestWc.once('did-finish-load', () => {
|
|
308
|
+
if (nativeSimulatorView !== view || simWc.isDestroyed())
|
|
309
|
+
return;
|
|
310
|
+
// A loaded render guest means the shell booted end-to-end — from here
|
|
311
|
+
// on a soft reload (RELAUNCH into this live shell) is deliverable.
|
|
312
|
+
nativeSimulatorShellReady = true;
|
|
313
|
+
settleNativeSimulatorReady?.();
|
|
314
|
+
settleNativeSimulatorReady = null;
|
|
315
|
+
});
|
|
316
|
+
// A render-host guest only attaches after a spawn, so the service window
|
|
317
|
+
// exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
|
|
318
|
+
// with the `onRenderEvent` path in case its emit lost the attach race.
|
|
319
|
+
devtoolsHost.followServiceHost();
|
|
320
|
+
});
|
|
321
|
+
// The simulator loads http://localhost:<port>/simulator.html. Harden popups
|
|
322
|
+
// + in-place navigation: allow the dev server origin (and about:blank /
|
|
323
|
+
// file:// render hosts), route everything else to the OS browser.
|
|
324
|
+
simWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
325
|
+
simWc.on('will-navigate', (e, url) => {
|
|
326
|
+
try {
|
|
327
|
+
const u = new URL(url);
|
|
328
|
+
if (u.protocol === 'about:')
|
|
329
|
+
return;
|
|
330
|
+
if (u.protocol === 'file:')
|
|
331
|
+
return;
|
|
332
|
+
if ((u.protocol === 'http:' || u.protocol === 'https:')
|
|
333
|
+
&& (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
338
|
+
void shell.openExternal(url);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
void simWc.loadURL(simulatorUrl).catch((err) => {
|
|
346
|
+
console.error('[workbench] attachNativeSimulator — loadURL failed', err);
|
|
347
|
+
});
|
|
348
|
+
// This WebContentsView's webContents IS the simulator: the native-host
|
|
349
|
+
// preload runs here and issues SPAWN, so bridge-router resolves
|
|
350
|
+
// ap.simulatorWc = event.sender = this wc, and SIMULATOR_EVENTS flow back.
|
|
351
|
+
simulatorWebContentsId = simWc.id;
|
|
352
|
+
// Forward this WCV's network requests (wx.request/download/upload run here,
|
|
353
|
+
// not in the service host) into the service-host console so they show in the
|
|
354
|
+
// embedded DevTools the right panel hosts. Attaches the CDP debugger; no-op
|
|
355
|
+
// if the forwarder is unwired (partial test ctx) or the debugger is claimed.
|
|
356
|
+
ctx.networkForward?.attachSimulator(simWc);
|
|
357
|
+
// Model A: bounds come ONLY from the renderer's window-level placement
|
|
358
|
+
// snapshot. The view now exists, so re-reconcile: gateReadiness admits the
|
|
359
|
+
// simulator (gated hidden while its WCV was absent) and the latest desired
|
|
360
|
+
// placement attaches + sizes it. Until the next snapshot it stays unadded.
|
|
361
|
+
reconciler.reconcileNow();
|
|
362
|
+
// Native-host page code (console.log / wx.request / Sources) runs in the
|
|
363
|
+
// hidden SERVICE HOST window, not this DeviceShell host nor the render-host
|
|
364
|
+
// guests. Keep the right-panel DevTools host view stable and point it at the
|
|
365
|
+
// service host so its Console/Network(fetch)/Sources reflect the logic layer.
|
|
366
|
+
// (The UI/view layer's Elements equivalent is the native WXML panel +
|
|
367
|
+
// render-guest highlight chain, which targets the active render guest.)
|
|
368
|
+
devtoolsHost.attach();
|
|
369
|
+
return ready;
|
|
370
|
+
}
|
|
371
|
+
function softReloadNativeSimulator(simulatorUrl) {
|
|
372
|
+
if (!nativeSimulatorView || nativeSimulatorView.webContents.isDestroyed())
|
|
373
|
+
return false;
|
|
374
|
+
if (!nativeSimulatorShellReady)
|
|
375
|
+
return false;
|
|
376
|
+
const payload = { url: simulatorUrl };
|
|
377
|
+
nativeSimulatorView.webContents.send(SIMULATOR_EVENTS.RELAUNCH, payload);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
function detachSimulator() {
|
|
381
|
+
settleNativeSimulatorReady?.();
|
|
382
|
+
settleNativeSimulatorReady = null;
|
|
383
|
+
devtoolsHost.stopFollowing();
|
|
384
|
+
detachNativeCustomApiBridge();
|
|
385
|
+
// Stop Elements forwarding (detaches only the debugger sessions IT attached;
|
|
386
|
+
// safe-area's sessions are untouched). Idempotent — the host 'destroyed'
|
|
387
|
+
// handler may have already run.
|
|
388
|
+
devtoolsHost.stopElementsForwarding();
|
|
389
|
+
// Stop forwarding the simulator WCV's network (its debugger is detached as
|
|
390
|
+
// the view is closed below, but drop our session deterministically first).
|
|
391
|
+
// Also drop the DevTools front-end host — its wc is destroyed below; a stale
|
|
392
|
+
// ref would make the forwarder dispatch into a dead wc instead of falling
|
|
393
|
+
// back to the console.
|
|
394
|
+
ctx.networkForward?.detachSimulator();
|
|
395
|
+
ctx.networkForward?.setDevtoolsHost(null);
|
|
396
|
+
// Native-host simulator content view (no-op in the default path) — same
|
|
397
|
+
// single teardown path as relaunch. detachNativeCustomApiBridge was already
|
|
398
|
+
// called above.
|
|
399
|
+
tearDownNativeSimulatorView('close');
|
|
400
|
+
overlayPanels.hidePopover();
|
|
401
|
+
// Drop the settings view too — the previous detachAllViews() did.
|
|
402
|
+
overlayPanels.destroySettings();
|
|
403
|
+
// Destroy the right-panel DevTools front-end host — its wc is closed here.
|
|
404
|
+
devtoolsHost.destroyHostView();
|
|
405
|
+
simulatorWebContentsId = null;
|
|
406
|
+
nativeSimulatorProjectPath = null;
|
|
407
|
+
}
|
|
408
|
+
reconciler.registerView(VIEW_ID.simulator, {
|
|
409
|
+
getView: () => nativeSimulatorView,
|
|
410
|
+
setAdded: (added) => { nativeSimulatorViewAdded = added; },
|
|
411
|
+
gateHidden: () => !nativeSimulatorView,
|
|
412
|
+
applyBounds: (view, bounds, extra) => applyNativeSimulatorBounds(view, bounds, extra?.zoom ?? 1),
|
|
413
|
+
});
|
|
414
|
+
return {
|
|
415
|
+
attachNativeSimulator,
|
|
416
|
+
softReloadNativeSimulator,
|
|
417
|
+
detachSimulator,
|
|
418
|
+
getSimulatorWebContentsId: () => simulatorWebContentsId,
|
|
419
|
+
getSimulatorWebContents: () => {
|
|
420
|
+
if (simulatorWebContentsId == null)
|
|
421
|
+
return null;
|
|
422
|
+
const wc = webContents.fromId(simulatorWebContentsId);
|
|
423
|
+
return wc && !wc.isDestroyed() ? wc : null;
|
|
424
|
+
},
|
|
425
|
+
getSimulatorProjectPath: () => nativeSimulatorProjectPath,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
//# sourceMappingURL=native-simulator-view.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import type { PlacementReconciler } from './placement-reconciler.js';
|
|
3
|
+
import type { ViewManagerContext } from './view-manager.js';
|
|
4
|
+
/**
|
|
5
|
+
* The two main-owned overlay panels: the settings sheet (right-side panel over
|
|
6
|
+
* a transparent backdrop) and the transient popover. Their bounds are
|
|
7
|
+
* main-computed and published into the reconciler's `overlayDesired` (top-tier
|
|
8
|
+
* layers, so a reorder keeps them above every base overlay).
|
|
9
|
+
*/
|
|
10
|
+
export interface OverlayPanelsView {
|
|
11
|
+
showSettings(): Promise<void>;
|
|
12
|
+
hideSettings(): void;
|
|
13
|
+
showPopover(data: unknown): void;
|
|
14
|
+
hidePopover(): void;
|
|
15
|
+
/** Re-apply whichever of settings/popover is currently present (window resize / toolbar height change). */
|
|
16
|
+
reapplyPresentOverlays(): void;
|
|
17
|
+
/** Re-apply the settings overlay only (the resize entry point re-applies settings, not popover). */
|
|
18
|
+
applySettingsBoundsIfPresent(): void;
|
|
19
|
+
/** Destroy the cached settings view (aggregate simulator detach). */
|
|
20
|
+
destroySettings(): void;
|
|
21
|
+
getSettingsWebContents(): WebContents | null;
|
|
22
|
+
getSettingsWebContentsId(): number | null;
|
|
23
|
+
getPopoverWebContentsId(): number | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function createOverlayPanelsView(ctx: ViewManagerContext, reconciler: PlacementReconciler, deps: {
|
|
26
|
+
/** The host-toolbar strip height that offsets the overlay top edge. */
|
|
27
|
+
getHostToolbarHeight(): number;
|
|
28
|
+
}): OverlayPanelsView;
|
|
29
|
+
//# sourceMappingURL=overlay-panels-view.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { WebContentsView } from 'electron';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { mainPreloadPath } from '../../utils/paths.js';
|
|
4
|
+
import { applyNavigationHardening } from '../../windows/navigation-hardening.js';
|
|
5
|
+
import * as layout from '../layout/index.js';
|
|
6
|
+
import { HEADER_H } from '../../../shared/constants.js';
|
|
7
|
+
import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
|
|
8
|
+
import { destroyChildView } from './destroy-child-view.js';
|
|
9
|
+
export function createOverlayPanelsView(ctx, reconciler, deps) {
|
|
10
|
+
let settingsView = null;
|
|
11
|
+
let popoverView = null;
|
|
12
|
+
function overlayHeaderHeight() {
|
|
13
|
+
return HEADER_H + deps.getHostToolbarHeight();
|
|
14
|
+
}
|
|
15
|
+
// settings/popover bounds are main-computed; publish them into overlayDesired
|
|
16
|
+
// and let the reconciler place them (they are top-tier layers, so a reorder
|
|
17
|
+
// keeps them above every base overlay — the old raiseTopOverlays is gone).
|
|
18
|
+
function applySettingsBounds() {
|
|
19
|
+
if (!settingsView || ctx.windows.mainWindow.isDestroyed())
|
|
20
|
+
return;
|
|
21
|
+
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
22
|
+
reconciler.setOverlayDesired(VIEW_ID.settings, {
|
|
23
|
+
viewId: VIEW_ID.settings,
|
|
24
|
+
placement: {
|
|
25
|
+
visible: true,
|
|
26
|
+
bounds: layout.computeSettingsBounds(w, h, overlayHeaderHeight()),
|
|
27
|
+
},
|
|
28
|
+
layer: VIEW_LAYER.settings,
|
|
29
|
+
});
|
|
30
|
+
reconciler.reconcileNow();
|
|
31
|
+
}
|
|
32
|
+
function applyPopoverBounds() {
|
|
33
|
+
if (!popoverView || ctx.windows.mainWindow.isDestroyed())
|
|
34
|
+
return;
|
|
35
|
+
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
36
|
+
reconciler.setOverlayDesired(VIEW_ID.popover, {
|
|
37
|
+
viewId: VIEW_ID.popover,
|
|
38
|
+
placement: {
|
|
39
|
+
visible: true,
|
|
40
|
+
bounds: layout.computePopoverBounds(w, h, overlayHeaderHeight()),
|
|
41
|
+
},
|
|
42
|
+
layer: VIEW_LAYER.popover,
|
|
43
|
+
});
|
|
44
|
+
reconciler.reconcileNow();
|
|
45
|
+
}
|
|
46
|
+
async function showSettings() {
|
|
47
|
+
if (!settingsView) {
|
|
48
|
+
settingsView = new WebContentsView({
|
|
49
|
+
webPreferences: {
|
|
50
|
+
nodeIntegration: false,
|
|
51
|
+
contextIsolation: true,
|
|
52
|
+
sandbox: false,
|
|
53
|
+
preload: mainPreloadPath,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// Overlay loads mainPreloadPath, so the same navigation rules as the
|
|
57
|
+
// main window apply — see navigation-hardening.ts.
|
|
58
|
+
applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
|
|
59
|
+
// Transparent backing: the settings view now spans the whole content area
|
|
60
|
+
// (computeSettingsBounds) and its renderer paints a transparent backdrop +
|
|
61
|
+
// an opaque right-side panel, so the underlying editor/simulator show
|
|
62
|
+
// through and a backdrop click closes the overlay (mirrors the popover).
|
|
63
|
+
settingsView.setBackgroundColor('#00000000');
|
|
64
|
+
await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
|
|
65
|
+
}
|
|
66
|
+
applySettingsBounds();
|
|
67
|
+
}
|
|
68
|
+
function hideSettings() {
|
|
69
|
+
reconciler.deleteOverlayDesired(VIEW_ID.settings);
|
|
70
|
+
reconciler.reconcileNow();
|
|
71
|
+
}
|
|
72
|
+
function showPopover(data) {
|
|
73
|
+
hidePopover();
|
|
74
|
+
const popover = new WebContentsView({
|
|
75
|
+
webPreferences: {
|
|
76
|
+
nodeIntegration: false,
|
|
77
|
+
contextIsolation: true,
|
|
78
|
+
sandbox: false,
|
|
79
|
+
preload: mainPreloadPath,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
// Popover overlay loads mainPreloadPath — same navigation rules apply.
|
|
83
|
+
applyNavigationHardening(popover.webContents, ctx.rendererDir);
|
|
84
|
+
popoverView = popover;
|
|
85
|
+
popover.setBackgroundColor('#00000000');
|
|
86
|
+
applyPopoverBounds();
|
|
87
|
+
popover.webContents.once('did-finish-load', () => {
|
|
88
|
+
ctx.notify.popoverInit(popover, data);
|
|
89
|
+
});
|
|
90
|
+
popover.webContents.loadFile(path.join(ctx.rendererDir, 'entries/popover/index.html'));
|
|
91
|
+
}
|
|
92
|
+
function hidePopover() {
|
|
93
|
+
if (!popoverView)
|
|
94
|
+
return;
|
|
95
|
+
reconciler.deleteOverlayDesired(VIEW_ID.popover);
|
|
96
|
+
// Destroy the WCV first (removeChildView + close), THEN reconcile: the detach
|
|
97
|
+
// op then finds the view already gone and does not double-removeChildView.
|
|
98
|
+
destroyChildView(ctx.windows.mainWindow, popoverView);
|
|
99
|
+
popoverView = null;
|
|
100
|
+
reconciler.reconcileNow();
|
|
101
|
+
ctx.notify.popoverClosed();
|
|
102
|
+
}
|
|
103
|
+
function reapplyPresentOverlays() {
|
|
104
|
+
if (reconciler.hasOverlayDesired(VIEW_ID.settings))
|
|
105
|
+
applySettingsBounds();
|
|
106
|
+
if (reconciler.hasOverlayDesired(VIEW_ID.popover))
|
|
107
|
+
applyPopoverBounds();
|
|
108
|
+
}
|
|
109
|
+
function applySettingsBoundsIfPresent() {
|
|
110
|
+
if (reconciler.hasOverlayDesired(VIEW_ID.settings))
|
|
111
|
+
applySettingsBounds();
|
|
112
|
+
}
|
|
113
|
+
function destroySettings() {
|
|
114
|
+
destroyChildView(ctx.windows.mainWindow, settingsView);
|
|
115
|
+
settingsView = null;
|
|
116
|
+
}
|
|
117
|
+
reconciler.registerView(VIEW_ID.settings, { getView: () => settingsView });
|
|
118
|
+
reconciler.registerView(VIEW_ID.popover, { getView: () => popoverView });
|
|
119
|
+
return {
|
|
120
|
+
showSettings,
|
|
121
|
+
hideSettings,
|
|
122
|
+
showPopover,
|
|
123
|
+
hidePopover,
|
|
124
|
+
reapplyPresentOverlays,
|
|
125
|
+
applySettingsBoundsIfPresent,
|
|
126
|
+
destroySettings,
|
|
127
|
+
getSettingsWebContents: () => {
|
|
128
|
+
if (!settingsView)
|
|
129
|
+
return null;
|
|
130
|
+
if (settingsView.webContents.isDestroyed())
|
|
131
|
+
return null;
|
|
132
|
+
return settingsView.webContents;
|
|
133
|
+
},
|
|
134
|
+
getSettingsWebContentsId: () => {
|
|
135
|
+
if (!settingsView)
|
|
136
|
+
return null;
|
|
137
|
+
if (settingsView.webContents.isDestroyed())
|
|
138
|
+
return null;
|
|
139
|
+
return settingsView.webContents.id;
|
|
140
|
+
},
|
|
141
|
+
getPopoverWebContentsId: () => {
|
|
142
|
+
if (!popoverView)
|
|
143
|
+
return null;
|
|
144
|
+
if (popoverView.webContents.isDestroyed())
|
|
145
|
+
return null;
|
|
146
|
+
return popoverView.webContents.id;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=overlay-panels-view.js.map
|