@dimina-kit/devtools 0.3.2-dev.20260611063505 → 0.3.2-dev.20260611135124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -40
- package/dist/main/api.d.ts +4 -2
- package/dist/main/api.js +5 -1
- package/dist/main/app/app.d.ts +1 -5
- package/dist/main/app/app.js +4 -19
- package/dist/main/index.bundle.js +238 -392
- package/dist/main/ipc/app.d.ts +1 -1
- package/dist/main/ipc/app.js +0 -7
- package/dist/main/ipc/index.d.ts +0 -2
- package/dist/main/ipc/index.js +0 -2
- package/dist/main/ipc/settings.d.ts +1 -1
- package/dist/main/ipc/settings.js +1 -13
- package/dist/main/ipc/simulator-module.d.ts +3 -3
- package/dist/main/ipc/simulator-module.js +3 -7
- package/dist/main/ipc/simulator.js +1 -12
- package/dist/main/runtime/miniapp-runtime.d.ts +138 -28
- package/dist/main/runtime/miniapp-runtime.js +6 -11
- package/dist/main/services/layout/index.d.ts +0 -11
- package/dist/main/services/layout/index.js +0 -23
- package/dist/main/services/notifications/renderer-notifier.d.ts +0 -6
- package/dist/main/services/notifications/renderer-notifier.js +1 -10
- package/dist/main/services/service-host-pool/pool.d.ts +5 -4
- package/dist/main/services/views/host-toolbar-port-channel.d.ts +93 -0
- package/dist/main/services/views/host-toolbar-port-channel.js +169 -0
- package/dist/main/services/views/host-toolbar-session-runtime.d.ts +35 -0
- package/dist/main/services/views/host-toolbar-session-runtime.js +55 -0
- package/dist/main/services/views/view-manager.d.ts +68 -39
- package/dist/main/services/views/view-manager.js +143 -182
- package/dist/main/services/workbench-context.d.ts +6 -26
- package/dist/main/services/workbench-context.js +0 -14
- package/dist/main/services/workspace/workspace-service.js +7 -13
- package/dist/main/utils/paths.d.ts +11 -2
- package/dist/main/utils/paths.js +11 -2
- package/dist/preload/index.d.ts +15 -0
- package/dist/preload/index.js +15 -0
- package/dist/preload/runtime/host-toolbar-port.d.ts +44 -0
- package/dist/preload/runtime/host-toolbar-port.js +127 -0
- package/dist/preload/runtime/host-toolbar-runtime.d.ts +41 -0
- package/dist/preload/runtime/host-toolbar-runtime.js +49 -0
- package/dist/preload/windows/{host-toolbar.cjs → host-toolbar-runtime.cjs} +94 -3
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +7 -0
- package/dist/preload/windows/host-toolbar-runtime.d.ts +2 -0
- package/dist/preload/windows/host-toolbar-runtime.js +13 -0
- package/dist/preload/windows/simulator.cjs +0 -2
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +0 -2
- package/dist/renderer/assets/constants-DI382mTP.js +2 -0
- package/dist/renderer/assets/index-DLzjz1wv.js +50 -0
- package/dist/renderer/assets/{input-CFHmbOgz.js → input-6fpCXV-n.js} +2 -2
- package/dist/renderer/assets/ipc-transport-Bs8Sf1B2.css +1 -0
- package/dist/renderer/assets/{ipc-transport-BkKKww3u.js → ipc-transport-D22e4dv7.js} +6 -6
- package/dist/renderer/assets/popover-CdGdKkkc.js +2 -0
- package/dist/renderer/assets/select-X0sbjwdP.js +2 -0
- package/dist/renderer/assets/{settings-Cl4IcWnG.js → settings-CS4jYaCw.js} +2 -2
- package/dist/renderer/assets/settings-api-CCWTAao_.js +2 -0
- package/dist/renderer/assets/workbenchSettings-Bpzaj3bm.js +8 -0
- package/dist/renderer/entries/main/index.html +7 -6
- package/dist/renderer/entries/popover/index.html +6 -6
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +5 -5
- package/dist/shared/constants.d.ts +18 -0
- package/dist/shared/constants.js +18 -0
- package/dist/shared/ipc-channels.d.ts +10 -23
- package/dist/shared/ipc-channels.js +13 -34
- package/dist/shared/ipc-schemas.d.ts +0 -12
- package/dist/shared/ipc-schemas.js +0 -12
- package/dist/shared/types.d.ts +14 -67
- package/package.json +6 -6
- package/dist/main/ipc/panels.d.ts +0 -4
- package/dist/main/ipc/panels.js +0 -46
- package/dist/main/ipc/toolbar.d.ts +0 -4
- package/dist/main/ipc/toolbar.js +0 -21
- package/dist/main/services/toolbar/toolbar-store.d.ts +0 -23
- package/dist/main/services/toolbar/toolbar-store.js +0 -40
- package/dist/preload/windows/host-toolbar.cjs.map +0 -7
- package/dist/preload/windows/host-toolbar.d.ts +0 -2
- package/dist/preload/windows/host-toolbar.js +0 -12
- package/dist/renderer/assets/constants-CLAQX6VZ.js +0 -2
- package/dist/renderer/assets/index-CZQOpCvg.js +0 -50
- package/dist/renderer/assets/ipc-transport-Bv_BVu4T.css +0 -1
- package/dist/renderer/assets/popover-h1M7n3tt.js +0 -2
- package/dist/renderer/assets/select-kj0n1oYf.js +0 -2
- package/dist/renderer/assets/settings-api-CCwo2eOi.js +0 -2
- package/dist/renderer/assets/workbenchSettings-BzlpQnve.js +0 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ipcMain, shell, WebContentsView, webContents } from 'electron';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { cjsSiblingPreloadPath,
|
|
3
|
+
import { cjsSiblingPreloadPath, mainPreloadPath } from '../../utils/paths.js';
|
|
4
4
|
import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
|
|
5
5
|
import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
|
|
6
6
|
import { OPEN_IN_EDITOR_SCHEME, decodeOpenInEditorUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
|
|
@@ -9,9 +9,11 @@ import { buildCustomizeTabsScript } from './devtools-tabs.js';
|
|
|
9
9
|
import { installElementsForward } from '../elements-forward/index.js';
|
|
10
10
|
import * as layout from '../layout/index.js';
|
|
11
11
|
import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
|
|
12
|
-
import { getDefaultTab } from '../workbench-context.js';
|
|
13
12
|
import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
|
|
13
|
+
import { acquireHostToolbarSessionRuntime, releaseHostToolbarSessionRuntime, } from './host-toolbar-session-runtime.js';
|
|
14
|
+
import { createHostToolbarPortChannel, } from './host-toolbar-port-channel.js';
|
|
14
15
|
import { parseRoute } from '../../../shared/simulator-route.js';
|
|
16
|
+
import { HEADER_H, HOST_TOOLBAR_RUNTIME_MARKER } from '../../../shared/constants.js';
|
|
15
17
|
/**
|
|
16
18
|
* Build a ViewManager bound to the given context. The returned object is the
|
|
17
19
|
* only component allowed to instantiate or add/remove overlay WebContentsViews.
|
|
@@ -20,9 +22,6 @@ import { parseRoute } from '../../../shared/simulator-route.js';
|
|
|
20
22
|
* on the context object.
|
|
21
23
|
*/
|
|
22
24
|
export function createViewManager(ctx) {
|
|
23
|
-
// Resolve once: full WorkbenchContext always provides headerHeight; partial
|
|
24
|
-
// test contexts may omit it, in which case fall back to the default 40.
|
|
25
|
-
const headerHeight = ctx.headerHeight ?? 40;
|
|
26
25
|
// CSS env(safe-area-inset-*) simulation for render-host guests (per device).
|
|
27
26
|
// Driven from did-attach-webview below; re-pushed on device change via
|
|
28
27
|
// reapplySafeArea. Torn down in disposeAll.
|
|
@@ -56,7 +55,6 @@ export function createViewManager(ctx) {
|
|
|
56
55
|
let settingsView = null;
|
|
57
56
|
let settingsViewAdded = false;
|
|
58
57
|
let popoverView = null;
|
|
59
|
-
let lastSimWidth = 375;
|
|
60
58
|
let simulatorWebContentsId = null;
|
|
61
59
|
// NATIVE-HOST ONLY. The webContents the right-panel Chrome DevTools front-end
|
|
62
60
|
// currently inspects. We point it at the SERVICE HOST (logic layer) — the
|
|
@@ -75,10 +73,10 @@ export function createViewManager(ctx) {
|
|
|
75
73
|
let stopElementsForward = null;
|
|
76
74
|
let nativeDevtoolsRetryTimer = null;
|
|
77
75
|
let nativeDevtoolsRetryToken = 0;
|
|
78
|
-
// Renderer-driven overlay bounds for the simulator DevTools view
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
76
|
+
// Renderer-driven overlay bounds for the simulator DevTools view — the SOLE
|
|
77
|
+
// mount/geometry authority (no static-layout fallback). A zero-area
|
|
78
|
+
// rectangle means "hide" — the overlay is removed from the contentView but
|
|
79
|
+
// its WebContents stays alive.
|
|
82
80
|
let simulatorBoundsOverride = null;
|
|
83
81
|
// ── Host-controllable toolbar WebContentsView ───────────────────────────
|
|
84
82
|
// A strip above the devtools header that the downstream host loads its own
|
|
@@ -88,6 +86,21 @@ export function createViewManager(ctx) {
|
|
|
88
86
|
let hostToolbarView = null;
|
|
89
87
|
let hostToolbarPreloadOverride = null;
|
|
90
88
|
let hostToolbarViewAdded = false;
|
|
89
|
+
// Whether THIS manager holds a reference on the shared defaultSession
|
|
90
|
+
// registration of the toolbar-runtime preload (see
|
|
91
|
+
// host-toolbar-session-runtime.ts). Acquired on first toolbar need,
|
|
92
|
+
// released exactly once in disposeAll — a manager that never used the
|
|
93
|
+
// toolbar must not decrement a ref it never took.
|
|
94
|
+
let hostToolbarRuntimeAcquired = false;
|
|
95
|
+
// Placeholder height authority: 'auto' = advertiser reports forward to the
|
|
96
|
+
// renderer; { fixed } = host-pinned, advertiser reports are dropped.
|
|
97
|
+
let hostToolbarHeightMode = 'auto';
|
|
98
|
+
// Gated narrow channel to the toolbar PAGE (per-load MessagePort handshake;
|
|
99
|
+
// see host-toolbar-port-channel.ts). Control-level registry — created with
|
|
100
|
+
// the manager so onMessage() works before any toolbar view exists.
|
|
101
|
+
const hostToolbarPort = createHostToolbarPortChannel({
|
|
102
|
+
isCurrent: (wc) => liveHostToolbarWebContents() === wc,
|
|
103
|
+
});
|
|
91
104
|
// ── Internal helpers ────────────────────────────────────────────────────
|
|
92
105
|
function destroyViewInternal(view) {
|
|
93
106
|
if (!view)
|
|
@@ -105,16 +118,6 @@ export function createViewManager(ctx) {
|
|
|
105
118
|
}
|
|
106
119
|
catch { /* ignore */ }
|
|
107
120
|
}
|
|
108
|
-
function applySimulatorBounds(simWidth) {
|
|
109
|
-
if (!simulatorView || ctx.windows.mainWindow.isDestroyed())
|
|
110
|
-
return;
|
|
111
|
-
if (simulatorBoundsOverride) {
|
|
112
|
-
simulatorView.setBounds(simulatorBoundsOverride);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
116
|
-
simulatorView.setBounds(layout.computeSimulatorBounds(w, h, simWidth, headerHeight));
|
|
117
|
-
}
|
|
118
121
|
// Renderer publishes width/height = 0 to mean "hide overlay" — the
|
|
119
122
|
// surrounding React panel is unmounted/collapsed. We keep the cached
|
|
120
123
|
// value (so future republishes win over legacy layout) but remove the
|
|
@@ -145,11 +148,23 @@ export function createViewManager(ctx) {
|
|
|
145
148
|
simulatorView.setBounds(bounds);
|
|
146
149
|
}
|
|
147
150
|
// ── Host-controllable toolbar WebContentsView ───────────────────────────
|
|
151
|
+
// The toolbar's webContents lifecycle belongs to the HOST, which may close
|
|
152
|
+
// it out from under us (the documented rebuild path). In real Electron a
|
|
153
|
+
// WebContentsView whose webContents was destroyed can report `webContents`
|
|
154
|
+
// as undefined — not merely a destroyed handle — so every access must
|
|
155
|
+
// tolerate BOTH (observed in the R1 e2e: `.isDestroyed()` on undefined threw
|
|
156
|
+
// inside the control surface after the host closed the wc).
|
|
157
|
+
function liveHostToolbarWebContents() {
|
|
158
|
+
const wc = hostToolbarView?.webContents;
|
|
159
|
+
if (!wc || wc.isDestroyed())
|
|
160
|
+
return null;
|
|
161
|
+
return wc;
|
|
162
|
+
}
|
|
148
163
|
// Lazily create the host-toolbar view. Mirrors `showSettings` for the
|
|
149
164
|
// webPreferences shape and the native simulator for nav hardening +
|
|
150
165
|
// background color (the host may load arbitrary URLs / content). Idempotent.
|
|
151
166
|
function ensureHostToolbarView() {
|
|
152
|
-
if (hostToolbarView &&
|
|
167
|
+
if (hostToolbarView && liveHostToolbarWebContents()) {
|
|
153
168
|
return hostToolbarView;
|
|
154
169
|
}
|
|
155
170
|
// Rebuilding after the host destroyed the underlying webContents: detach the
|
|
@@ -163,21 +178,36 @@ export function createViewManager(ctx) {
|
|
|
163
178
|
catch { /* already removed */ }
|
|
164
179
|
hostToolbarViewAdded = false;
|
|
165
180
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
// The framework's height-advertiser runtime is SESSION-resident: register
|
|
182
|
+
// it on session.defaultSession (ref-counted across coexisting managers)
|
|
183
|
+
// BEFORE the view exists, so the very first load already runs it. The
|
|
184
|
+
// toolbar WCV stays on the defaultSession (no partition/session override)
|
|
185
|
+
// — moving it onto its own partition would silently detach it from this
|
|
186
|
+
// registration and height advertising would die with no error.
|
|
187
|
+
if (!hostToolbarRuntimeAcquired) {
|
|
188
|
+
acquireHostToolbarSessionRuntime();
|
|
189
|
+
hostToolbarRuntimeAcquired = true;
|
|
190
|
+
}
|
|
191
|
+
// `webPreferences.preload` is the HOST's alone (setPreloadPath); the
|
|
192
|
+
// built-in advertiser no longer rides it (it would execute twice — the
|
|
193
|
+
// session copy + the webPreferences copy). The additionalArguments marker
|
|
194
|
+
// is what the session runtime's guard keys on to activate here and stay a
|
|
195
|
+
// zero-footprint no-op in every other defaultSession renderer.
|
|
196
|
+
const webPreferences = {
|
|
197
|
+
nodeIntegration: false,
|
|
198
|
+
contextIsolation: true,
|
|
199
|
+
sandbox: false,
|
|
200
|
+
additionalArguments: [HOST_TOOLBAR_RUNTIME_MARKER],
|
|
201
|
+
};
|
|
202
|
+
if (hostToolbarPreloadOverride !== null) {
|
|
203
|
+
webPreferences.preload = hostToolbarPreloadOverride;
|
|
204
|
+
}
|
|
205
|
+
const view = new WebContentsView({ webPreferences });
|
|
180
206
|
hostToolbarView = view;
|
|
207
|
+
// Hook the per-load MessagePort handshake (did-finish-load) + dead-port
|
|
208
|
+
// cleanup (destroyed) on the fresh wc. AFTER the assignment above so the
|
|
209
|
+
// channel's isCurrent guard sees this wc as the live one.
|
|
210
|
+
hostToolbarPort.attach(view.webContents);
|
|
181
211
|
// Paint the surface a neutral color so growing the reserved strip never
|
|
182
212
|
// flashes white before the host content paints (mirrors the native
|
|
183
213
|
// simulator's setBackgroundColor anti-flash).
|
|
@@ -200,7 +230,7 @@ export function createViewManager(ctx) {
|
|
|
200
230
|
// WebContents (and the host's loaded content) alive. Do NOT create the
|
|
201
231
|
// view just to immediately hide it.
|
|
202
232
|
if (isHidden(bounds)) {
|
|
203
|
-
if (hostToolbarView && hostToolbarViewAdded &&
|
|
233
|
+
if (hostToolbarView && hostToolbarViewAdded && liveHostToolbarWebContents()) {
|
|
204
234
|
try {
|
|
205
235
|
ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
|
|
206
236
|
}
|
|
@@ -219,6 +249,12 @@ export function createViewManager(ctx) {
|
|
|
219
249
|
view.setBounds(bounds);
|
|
220
250
|
}
|
|
221
251
|
function setHostToolbarHeight(extent) {
|
|
252
|
+
// While the host pins a fixed height, drop advertiser reports entirely —
|
|
253
|
+
// the session-resident advertiser is always installed, so forwarding its
|
|
254
|
+
// reports would make the strip oscillate between the pinned and measured
|
|
255
|
+
// heights on every content resize.
|
|
256
|
+
if (hostToolbarHeightMode !== 'auto')
|
|
257
|
+
return;
|
|
222
258
|
// Push the reserved height back to the main-window renderer so its
|
|
223
259
|
// placeholder div resizes (closing the dynamic-height loop). The height is
|
|
224
260
|
// not retained in main — the renderer placeholder is the single source of
|
|
@@ -242,37 +278,62 @@ export function createViewManager(ctx) {
|
|
|
242
278
|
const hostToolbar = {
|
|
243
279
|
async loadURL(url) {
|
|
244
280
|
const view = ensureHostToolbarView();
|
|
281
|
+
// Invalidate SYNCHRONOUSLY at initiation, before the load is issued:
|
|
282
|
+
// the current document is about to be replaced, so a same-tick send()
|
|
283
|
+
// must report false instead of confirming delivery into it. The channel
|
|
284
|
+
// recovers on the new document's did-finish-load handshake. (Cannot
|
|
285
|
+
// rely on did-start-navigation here — that only covers page-initiated
|
|
286
|
+
// navigations once the load is actually under way.)
|
|
287
|
+
hostToolbarPort.invalidate();
|
|
245
288
|
await view.webContents.loadURL(url);
|
|
246
289
|
},
|
|
247
290
|
async loadFile(filePath) {
|
|
248
291
|
const view = ensureHostToolbarView();
|
|
292
|
+
// Same initiation-invalidates contract as loadURL above.
|
|
293
|
+
hostToolbarPort.invalidate();
|
|
249
294
|
await view.webContents.loadFile(filePath);
|
|
250
295
|
},
|
|
251
296
|
get webContents() {
|
|
252
|
-
|
|
253
|
-
return null;
|
|
254
|
-
if (hostToolbarView.webContents.isDestroyed())
|
|
255
|
-
return null;
|
|
256
|
-
return hostToolbarView.webContents;
|
|
297
|
+
return liveHostToolbarWebContents();
|
|
257
298
|
},
|
|
258
299
|
hide() {
|
|
259
300
|
hideHostToolbar();
|
|
260
301
|
},
|
|
261
302
|
setPreloadPath(path) {
|
|
303
|
+
// The HOST's own webPreferences.preload, applied when the view is next
|
|
304
|
+
// (re)created. `null` = no host preload. The framework advertiser is
|
|
305
|
+
// session-resident and unaffected either way (see ensureHostToolbarView).
|
|
262
306
|
hostToolbarPreloadOverride = path;
|
|
263
307
|
},
|
|
308
|
+
setHeightMode(mode) {
|
|
309
|
+
hostToolbarHeightMode = mode;
|
|
310
|
+
if (mode !== 'auto') {
|
|
311
|
+
// Pin immediately: a preload-less/static toolbar never advertises, so
|
|
312
|
+
// waiting for the next report would leave the strip at height 0.
|
|
313
|
+
ctx.notify.hostToolbarHeightChanged(mode.fixed);
|
|
314
|
+
}
|
|
315
|
+
// Switching back to 'auto' deliberately does NOT synthesize a notify —
|
|
316
|
+
// replaying a stale cached height would flash the old size; the NEXT
|
|
317
|
+
// advertiser report drives the placeholder again.
|
|
318
|
+
},
|
|
319
|
+
onMessage(channel, handler) {
|
|
320
|
+
return hostToolbarPort.onMessage(channel, handler);
|
|
321
|
+
},
|
|
322
|
+
send(channel, payload) {
|
|
323
|
+
return hostToolbarPort.send(channel, payload);
|
|
324
|
+
},
|
|
264
325
|
};
|
|
265
326
|
function applySettingsBounds() {
|
|
266
327
|
if (!settingsView || ctx.windows.mainWindow.isDestroyed())
|
|
267
328
|
return;
|
|
268
329
|
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
269
|
-
settingsView.setBounds(layout.computeSettingsBounds(w, h,
|
|
330
|
+
settingsView.setBounds(layout.computeSettingsBounds(w, h, HEADER_H));
|
|
270
331
|
}
|
|
271
332
|
function applyPopoverBounds() {
|
|
272
333
|
if (!popoverView || ctx.windows.mainWindow.isDestroyed())
|
|
273
334
|
return;
|
|
274
335
|
const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
|
|
275
|
-
popoverView.setBounds(layout.computePopoverBounds(w, h,
|
|
336
|
+
popoverView.setBounds(layout.computePopoverBounds(w, h, HEADER_H));
|
|
276
337
|
}
|
|
277
338
|
function clearNativeDevtoolsRetry() {
|
|
278
339
|
nativeDevtoolsRetryToken++;
|
|
@@ -511,84 +572,11 @@ export function createViewManager(ctx) {
|
|
|
511
572
|
followNativeDevtoolsServiceHost(event.appId);
|
|
512
573
|
}
|
|
513
574
|
// ── ViewManager methods ─────────────────────────────────────────────────
|
|
514
|
-
function
|
|
515
|
-
const sim = webContents.fromId(simWcId);
|
|
516
|
-
if (!sim) {
|
|
517
|
-
console.error('[workbench] attachSimulator — simWc not found for id', simWcId);
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
lastSimWidth = simWidth;
|
|
521
|
-
simulatorWebContentsId = simWcId;
|
|
522
|
-
// Destroy old simulatorView to prevent WebContentsView leak
|
|
523
|
-
if (simulatorView) {
|
|
524
|
-
hideSimulator();
|
|
525
|
-
try {
|
|
526
|
-
if (!simulatorView.webContents.isDestroyed()) {
|
|
527
|
-
simulatorView.webContents.close();
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
catch { /* ignore */ }
|
|
531
|
-
simulatorView = null;
|
|
532
|
-
}
|
|
533
|
-
simulatorView = new WebContentsView();
|
|
534
|
-
sim.setDevToolsWebContents(simulatorView.webContents);
|
|
535
|
-
// Embedded in the right-panel host view; never bring it to the foreground
|
|
536
|
-
// (would steal focus from the user / disrupt e2e).
|
|
537
|
-
sim.openDevTools({ mode: 'detach', activate: false });
|
|
538
|
-
// Default DevTools to Console panel (Chrome DevTools defaults to Elements).
|
|
539
|
-
// The DevTools UI lives inside closed shadow roots, so a light-DOM
|
|
540
|
-
// querySelector('[role="tab"]') cannot reach the tab bar to click it.
|
|
541
|
-
// Instead drive the front-end's own view manager: the bundled DevTools
|
|
542
|
-
// exposes `UI.ViewManager.instance().showView(id)` on `globalThis.UI`
|
|
543
|
-
// once the front-end has finished bootstrapping. We poll for it and
|
|
544
|
-
// request the `console` view, and also persist the choice via the
|
|
545
|
-
// `panel-selectedTab` localStorage key so subsequent reloads honor it.
|
|
546
|
-
const devtoolsWc = simulatorView.webContents;
|
|
547
|
-
// Keep only Elements/Console/Network tabs (front-most, in that order).
|
|
548
|
-
customizeDevtoolsTabs(devtoolsWc);
|
|
549
|
-
devtoolsWc.once('dom-ready', () => {
|
|
550
|
-
devtoolsWc.executeJavaScript(`
|
|
551
|
-
(function() {
|
|
552
|
-
try { localStorage.setItem('panel-selectedTab', '"console"') } catch {}
|
|
553
|
-
let tries = 0
|
|
554
|
-
const timer = setInterval(() => {
|
|
555
|
-
tries++
|
|
556
|
-
try {
|
|
557
|
-
const UI = globalThis.UI
|
|
558
|
-
const vm = UI && UI.ViewManager && typeof UI.ViewManager.instance === 'function'
|
|
559
|
-
? UI.ViewManager.instance()
|
|
560
|
-
: null
|
|
561
|
-
if (vm && typeof vm.showView === 'function') {
|
|
562
|
-
vm.showView('console')
|
|
563
|
-
clearInterval(timer)
|
|
564
|
-
return
|
|
565
|
-
}
|
|
566
|
-
} catch {}
|
|
567
|
-
if (tries > 80) clearInterval(timer)
|
|
568
|
-
}, 50)
|
|
569
|
-
})()
|
|
570
|
-
`).catch(() => { });
|
|
571
|
-
});
|
|
572
|
-
if (getDefaultTab(ctx) === 'simulator') {
|
|
573
|
-
if (simulatorBoundsOverride) {
|
|
574
|
-
if (!isHidden(simulatorBoundsOverride)) {
|
|
575
|
-
ctx.windows.mainWindow.contentView.addChildView(simulatorView);
|
|
576
|
-
simulatorViewAdded = true;
|
|
577
|
-
simulatorView.setBounds(simulatorBoundsOverride);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
ctx.windows.mainWindow.contentView.addChildView(simulatorView);
|
|
582
|
-
simulatorViewAdded = true;
|
|
583
|
-
applySimulatorBounds(simWidth);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
function attachNativeSimulatorDevtoolsHost(simWidth) {
|
|
575
|
+
function attachNativeSimulatorDevtoolsHost() {
|
|
588
576
|
stopFollowingNativeServiceHost();
|
|
589
577
|
// Destroy old simulatorView to prevent WebContentsView leak
|
|
590
578
|
if (simulatorView) {
|
|
591
|
-
|
|
579
|
+
removeSimulatorDevtoolsView();
|
|
592
580
|
try {
|
|
593
581
|
if (!simulatorView.webContents.isDestroyed()) {
|
|
594
582
|
simulatorView.webContents.close();
|
|
@@ -657,19 +645,16 @@ export function createViewManager(ctx) {
|
|
|
657
645
|
})()
|
|
658
646
|
`).catch(() => { });
|
|
659
647
|
});
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
simulatorViewAdded = true;
|
|
671
|
-
applySimulatorBounds(simWidth);
|
|
672
|
-
}
|
|
648
|
+
// Anchor-only mount: the renderer's published rect is the SOLE authority.
|
|
649
|
+
// If a non-zero rect was already published (it can land before this attach
|
|
650
|
+
// on the project-open ordering), replay it; otherwise the view stays
|
|
651
|
+
// unadded and unsized until the first publish arrives. No static-layout
|
|
652
|
+
// fallback — an attach-time computed rect raced the precise anchor rect
|
|
653
|
+
// and flashed the overlay at the wrong rectangle.
|
|
654
|
+
if (simulatorBoundsOverride && !isHidden(simulatorBoundsOverride)) {
|
|
655
|
+
ctx.windows.mainWindow.contentView.addChildView(simulatorView);
|
|
656
|
+
simulatorViewAdded = true;
|
|
657
|
+
simulatorView.setBounds(simulatorBoundsOverride);
|
|
673
658
|
}
|
|
674
659
|
if (ctx.bridge?.isNativeHost()) {
|
|
675
660
|
unsubscribeNativeRenderEvents = ctx.bridge.onRenderEvent(onNativeRenderEvent);
|
|
@@ -738,12 +723,11 @@ export function createViewManager(ctx) {
|
|
|
738
723
|
// the defensive re-attach path calling it directly remains safe.
|
|
739
724
|
ctx.connections.acquire(simWc).own(detachNativeCustomApiBridge);
|
|
740
725
|
}
|
|
741
|
-
function attachNativeSimulator(simulatorUrl,
|
|
726
|
+
function attachNativeSimulator(simulatorUrl, _simWidth) {
|
|
742
727
|
if (!ctx.preloadPath) {
|
|
743
728
|
console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
|
|
744
729
|
return;
|
|
745
730
|
}
|
|
746
|
-
lastSimWidth = simWidth;
|
|
747
731
|
// Tear down any previous native simulator view (relaunch / re-open).
|
|
748
732
|
if (nativeSimulatorView) {
|
|
749
733
|
detachNativeCustomApiBridge();
|
|
@@ -901,7 +885,7 @@ export function createViewManager(ctx) {
|
|
|
901
885
|
// service host so its Console/Network(fetch)/Sources reflect the logic layer.
|
|
902
886
|
// (The UI/view layer's Elements equivalent is the native WXML panel +
|
|
903
887
|
// render-guest highlight chain, which targets the active render guest.)
|
|
904
|
-
attachNativeSimulatorDevtoolsHost(
|
|
888
|
+
attachNativeSimulatorDevtoolsHost();
|
|
905
889
|
}
|
|
906
890
|
function detachSimulator() {
|
|
907
891
|
stopFollowingNativeServiceHost();
|
|
@@ -955,23 +939,16 @@ export function createViewManager(ctx) {
|
|
|
955
939
|
// session must not be replayed onto a fresh re-attach (stale slice/offset).
|
|
956
940
|
lastRendererRect = null;
|
|
957
941
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (!simulatorViewAdded) {
|
|
963
|
-
ctx.windows.mainWindow.contentView.addChildView(simulatorView);
|
|
964
|
-
simulatorViewAdded = true;
|
|
965
|
-
}
|
|
966
|
-
applySimulatorBounds(simWidth);
|
|
967
|
-
}
|
|
968
|
-
function hideSimulator() {
|
|
942
|
+
// Remove (but do not destroy) the DevTools overlay from the contentView.
|
|
943
|
+
// Internal teardown helper for re-attach; user-facing visibility is the
|
|
944
|
+
// anchor 0×0 single path (`setSimulatorDevtoolsBounds`).
|
|
945
|
+
function removeSimulatorDevtoolsView() {
|
|
969
946
|
if (simulatorView && simulatorViewAdded) {
|
|
970
947
|
try {
|
|
971
948
|
ctx.windows.mainWindow.contentView.removeChildView(simulatorView);
|
|
972
949
|
}
|
|
973
950
|
catch (e) {
|
|
974
|
-
console.error('[workbench]
|
|
951
|
+
console.error('[workbench] removeSimulatorDevtoolsView error', e);
|
|
975
952
|
}
|
|
976
953
|
simulatorViewAdded = false;
|
|
977
954
|
}
|
|
@@ -1035,10 +1012,9 @@ export function createViewManager(ctx) {
|
|
|
1035
1012
|
}
|
|
1036
1013
|
}
|
|
1037
1014
|
function repositionAll() {
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
applySimulatorBounds(lastSimWidth);
|
|
1015
|
+
// Simulator + DevTools overlay: bounds owned by the renderer's anchor
|
|
1016
|
+
// publishes; their ResizeObserver/window-resize listeners re-measure, so
|
|
1017
|
+
// no static re-apply here.
|
|
1042
1018
|
if (settingsView && settingsViewAdded)
|
|
1043
1019
|
applySettingsBounds();
|
|
1044
1020
|
if (popoverView)
|
|
@@ -1046,11 +1022,23 @@ export function createViewManager(ctx) {
|
|
|
1046
1022
|
}
|
|
1047
1023
|
function disposeAll() {
|
|
1048
1024
|
detachSimulator();
|
|
1025
|
+
// Narrow channel first: close the live MessagePort + sweep the onMessage
|
|
1026
|
+
// registry, so a send() racing teardown reports false instead of posting
|
|
1027
|
+
// into a wc that is about to be closed.
|
|
1028
|
+
hostToolbarPort.dispose();
|
|
1049
1029
|
// Host-controllable toolbar view: removed from the contentView + its
|
|
1050
1030
|
// WebContents closed (the host's loaded content is torn down on app exit).
|
|
1051
1031
|
destroyViewInternal(hostToolbarView);
|
|
1052
1032
|
hostToolbarView = null;
|
|
1053
1033
|
hostToolbarViewAdded = false;
|
|
1034
|
+
// Release this manager's reference on the shared defaultSession
|
|
1035
|
+
// toolbar-runtime registration (only if it ever acquired one — a manager
|
|
1036
|
+
// that never used the toolbar must not drive the shared count to zero).
|
|
1037
|
+
// The LAST release unregisters; other coexisting managers keep theirs.
|
|
1038
|
+
if (hostToolbarRuntimeAcquired) {
|
|
1039
|
+
releaseHostToolbarSessionRuntime();
|
|
1040
|
+
hostToolbarRuntimeAcquired = false;
|
|
1041
|
+
}
|
|
1054
1042
|
safeArea.dispose();
|
|
1055
1043
|
}
|
|
1056
1044
|
function setNativeSimulatorViewBounds(params) {
|
|
@@ -1102,34 +1090,17 @@ export function createViewManager(ctx) {
|
|
|
1102
1090
|
}
|
|
1103
1091
|
catch { /* hostWebContents unavailable; guests get zoom on attach */ }
|
|
1104
1092
|
}
|
|
1105
|
-
function resize(
|
|
1106
|
-
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
// same resize. No coarse panel-size re-apply here (it raced the precise rect).
|
|
1110
|
-
if (simulatorViewAdded)
|
|
1111
|
-
applySimulatorBounds(simWidth);
|
|
1093
|
+
function resize(_simWidth) {
|
|
1094
|
+
// Simulator + DevTools overlay bounds are owned solely by the renderer's
|
|
1095
|
+
// anchor publishes — its ResizeObserver/window-resize listeners re-measure
|
|
1096
|
+
// on this same resize. No static re-apply (it raced the precise rect).
|
|
1112
1097
|
if (settingsViewAdded)
|
|
1113
1098
|
applySettingsBounds();
|
|
1114
1099
|
}
|
|
1115
|
-
function setVisible(visible, simWidth) {
|
|
1116
|
-
lastSimWidth = simWidth;
|
|
1117
|
-
if (!simulatorView)
|
|
1118
|
-
return;
|
|
1119
|
-
if (visible && !simulatorViewAdded) {
|
|
1120
|
-
showSimulator(simWidth);
|
|
1121
|
-
}
|
|
1122
|
-
else if (!visible) {
|
|
1123
|
-
hideSimulator();
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
1100
|
return {
|
|
1127
|
-
attachSimulator,
|
|
1128
1101
|
attachNativeSimulator,
|
|
1129
1102
|
detachSimulator,
|
|
1130
1103
|
reapplySafeArea: (device) => safeArea.reapplyAll(device),
|
|
1131
|
-
showSimulator,
|
|
1132
|
-
hideSimulator,
|
|
1133
1104
|
showSettings,
|
|
1134
1105
|
hideSettings,
|
|
1135
1106
|
showPopover,
|
|
@@ -1143,9 +1114,6 @@ export function createViewManager(ctx) {
|
|
|
1143
1114
|
const wc = webContents.fromId(simulatorWebContentsId);
|
|
1144
1115
|
return wc && !wc.isDestroyed() ? wc : null;
|
|
1145
1116
|
},
|
|
1146
|
-
getLastSimWidth: () => lastSimWidth,
|
|
1147
|
-
isSimulatorAdded: () => simulatorViewAdded,
|
|
1148
|
-
hasSimulatorView: () => simulatorView !== null,
|
|
1149
1117
|
getSettingsWebContents: () => {
|
|
1150
1118
|
if (!settingsView)
|
|
1151
1119
|
return null;
|
|
@@ -1167,16 +1135,9 @@ export function createViewManager(ctx) {
|
|
|
1167
1135
|
return null;
|
|
1168
1136
|
return popoverView.webContents.id;
|
|
1169
1137
|
},
|
|
1170
|
-
getHostToolbarWebContentsId: () =>
|
|
1171
|
-
if (!hostToolbarView)
|
|
1172
|
-
return null;
|
|
1173
|
-
if (hostToolbarView.webContents.isDestroyed())
|
|
1174
|
-
return null;
|
|
1175
|
-
return hostToolbarView.webContents.id;
|
|
1176
|
-
},
|
|
1138
|
+
getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
|
|
1177
1139
|
setNativeSimulatorViewBounds,
|
|
1178
1140
|
resize,
|
|
1179
|
-
setVisible,
|
|
1180
1141
|
setSimulatorDevtoolsBounds,
|
|
1181
1142
|
setHostToolbarBounds,
|
|
1182
1143
|
setHostToolbarHeight,
|
|
@@ -12,10 +12,9 @@ import { type ViewManager } from './views/view-manager.js';
|
|
|
12
12
|
import { type WindowService } from './window-service.js';
|
|
13
13
|
import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
14
14
|
import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
|
|
15
|
-
import { type ToolbarStore } from './toolbar/toolbar-store.js';
|
|
16
15
|
import { sanitizeTemplates } from './projects/templates.js';
|
|
17
16
|
import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
|
|
18
|
-
import type {
|
|
17
|
+
import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
|
|
19
18
|
/**
|
|
20
19
|
* Shared mutable state for the workbench application.
|
|
21
20
|
* Passed to each IPC module so they can read/write shared state without closures.
|
|
@@ -26,14 +25,10 @@ export interface WorkbenchContext {
|
|
|
26
25
|
preloadPath: string;
|
|
27
26
|
/** Absolute path to the renderer dist directory */
|
|
28
27
|
rendererDir: string;
|
|
29
|
-
/** Built-in panel IDs to display (default: all) */
|
|
30
|
-
panels: string[];
|
|
31
28
|
/** Custom API namespace names (e.g. ['qd']) passed to the simulator */
|
|
32
29
|
apiNamespaces: string[];
|
|
33
30
|
/** Branding name shown in title bar and getBranding IPC */
|
|
34
31
|
appName: string;
|
|
35
|
-
/** Header bar height in px, used for view layout and exposed to the renderer */
|
|
36
|
-
headerHeight: number;
|
|
37
32
|
/** Host-injected provider for branding info (overrides default appName) */
|
|
38
33
|
brandingProvider?: () => Promise<{
|
|
39
34
|
appName: string;
|
|
@@ -72,11 +67,6 @@ export interface WorkbenchContext {
|
|
|
72
67
|
parentWindow: BrowserWindow;
|
|
73
68
|
templates: ProjectTemplate[];
|
|
74
69
|
}) => Promise<CustomCreateProjectDialogResult>;
|
|
75
|
-
/**
|
|
76
|
-
* Optional host gate for opening projects. Runs after basic directory
|
|
77
|
-
* validation and before the current project session is closed.
|
|
78
|
-
*/
|
|
79
|
-
beforeOpenProject?: (projectPath: string) => BeforeOpenProjectResult | Promise<BeforeOpenProjectResult>;
|
|
80
70
|
/**
|
|
81
71
|
* Trust predicate consulted by `IpcRegistry` for every incoming IPC.
|
|
82
72
|
* Resolves the currently-trusted senders (main renderer + overlays)
|
|
@@ -99,12 +89,6 @@ export interface WorkbenchContext {
|
|
|
99
89
|
* One registry per context — no process-global crosstalk.
|
|
100
90
|
*/
|
|
101
91
|
simulatorApis: SimulatorApiRegistry;
|
|
102
|
-
/**
|
|
103
|
-
* Per-context store of host-registered toolbar actions. Populated via
|
|
104
|
-
* `instance.toolbar.set`; read by the toolbar IPC handlers. One store per
|
|
105
|
-
* context — no process-global crosstalk.
|
|
106
|
-
*/
|
|
107
|
-
toolbar: ToolbarStore;
|
|
108
92
|
/** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
|
|
109
93
|
registry: DisposableRegistry;
|
|
110
94
|
/**
|
|
@@ -162,29 +146,27 @@ export interface WorkbenchContext {
|
|
|
162
146
|
* run) and injects its raw Network.* CDP events into the DevTools FRONT-END wc
|
|
163
147
|
* (`window.DevToolsAPI.dispatchMessage`) so the native Network tab renders them;
|
|
164
148
|
* falls back to a `[网络]` service-host console line when the front-end is
|
|
165
|
-
* unavailable. The ViewManager calls `attachSimulator` +
|
|
166
|
-
* `attachNativeSimulator` once the simulator WCV +
|
|
167
|
-
* Undefined until bootstrap wires it.
|
|
149
|
+
* unavailable. The ViewManager calls the forwarder's own `attachSimulator` +
|
|
150
|
+
* `setDevtoolsHost` from `attachNativeSimulator` once the simulator WCV +
|
|
151
|
+
* DevTools host exist. Undefined until bootstrap wires it.
|
|
168
152
|
*/
|
|
169
153
|
networkForward?: NetworkForwarder;
|
|
170
154
|
}
|
|
171
155
|
/**
|
|
172
156
|
* Inputs for `createWorkbenchContext`. The scalar config fields it shares with
|
|
173
|
-
* `WorkbenchConfig` (`adapter` / `apiNamespaces` / `appName`
|
|
157
|
+
* `WorkbenchConfig` (`adapter` / `apiNamespaces` / `appName`)
|
|
174
158
|
* are derived from there via `Pick` so the two stay in lockstep — no
|
|
175
159
|
* field-by-field re-declaration. The remaining fields are kept explicit
|
|
176
160
|
* because their shapes intentionally differ from the config:
|
|
177
161
|
* - `preloadPath` / `rendererDir` are REQUIRED here (the caller resolves the
|
|
178
162
|
* defaults before constructing the context) but optional in the config;
|
|
179
|
-
* - `panels` is the structural `string[]` (the context stores raw ids);
|
|
180
163
|
* - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` use
|
|
181
164
|
* the main-process types, not the structural mirrors in `shared/types`.
|
|
182
165
|
*/
|
|
183
|
-
export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName'
|
|
166
|
+
export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName'> {
|
|
184
167
|
mainWindow: BrowserWindow;
|
|
185
168
|
preloadPath: string;
|
|
186
169
|
rendererDir: string;
|
|
187
|
-
panels?: string[];
|
|
188
170
|
brandingProvider?: WorkbenchContext['brandingProvider'];
|
|
189
171
|
/** Host-supplied project list backend. Defaults to LocalProjectsProvider. */
|
|
190
172
|
projectsProvider?: ProjectsProvider;
|
|
@@ -195,8 +177,6 @@ export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' |
|
|
|
195
177
|
/** Host-supplied "新建项目" dialog hook. */
|
|
196
178
|
customCreateProjectDialog?: WorkbenchContext['customCreateProjectDialog'];
|
|
197
179
|
}
|
|
198
|
-
export declare function hasBuiltinPanel(ctx: Pick<WorkbenchContext, 'panels'>, panelId: string): boolean;
|
|
199
|
-
export declare function getDefaultTab(ctx: Pick<WorkbenchContext, 'panels'>): string;
|
|
200
180
|
export declare function createWorkbenchContext(opts: CreateContextOptions): WorkbenchContext;
|
|
201
181
|
export { sanitizeTemplates };
|
|
202
182
|
//# sourceMappingURL=workbench-context.d.ts.map
|
|
@@ -7,28 +7,15 @@ import { createWindowService } from './window-service.js';
|
|
|
7
7
|
import { createWorkspaceService, } from './workspace/workspace-service.js';
|
|
8
8
|
import { createLocalProjectsProvider } from './projects/local-provider.js';
|
|
9
9
|
import { createSimulatorApiRegistry, } from './simulator/custom-apis.js';
|
|
10
|
-
import { createToolbarStore } from './toolbar/toolbar-store.js';
|
|
11
10
|
import { resolveTemplates, sanitizeTemplates } from './projects/templates.js';
|
|
12
11
|
import { BUILTIN_TEMPLATES } from './projects/builtin-templates.js';
|
|
13
|
-
export function hasBuiltinPanel(ctx, panelId) {
|
|
14
|
-
return ctx.panels.includes(panelId);
|
|
15
|
-
}
|
|
16
|
-
export function getDefaultTab(ctx) {
|
|
17
|
-
if (hasBuiltinPanel(ctx, 'console'))
|
|
18
|
-
return 'simulator';
|
|
19
|
-
if (ctx.panels.length > 0)
|
|
20
|
-
return ctx.panels[0];
|
|
21
|
-
return 'simulator';
|
|
22
|
-
}
|
|
23
12
|
export function createWorkbenchContext(opts) {
|
|
24
13
|
const ctx = {
|
|
25
14
|
adapter: opts.adapter ?? defaultAdapter,
|
|
26
15
|
preloadPath: opts.preloadPath,
|
|
27
16
|
rendererDir: opts.rendererDir,
|
|
28
|
-
panels: opts.panels ?? ['wxml', 'console', 'appdata', 'storage'],
|
|
29
17
|
apiNamespaces: opts.apiNamespaces ?? [],
|
|
30
18
|
appName: opts.appName ?? 'Dimina DevTools',
|
|
31
|
-
headerHeight: opts.headerHeight ?? 40,
|
|
32
19
|
brandingProvider: opts.brandingProvider,
|
|
33
20
|
};
|
|
34
21
|
ctx.registry = new DisposableRegistry();
|
|
@@ -39,7 +26,6 @@ export function createWorkbenchContext(opts) {
|
|
|
39
26
|
ctx.connections = createConnectionRegistry();
|
|
40
27
|
ctx.trustedWindowSenderIds = new Map();
|
|
41
28
|
ctx.simulatorApis = createSimulatorApiRegistry();
|
|
42
|
-
ctx.toolbar = createToolbarStore();
|
|
43
29
|
ctx.windows = createWindowService(opts.mainWindow);
|
|
44
30
|
ctx.views = createViewManager(ctx);
|
|
45
31
|
ctx.notify = createRendererNotifier(ctx);
|