@dimina-kit/devtools 0.4.0-dev.20260718095333 → 0.4.0-dev.20260728063215
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/app.d.ts +10 -1
- package/dist/main/app/app.js +78 -2
- package/dist/main/app/lifecycle.d.ts +12 -1
- package/dist/main/app/lifecycle.js +27 -1
- package/dist/main/index.bundle.js +1263 -206
- package/dist/main/index.js +49 -1
- package/dist/main/ipc/bridge-router.d.ts +36 -0
- package/dist/main/ipc/bridge-router.js +56 -0
- package/dist/main/ipc/index.d.ts +1 -0
- package/dist/main/ipc/index.js +1 -0
- package/dist/main/ipc/internal-devtools.d.ts +15 -0
- package/dist/main/ipc/internal-devtools.js +9 -0
- package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +61 -0
- package/dist/main/runtime/devtools-backend-before-quit.harness.js +356 -0
- package/dist/main/runtime/devtools-backend.js +49 -4
- package/dist/main/services/compile-standby.js +4 -0
- package/dist/main/services/console-forward/cdp-inject.d.ts +18 -0
- package/dist/main/services/console-forward/cdp-inject.js +30 -0
- package/dist/main/services/console-forward/global-console-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-console-mirror.js +71 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.d.ts +33 -0
- package/dist/main/services/console-forward/global-diagnostics-mirror.js +73 -0
- package/dist/main/services/console-forward/index.d.ts +15 -1
- package/dist/main/services/console-forward/index.js +42 -2
- package/dist/main/services/console-forward/internal-log.d.ts +11 -0
- package/dist/main/services/console-forward/internal-log.js +23 -0
- package/dist/main/services/console-forward/open-gated-relay.d.ts +67 -0
- package/dist/main/services/console-forward/open-gated-relay.js +55 -0
- package/dist/main/services/diagnostics/index.d.ts +10 -0
- package/dist/main/services/elements-forward/index.js +14 -96
- package/dist/main/services/mcp/server.js +24 -2
- package/dist/main/services/network-forward/frontend-dispatch.d.ts +64 -0
- package/dist/main/services/network-forward/frontend-dispatch.js +113 -8
- package/dist/main/services/network-forward/global-body-gate.d.ts +46 -0
- package/dist/main/services/network-forward/global-body-gate.js +93 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.d.ts +48 -0
- package/dist/main/services/network-forward/global-mirror-test-fixtures.js +96 -0
- package/dist/main/services/network-forward/index.d.ts +31 -0
- package/dist/main/services/network-forward/index.js +256 -8
- package/dist/main/services/network-forward/user-facing.d.ts +21 -0
- package/dist/main/services/network-forward/user-facing.js +50 -0
- package/dist/main/services/projects/create-project-service.js +10 -3
- package/dist/main/services/update/github-release-checker.js +22 -3
- package/dist/main/services/update/update-manager.d.ts +4 -1
- package/dist/main/services/update/update-manager.js +30 -5
- package/dist/main/services/views/console-filter.d.ts +145 -0
- package/dist/main/services/views/console-filter.js +233 -0
- package/dist/main/services/views/frontend-bootstrap-gate.d.ts +59 -0
- package/dist/main/services/views/frontend-bootstrap-gate.js +117 -0
- package/dist/main/services/views/native-simulator-devtools-host.js +138 -30
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.d.ts +76 -0
- package/dist/main/services/views/view-manager-devtools-host-test-fixtures.js +182 -0
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/utils/theme.d.ts +10 -6
- package/dist/main/utils/theme.js +10 -6
- package/dist/main/windows/internal-devtools-window/index.d.ts +66 -0
- package/dist/main/windows/internal-devtools-window/index.js +160 -0
- package/dist/native-host/render/render.js +89 -73
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/renderer/assets/index-M5NDq_vi.js +49 -0
- package/dist/renderer/assets/{input-c2OcrrZy.js → input-dsgeTmVX.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BgFdT9bT.js +9 -0
- package/dist/renderer/assets/{popover-kv2qspWz.js → popover-Disfu1cx.js} +2 -2
- package/dist/renderer/assets/{select-DKqzMtTh.js → select-DAw1hzkl.js} +2 -2
- package/dist/renderer/assets/{settings-dJNKTGr8.js → settings-M_8GFW2M.js} +2 -2
- package/dist/renderer/assets/settings-api-DFR5eDl_.js +2 -0
- package/dist/renderer/assets/{workbenchSettings-DQ_VP5-W.js → workbenchSettings-CxdXeoOu.js} +2 -2
- package/dist/renderer/entries/main/index.html +5 -5
- package/dist/renderer/entries/popover/index.html +4 -4
- package/dist/renderer/entries/settings/index.html +4 -4
- package/dist/renderer/entries/workbench-settings/index.html +3 -3
- package/dist/service-host/sync-impls/menu-button-geometry.js +15 -5
- package/dist/service-host/sync-impls/menu-button.js +5 -3
- package/dist/shared/ipc-channels.d.ts +3 -0
- package/dist/shared/ipc-channels.js +6 -0
- package/dist/simulator/assets/device-shell-Cs-rmP1f.js +2 -0
- package/dist/simulator/assets/device-shell-DOy53Y0s.css +1 -0
- package/dist/simulator/assets/{simulator-Cy8c-eA2.js → simulator-BG-5CADK.js} +3 -3
- package/dist/simulator/device-shell/menu-button-geometry.js +15 -5
- package/dist/simulator/simulator.html +1 -1
- package/package.json +7 -7
- package/dist/renderer/assets/index-U9F8MrtV.js +0 -49
- package/dist/renderer/assets/ipc-transport-CuDJ07aE.js +0 -9
- package/dist/renderer/assets/settings-api-qpXKDRXt.js +0 -2
- package/dist/simulator/assets/device-shell-C-HZY3bL.css +0 -1
- package/dist/simulator/assets/device-shell-Cu1ZPLfe.js +0 -2
package/dist/main/index.js
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
import { app } from 'electron';
|
|
2
2
|
import { launch } from './app/launch.js';
|
|
3
|
+
import { createGitHubReleaseChecker } from './services/update/index.js';
|
|
4
|
+
// Only a packaged RELEASE-channel build self-updates against this repo's
|
|
5
|
+
// GitHub Releases:
|
|
6
|
+
// - `pnpm dev` runs stay quiet (no API calls, no update dialog) — same
|
|
7
|
+
// app.isPackaged gate used elsewhere for dev-only behavior.
|
|
8
|
+
// - A packaged DEV-channel build (release.yml's `channel: dev` — distributed
|
|
9
|
+
// only as a 30-day Actions artifact for QA/branch testing, never a GitHub
|
|
10
|
+
// Release) must NOT get this either: release.yml's dev channel never
|
|
11
|
+
// creates a GitHub Release, so /releases/latest always reflects the
|
|
12
|
+
// RELEASE channel — a dev build would be prompted to "update" to an
|
|
13
|
+
// unrelated stable build, and clicking it would silently swap out the
|
|
14
|
+
// very branch/PR build the person downloaded it to test. dev-channel
|
|
15
|
+
// versions always carry bump-dev-version.js's `-dev.<timestamp>` suffix;
|
|
16
|
+
// release-channel versions never do — use that as the channel signal.
|
|
17
|
+
// - Hosts that embed devtools via `launch(config)` supply their own
|
|
18
|
+
// `updateChecker` (or none) and are unaffected — this default only
|
|
19
|
+
// applies to this entry point.
|
|
20
|
+
//
|
|
21
|
+
// Custom parseVersion, NOT the default asset-name fallback and NOT
|
|
22
|
+
// 'trailing-number':
|
|
23
|
+
// - release.yml's GitHub Release tag (`release-YYYYMMDD-N`) is an unrelated
|
|
24
|
+
// release-sequence label, not this app's version — 'trailing-number' would
|
|
25
|
+
// compare its bare counter against app.getVersion() (e.g. "0.4.0")
|
|
26
|
+
// numerically, which looks "newer" once the counter passes the app's major
|
|
27
|
+
// version, reporting an update on every single check forever.
|
|
28
|
+
// - The built-in asset-name fallback (defaultParseVersion's SEMVER_RE) finds
|
|
29
|
+
// the version inside `dimina-devtools-0.4.0-mac-arm64.dmg` (see release.yml's
|
|
30
|
+
// "Rename macOS dmg" / Archive steps) but its optional prerelease-suffix
|
|
31
|
+
// capture greedily swallows the trailing `-mac-arm64.dmg` as a "prerelease"
|
|
32
|
+
// tag, so the update dialog would show a garbled "0.5.0-mac-arm64.dmg".
|
|
33
|
+
// Extract just the clean `x.y.z` from our own known asset naming instead.
|
|
34
|
+
// Short-circuited: app.getVersion() must only be read once isPackaged is
|
|
35
|
+
// already known true, both because it's meaningless before then and because
|
|
36
|
+
// some minimal test/dev electron mocks don't stub getVersion() at all.
|
|
37
|
+
const updateChecker = app.isPackaged && !app.getVersion().includes('-dev.')
|
|
38
|
+
? createGitHubReleaseChecker({
|
|
39
|
+
owner: 'EchoTechFE',
|
|
40
|
+
repo: 'dimina-kit',
|
|
41
|
+
parseVersion: (release) => {
|
|
42
|
+
for (const asset of release.assets) {
|
|
43
|
+
const match = /^dimina-devtools-(\d+\.\d+\.\d+)-/.exec(asset.name);
|
|
44
|
+
if (match)
|
|
45
|
+
return match[1];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
: undefined;
|
|
3
51
|
// Fire-and-forget boot: launch() returns the electron-deck whenReady gate
|
|
4
52
|
// promise. A bare call would let a boot failure escape as an unhandledRejection
|
|
5
53
|
// (no diagnostics, can be swallowed). Surface it via a structured failure exit:
|
|
6
54
|
// log the cause and exit non-zero (per workbench-model.md fire-and-forget rule).
|
|
7
|
-
launch().catch((err) => {
|
|
55
|
+
launch({ updateChecker }).catch((err) => {
|
|
8
56
|
console.error('[devtools] fatal: launch() failed during boot', err);
|
|
9
57
|
app.exit(1);
|
|
10
58
|
});
|
|
@@ -33,6 +33,33 @@ export interface RenderEvent {
|
|
|
33
33
|
* Lets the current-page panel push the route without a separate lookup. */
|
|
34
34
|
pagePath?: string;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Fires once per app session, the moment its hidden SERVICE HOST window's
|
|
38
|
+
* `service.html` document has `did-finish-load`'d — i.e. `serviceWcId` is
|
|
39
|
+
* GUARANTEED resolvable via `webContents.fromId(serviceWcId)` right now. This
|
|
40
|
+
* is document readiness, NOT mini-app runtime readiness: `injectLogicBundle`
|
|
41
|
+
* and the service's own `serviceResourceLoaded` handshake both happen AFTER
|
|
42
|
+
* this fires (see `bootServiceHost`) — a consumer that needs "the mini-app
|
|
43
|
+
* has actually booted" must not treat this as that signal.
|
|
44
|
+
*
|
|
45
|
+
* Exists so a consumer that needs to act on the service host wc THE MOMENT it
|
|
46
|
+
* becomes resolvable (e.g. attaching the right-panel DevTools front-end) has
|
|
47
|
+
* a real event to react to instead of polling `getServiceWc(appId)` on a
|
|
48
|
+
* fixed retry budget and silently giving up if that budget is exhausted
|
|
49
|
+
* before the wc resolves under load — the bug this event was added to fix
|
|
50
|
+
* (see `native-simulator-devtools-host.ts`'s `onServiceHostReady` subscriber).
|
|
51
|
+
*
|
|
52
|
+
* `appId`-keyed resolution (`getServiceWc(appId)`) is NOT equivalent to this
|
|
53
|
+
* event during same-app respawn overlap — `getServiceWc` returns the MOST
|
|
54
|
+
* RECENT live session for that appId, which may not be the specific session
|
|
55
|
+
* that just fired this event. Consumers that must target the EXACT session
|
|
56
|
+
* should resolve `serviceWcId` directly rather than re-resolving by `appId`.
|
|
57
|
+
*/
|
|
58
|
+
export interface ServiceHostReadyEvent {
|
|
59
|
+
appId: string;
|
|
60
|
+
appSessionId: string;
|
|
61
|
+
serviceWcId: number;
|
|
62
|
+
}
|
|
36
63
|
/**
|
|
37
64
|
* Point-in-time counts of every resource class RouterState owns. Leak coverage
|
|
38
65
|
* (unit + e2e) asserts EXACT equality of this ledger around a churn cycle —
|
|
@@ -75,8 +102,17 @@ export interface BridgeRouterHandle {
|
|
|
75
102
|
* before the first PAGE_STACK signal. Optional: only the native-host bridge
|
|
76
103
|
* provides it (the default path derives the stack from the simulator URL). */
|
|
77
104
|
getPageStack?(appId?: string): PageStackEntry[] | null;
|
|
105
|
+
/** The current (or named) app session's resource-server baseUrl (serves
|
|
106
|
+
* that session's framework js/css), or null when no matching session
|
|
107
|
+
* exists. Per-session, never a global singleton — see AppSession.resourceBaseUrl. */
|
|
108
|
+
getResourceBaseUrl?(appId?: string): string | null;
|
|
78
109
|
/** Subscribe to render-side activity (domReady / active-page change). */
|
|
79
110
|
onRenderEvent(listener: (event: RenderEvent) => void): () => void;
|
|
111
|
+
/** Subscribe to a service host's `did-finish-load` readiness (see
|
|
112
|
+
* `ServiceHostReadyEvent`'s doc comment). A subscriber registering after
|
|
113
|
+
* the still-live session's event already fired gets a missed-signal
|
|
114
|
+
* catch-up — it never needs to poll. */
|
|
115
|
+
onServiceHostReady(listener: (event: ServiceHostReadyEvent) => void): () => void;
|
|
80
116
|
/** The currently-selected device (renderer toolbar), or null pre-selection. */
|
|
81
117
|
getDevice(): NativeDeviceInfo | null;
|
|
82
118
|
/** Cache the selected device + push DEVICE_CHANGE to the live simulator WC(s). */
|
|
@@ -166,6 +166,8 @@ export function installBridgeRouter(ctx) {
|
|
|
166
166
|
pendingApiCalls: new Map(),
|
|
167
167
|
pool: null,
|
|
168
168
|
emitRenderEvent: () => { },
|
|
169
|
+
emitServiceHostReady: () => { },
|
|
170
|
+
lastServiceHostReady: null,
|
|
169
171
|
connections: ctx.connections,
|
|
170
172
|
debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
|
|
171
173
|
appLifecycle: createAppLifecycleController(),
|
|
@@ -237,6 +239,22 @@ export function installBridgeRouter(ctx) {
|
|
|
237
239
|
}
|
|
238
240
|
};
|
|
239
241
|
state.emitRenderEvent = emitRenderEvent;
|
|
242
|
+
// Subscribers to `ServiceHostReadyEvent` (see its doc comment). Separate
|
|
243
|
+
// from `renderEventListeners`: this is session-scoped (appSessionId +
|
|
244
|
+
// serviceWcId), not page/bridgeId-scoped like `RenderEvent`.
|
|
245
|
+
const serviceHostReadyListeners = new Set();
|
|
246
|
+
const emitServiceHostReady = (event) => {
|
|
247
|
+
state.lastServiceHostReady = event;
|
|
248
|
+
for (const listener of serviceHostReadyListeners) {
|
|
249
|
+
try {
|
|
250
|
+
listener(event);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
console.warn('[bridge-router] service-host-ready listener threw:', error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
state.emitServiceHostReady = emitServiceHostReady;
|
|
240
258
|
// Expose a thin accessor over RouterState so other main services (storage,
|
|
241
259
|
// automation, appdata) can resolve live render/service WebContents without
|
|
242
260
|
// owning router state. Getters resolve fresh — the pre-warm pool can swap
|
|
@@ -269,6 +287,10 @@ export function installBridgeRouter(ctx) {
|
|
|
269
287
|
const ap = resolveCurrentApp(state, ctx, appId);
|
|
270
288
|
return ap?.pageStack ?? null;
|
|
271
289
|
},
|
|
290
|
+
getResourceBaseUrl: (appId) => {
|
|
291
|
+
const ap = resolveCurrentApp(state, ctx, appId);
|
|
292
|
+
return ap?.resourceBaseUrl ?? null;
|
|
293
|
+
},
|
|
272
294
|
getActiveRenderWc: (appId) => {
|
|
273
295
|
const ap = resolveCurrentApp(state, ctx, appId);
|
|
274
296
|
if (!ap)
|
|
@@ -280,6 +302,35 @@ export function installBridgeRouter(ctx) {
|
|
|
280
302
|
renderEventListeners.add(listener);
|
|
281
303
|
return () => renderEventListeners.delete(listener);
|
|
282
304
|
},
|
|
305
|
+
onServiceHostReady: (listener) => {
|
|
306
|
+
serviceHostReadyListeners.add(listener);
|
|
307
|
+
// Missed-signal catch-up (mirrors host-toolbar-port-channel.ts's
|
|
308
|
+
// `onReady`): a subscriber registering AFTER the session it cares
|
|
309
|
+
// about already booted must not wait forever for an event that
|
|
310
|
+
// already happened. Scheduled on a microtask (never synchronous —
|
|
311
|
+
// matches the same-tick-dispose/re-attach safety the host-toolbar
|
|
312
|
+
// pattern documents) and RE-VALIDATED at fire time: the session must
|
|
313
|
+
// still be live and still bound to the SAME serviceWcId the event
|
|
314
|
+
// captured, so a session that respawned/closed between registration
|
|
315
|
+
// and the microtask can't resurrect a stale attach.
|
|
316
|
+
const candidate = state.lastServiceHostReady;
|
|
317
|
+
if (candidate) {
|
|
318
|
+
queueMicrotask(() => {
|
|
319
|
+
if (!serviceHostReadyListeners.has(listener))
|
|
320
|
+
return;
|
|
321
|
+
const ap = state.appSessions.get(candidate.appSessionId);
|
|
322
|
+
if (!ap || ap.serviceWc.isDestroyed() || ap.serviceWc.id !== candidate.serviceWcId)
|
|
323
|
+
return;
|
|
324
|
+
try {
|
|
325
|
+
listener(candidate);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.warn('[bridge-router] service-host-ready catch-up listener threw:', error);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return () => serviceHostReadyListeners.delete(listener);
|
|
333
|
+
},
|
|
283
334
|
getDevice: () => currentDevice,
|
|
284
335
|
setDevice: (device) => {
|
|
285
336
|
currentDevice = device;
|
|
@@ -970,6 +1021,11 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
970
1021
|
// any diagnostic queued for this session (or the global bucket) into its now
|
|
971
1022
|
// resolvable console. Safe to call even when nothing is queued.
|
|
972
1023
|
ctx.consoleForwarder?.notifyServiceHostReady?.(ap.appSessionId);
|
|
1024
|
+
// Same authoritative moment, exposed as a general subscribable event (see
|
|
1025
|
+
// `ServiceHostReadyEvent`'s doc comment) — other main-process consumers
|
|
1026
|
+
// (the right-panel DevTools attach) need this exact signal too and must
|
|
1027
|
+
// not poll `getServiceWc` on a fixed retry budget for it.
|
|
1028
|
+
state.emitServiceHostReady({ appId: ap.appId, appSessionId: ap.appSessionId, serviceWcId: ap.serviceWc.id });
|
|
973
1029
|
ap.logicInjected = await injectLogicBundle(ap);
|
|
974
1030
|
if (!ap.logicInjected) {
|
|
975
1031
|
// The compiled logic.js never executed, so `modDefine` registered nothing.
|
package/dist/main/ipc/index.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ export { registerPopoverIpc, popoverModule } from './popover.js';
|
|
|
4
4
|
export { registerProjectsIpc, projectsModule } from './projects.js';
|
|
5
5
|
export { registerSessionIpc, sessionModule } from './session.js';
|
|
6
6
|
export { registerSettingsIpc, settingsModule } from './settings.js';
|
|
7
|
+
export { registerInternalDevtoolsIpc } from './internal-devtools.js';
|
|
7
8
|
export { simulatorModule } from './simulator-module.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/main/ipc/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { registerPopoverIpc, popoverModule } from './popover.js';
|
|
|
4
4
|
export { registerProjectsIpc, projectsModule } from './projects.js';
|
|
5
5
|
export { registerSessionIpc, sessionModule } from './session.js';
|
|
6
6
|
export { registerSettingsIpc, settingsModule } from './settings.js';
|
|
7
|
+
export { registerInternalDevtoolsIpc } from './internal-devtools.js';
|
|
7
8
|
export { simulatorModule } from './simulator-module.js';
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
2
|
+
import { type SenderPolicy } from '../utils/ipc-registry.js';
|
|
3
|
+
import type { InternalDevtoolsWindow } from '../windows/internal-devtools-window/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Standalone internal (app-wide) DevTools debug window. The simulator
|
|
6
|
+
* toolbar's "debug" button opens (or focuses) it via the window controller —
|
|
7
|
+
* a module-local narrow deps interface, not `WorkbenchContext`, per the
|
|
8
|
+
* shrink-only WorkbenchContext-import gate (see eslint.config.js).
|
|
9
|
+
*/
|
|
10
|
+
export interface InternalDevtoolsIpcDeps {
|
|
11
|
+
internalDevtoolsWindow?: InternalDevtoolsWindow;
|
|
12
|
+
senderPolicy: SenderPolicy;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerInternalDevtoolsIpc(ctx: InternalDevtoolsIpcDeps): Disposable;
|
|
15
|
+
//# sourceMappingURL=internal-devtools.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InternalDevtoolsChannel } from '../../shared/ipc-channels.js';
|
|
2
|
+
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
3
|
+
export function registerInternalDevtoolsIpc(ctx) {
|
|
4
|
+
return new IpcRegistry(ctx.senderPolicy)
|
|
5
|
+
.handle(InternalDevtoolsChannel.Open, () => {
|
|
6
|
+
ctx.internalDevtoolsWindow?.open();
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=internal-devtools.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { WorkbenchAppConfig } from '../../shared/types.js';
|
|
2
|
+
import type { RuntimeBackend } from '@dimina-kit/electron-deck';
|
|
3
|
+
declare const stubs: {
|
|
4
|
+
handlers: Map<string, (...args: unknown[]) => unknown>;
|
|
5
|
+
projectsJsonPath: string;
|
|
6
|
+
getProjectsJson(): string | null;
|
|
7
|
+
setProjectsJson(v: string | null): void;
|
|
8
|
+
projectsWithAppJson: Set<string>;
|
|
9
|
+
makeEmitter: () => {
|
|
10
|
+
listeners: Record<string, Set<(...args: unknown[]) => unknown>>;
|
|
11
|
+
on(event: string, fn: (...args: unknown[]) => unknown): /*elided*/ any;
|
|
12
|
+
once(event: string, fn: (...args: unknown[]) => unknown): /*elided*/ any;
|
|
13
|
+
off(event: string, fn: (...args: unknown[]) => unknown): /*elided*/ any;
|
|
14
|
+
removeListener(event: string, fn: (...args: unknown[]) => unknown): /*elided*/ any;
|
|
15
|
+
emit(event: string, ...args: unknown[]): void;
|
|
16
|
+
};
|
|
17
|
+
reset: () => void;
|
|
18
|
+
};
|
|
19
|
+
declare const devkitStubs: {
|
|
20
|
+
sessionClose: import("vitest").Mock<() => Promise<void>>;
|
|
21
|
+
};
|
|
22
|
+
declare const viewManagerStubs: {
|
|
23
|
+
createdManagers: Array<{
|
|
24
|
+
disposeAll: () => void;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export { stubs, devkitStubs, viewManagerStubs };
|
|
28
|
+
/**
|
|
29
|
+
* Shared per-test setup both `devtools-backend-before-quit.test.ts` and
|
|
30
|
+
* `devtools-backend-shutdown-race.test.ts` need identically: reset modules
|
|
31
|
+
* (fresh `let`-scoped module-level state in `devtools-backend.ts` each
|
|
32
|
+
* test), reset the mock harness above, and dynamically re-import `electron`
|
|
33
|
+
* + `createDevtoolsBackend` — dynamic, not a static top-level import,
|
|
34
|
+
* because it must happen AFTER `vi.resetModules()` picks up a fresh module
|
|
35
|
+
* instance each test. Registers its own `beforeEach`; call once per
|
|
36
|
+
* describing test file. Returns a mutable state object (not individual
|
|
37
|
+
* values) because `beforeEach` reassigns its fields once per test, after
|
|
38
|
+
* this function itself has already returned.
|
|
39
|
+
*/
|
|
40
|
+
export interface BackendTestState {
|
|
41
|
+
createDevtoolsBackend: typeof import('./devtools-backend.js').createDevtoolsBackend;
|
|
42
|
+
electron: typeof import('electron');
|
|
43
|
+
}
|
|
44
|
+
export declare function registerBackendTestLifecycle(): BackendTestState;
|
|
45
|
+
/**
|
|
46
|
+
* Shared setup for the two tests (one in `devtools-backend-before-quit.test.ts`,
|
|
47
|
+
* one in `devtools-backend-shutdown-race.test.ts`) that need to observe
|
|
48
|
+
* `assemble()` PAUSED mid-flight, inside a still-pending `config.onSetup`:
|
|
49
|
+
* builds the backend with a gated `onSetup`, runs `beforeReady`/`assemble`,
|
|
50
|
+
* and waits until the assembled `ViewManager` is actually reachable (proof
|
|
51
|
+
* `createDevtoolsRuntime` has progressed past instance construction) before
|
|
52
|
+
* handing control back — the caller then fires whatever quit-path event it's
|
|
53
|
+
* testing while `onSetup` is still gated, and is responsible for calling
|
|
54
|
+
* `releaseOnSetup()` + awaiting `assemblePromise` itself.
|
|
55
|
+
*/
|
|
56
|
+
export declare function startAssemblingWithGatedOnSetup(createDevtoolsBackend: BackendTestState['createDevtoolsBackend'], extraConfig?: Omit<WorkbenchAppConfig, 'onSetup'>): Promise<{
|
|
57
|
+
backend: RuntimeBackend;
|
|
58
|
+
assemblePromise: Promise<void>;
|
|
59
|
+
releaseOnSetup: () => void;
|
|
60
|
+
}>;
|
|
61
|
+
//# sourceMappingURL=devtools-backend-before-quit.harness.d.ts.map
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vitest mock harness for `devtools-backend-before-quit.test.ts` and
|
|
3
|
+
* `devtools-backend-shutdown-race.test.ts` — both drive the REAL
|
|
4
|
+
* `createDevtoolsBackend`/`createDevtoolsRuntime` against this same
|
|
5
|
+
* electron/fs/`@dimina-kit/devkit`/view-manager mock set (lifted from
|
|
6
|
+
* `quit-flag-onclose.test.ts`), split out to keep each test file under the
|
|
7
|
+
* repo's 500-line file-length ratchet without duplicating ~350 lines of
|
|
8
|
+
* mock setup between them.
|
|
9
|
+
*
|
|
10
|
+
* `vi.mock(...)` calls here run once per importing test file's own isolated
|
|
11
|
+
* module graph (vitest scopes mocking per test file), so importing this
|
|
12
|
+
* module has the same effect as if each call were written directly in the
|
|
13
|
+
* importing file — both test files' `beforeEach` still dynamically
|
|
14
|
+
* `await import('./devtools-backend.js')` AFTER `vi.resetModules()`, well
|
|
15
|
+
* after this module's mock registrations have already run.
|
|
16
|
+
*/
|
|
17
|
+
import { beforeEach, expect, vi } from 'vitest';
|
|
18
|
+
// ── Hoisted stub state (lifted from quit-flag-onclose.test.ts) ────────────
|
|
19
|
+
// `export const x = vi.hoisted(...)` is rejected by vitest's hoisting
|
|
20
|
+
// transform ("Cannot export hoisted variable") — declare then export below.
|
|
21
|
+
const stubs = vi.hoisted(() => {
|
|
22
|
+
const handlers = new Map();
|
|
23
|
+
const projectsJsonPath = '/tmp/dimina-test-userdata/dimina-projects.json';
|
|
24
|
+
let projectsJsonContent = null;
|
|
25
|
+
const projectsWithAppJson = new Set();
|
|
26
|
+
function makeEmitter() {
|
|
27
|
+
const listeners = {};
|
|
28
|
+
return {
|
|
29
|
+
listeners,
|
|
30
|
+
on(event, fn) {
|
|
31
|
+
;
|
|
32
|
+
(listeners[event] ??= new Set()).add(fn);
|
|
33
|
+
return this;
|
|
34
|
+
},
|
|
35
|
+
once(event, fn) {
|
|
36
|
+
const wrap = (...args) => {
|
|
37
|
+
listeners[event]?.delete(wrap);
|
|
38
|
+
return fn(...args);
|
|
39
|
+
};
|
|
40
|
+
(listeners[event] ??= new Set()).add(wrap);
|
|
41
|
+
return this;
|
|
42
|
+
},
|
|
43
|
+
off(event, fn) {
|
|
44
|
+
listeners[event]?.delete(fn);
|
|
45
|
+
return this;
|
|
46
|
+
},
|
|
47
|
+
removeListener(event, fn) {
|
|
48
|
+
listeners[event]?.delete(fn);
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
emit(event, ...args) {
|
|
52
|
+
for (const fn of [...(listeners[event] ?? [])])
|
|
53
|
+
fn(...args);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function reset() {
|
|
58
|
+
handlers.clear();
|
|
59
|
+
projectsJsonContent = null;
|
|
60
|
+
projectsWithAppJson.clear();
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
handlers,
|
|
64
|
+
projectsJsonPath,
|
|
65
|
+
getProjectsJson() {
|
|
66
|
+
return projectsJsonContent;
|
|
67
|
+
},
|
|
68
|
+
setProjectsJson(v) {
|
|
69
|
+
projectsJsonContent = v;
|
|
70
|
+
},
|
|
71
|
+
projectsWithAppJson,
|
|
72
|
+
makeEmitter,
|
|
73
|
+
reset,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
// ── electron stub ────────────────────────────────────────────────────────
|
|
77
|
+
// `app` is a live emitter so `registerAppLifecycle` can attach a
|
|
78
|
+
// `before-quit` listener that the test fires directly.
|
|
79
|
+
vi.mock('electron', () => {
|
|
80
|
+
const ipcEmitter = stubs.makeEmitter();
|
|
81
|
+
const ipcMain = {
|
|
82
|
+
...ipcEmitter,
|
|
83
|
+
handle: vi.fn((channel, fn) => {
|
|
84
|
+
stubs.handlers.set(channel, fn);
|
|
85
|
+
}),
|
|
86
|
+
removeHandler: vi.fn((channel) => {
|
|
87
|
+
stubs.handlers.delete(channel);
|
|
88
|
+
}),
|
|
89
|
+
on: vi.fn((event, fn) => ipcEmitter.on(event, fn)),
|
|
90
|
+
removeListener: vi.fn((event, fn) => ipcEmitter.removeListener(event, fn)),
|
|
91
|
+
};
|
|
92
|
+
const appEmitter = stubs.makeEmitter();
|
|
93
|
+
const app = {
|
|
94
|
+
...appEmitter,
|
|
95
|
+
isPackaged: true,
|
|
96
|
+
whenReady: vi.fn(() => Promise.resolve()),
|
|
97
|
+
getPath: vi.fn(() => '/tmp/dimina-test-userdata'),
|
|
98
|
+
quit: vi.fn(),
|
|
99
|
+
setName: vi.fn(),
|
|
100
|
+
commandLine: {
|
|
101
|
+
getSwitchValue: vi.fn(() => ''),
|
|
102
|
+
appendSwitch: vi.fn(),
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
// `WebContents` and `BrowserWindow` below both wrap a `stubs.makeEmitter()`
|
|
106
|
+
// and need the SAME on/once/off/removeListener/emit delegation (the
|
|
107
|
+
// emitter's own methods use `this` internally, so a plain property copy
|
|
108
|
+
// would call them with the wrong receiver — each needs `.bind(em)`). Shared
|
|
109
|
+
// base class instead of re-declaring the same 5 bound fields in both.
|
|
110
|
+
class EmittingStub {
|
|
111
|
+
em = stubs.makeEmitter();
|
|
112
|
+
on = this.em.on.bind(this.em);
|
|
113
|
+
once = this.em.once.bind(this.em);
|
|
114
|
+
off = this.em.off.bind(this.em);
|
|
115
|
+
removeListener = this.em.removeListener.bind(this.em);
|
|
116
|
+
emit = this.em.emit.bind(this.em);
|
|
117
|
+
}
|
|
118
|
+
class WebContents extends EmittingStub {
|
|
119
|
+
destroyed = false;
|
|
120
|
+
id = Math.floor(Math.random() * 1e6);
|
|
121
|
+
send = vi.fn();
|
|
122
|
+
isDestroyed = () => this.destroyed;
|
|
123
|
+
openDevTools = vi.fn();
|
|
124
|
+
closeDevTools = vi.fn();
|
|
125
|
+
setDevToolsWebContents = vi.fn();
|
|
126
|
+
setWindowOpenHandler = vi.fn();
|
|
127
|
+
loadFile = vi.fn(() => Promise.resolve());
|
|
128
|
+
loadURL = vi.fn(() => Promise.resolve());
|
|
129
|
+
executeJavaScript = vi.fn(() => Promise.resolve(undefined));
|
|
130
|
+
reload = vi.fn();
|
|
131
|
+
getType = () => 'window';
|
|
132
|
+
getURL = () => '';
|
|
133
|
+
debugger = {
|
|
134
|
+
attach: vi.fn(),
|
|
135
|
+
detach: vi.fn(),
|
|
136
|
+
isAttached: () => false,
|
|
137
|
+
on: vi.fn(),
|
|
138
|
+
removeListener: vi.fn(),
|
|
139
|
+
sendCommand: vi.fn(() => Promise.resolve({ entries: [] })),
|
|
140
|
+
};
|
|
141
|
+
close = vi.fn(() => {
|
|
142
|
+
this.destroyed = true;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
class WebContentsView {
|
|
146
|
+
webContents = new WebContents();
|
|
147
|
+
setBounds = vi.fn();
|
|
148
|
+
setBackgroundColor = vi.fn();
|
|
149
|
+
}
|
|
150
|
+
class View {
|
|
151
|
+
children = [];
|
|
152
|
+
addChildView(child) {
|
|
153
|
+
this.children.push(child);
|
|
154
|
+
}
|
|
155
|
+
removeChildView(child) {
|
|
156
|
+
const i = this.children.indexOf(child);
|
|
157
|
+
if (i >= 0)
|
|
158
|
+
this.children.splice(i, 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
class BrowserWindow extends EmittingStub {
|
|
162
|
+
destroyed = false;
|
|
163
|
+
webContents = new WebContents();
|
|
164
|
+
contentView = new WebContentsView();
|
|
165
|
+
isDestroyed = () => this.destroyed;
|
|
166
|
+
getContentSize = () => [1280, 980];
|
|
167
|
+
setIcon = vi.fn();
|
|
168
|
+
setTitle = vi.fn();
|
|
169
|
+
show = vi.fn();
|
|
170
|
+
showInactive = vi.fn();
|
|
171
|
+
focus = vi.fn();
|
|
172
|
+
close = vi.fn();
|
|
173
|
+
destroy = vi.fn(() => {
|
|
174
|
+
this.destroyed = true;
|
|
175
|
+
});
|
|
176
|
+
loadFile = vi.fn(() => Promise.resolve());
|
|
177
|
+
loadURL = vi.fn(() => Promise.resolve());
|
|
178
|
+
static getAllWindows = vi.fn(() => []);
|
|
179
|
+
}
|
|
180
|
+
const sessionStub = {
|
|
181
|
+
fromPartition: vi.fn(() => ({
|
|
182
|
+
webRequest: {
|
|
183
|
+
onBeforeSendHeaders: vi.fn(),
|
|
184
|
+
onHeadersReceived: vi.fn(),
|
|
185
|
+
},
|
|
186
|
+
registerPreloadScript: vi.fn(),
|
|
187
|
+
protocol: { handle: vi.fn(), unhandle: vi.fn() },
|
|
188
|
+
})),
|
|
189
|
+
defaultSession: {
|
|
190
|
+
protocol: { handle: vi.fn(), unhandle: vi.fn() },
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
const dialog = {
|
|
194
|
+
showOpenDialog: vi.fn(() => Promise.resolve({ canceled: true, filePaths: [] })),
|
|
195
|
+
showMessageBox: vi.fn(() => Promise.resolve({ response: 0 })),
|
|
196
|
+
};
|
|
197
|
+
const Menu = {
|
|
198
|
+
buildFromTemplate: vi.fn((tpl) => ({ template: tpl })),
|
|
199
|
+
setApplicationMenu: vi.fn(),
|
|
200
|
+
};
|
|
201
|
+
const shell = {
|
|
202
|
+
openExternal: vi.fn(() => Promise.resolve()),
|
|
203
|
+
openPath: vi.fn(() => Promise.resolve('')),
|
|
204
|
+
};
|
|
205
|
+
const nativeImage = {
|
|
206
|
+
createFromPath: vi.fn(() => ({ isEmpty: () => true })),
|
|
207
|
+
};
|
|
208
|
+
const nativeTheme = { ...stubs.makeEmitter(), themeSource: 'system' };
|
|
209
|
+
const globalShortcut = {
|
|
210
|
+
register: vi.fn(() => false),
|
|
211
|
+
unregister: vi.fn(),
|
|
212
|
+
unregisterAll: vi.fn(),
|
|
213
|
+
};
|
|
214
|
+
const webContentsStatic = {
|
|
215
|
+
fromId: vi.fn(() => null),
|
|
216
|
+
getAllWebContents: vi.fn(() => []),
|
|
217
|
+
};
|
|
218
|
+
const Tray = vi.fn();
|
|
219
|
+
return {
|
|
220
|
+
app,
|
|
221
|
+
ipcMain,
|
|
222
|
+
BrowserWindow,
|
|
223
|
+
WebContentsView,
|
|
224
|
+
BrowserView: WebContentsView,
|
|
225
|
+
View,
|
|
226
|
+
webContents: webContentsStatic,
|
|
227
|
+
session: sessionStub,
|
|
228
|
+
protocol: { registerSchemesAsPrivileged: vi.fn(), handle: vi.fn(), unhandle: vi.fn() },
|
|
229
|
+
dialog,
|
|
230
|
+
Menu,
|
|
231
|
+
shell,
|
|
232
|
+
nativeImage,
|
|
233
|
+
nativeTheme,
|
|
234
|
+
globalShortcut,
|
|
235
|
+
Tray,
|
|
236
|
+
default: {},
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
vi.mock('fs', async () => {
|
|
240
|
+
const real = await vi.importActual('fs');
|
|
241
|
+
function existsSync(p) {
|
|
242
|
+
const s = String(p);
|
|
243
|
+
if (s === stubs.projectsJsonPath)
|
|
244
|
+
return stubs.getProjectsJson() !== null;
|
|
245
|
+
if (s.endsWith('/app.json') || s.endsWith('\\app.json')) {
|
|
246
|
+
const dir = s.replace(/[\\/]app\.json$/, '');
|
|
247
|
+
return stubs.projectsWithAppJson.has(dir);
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
function readFileSync(p, opts) {
|
|
252
|
+
const s = String(p);
|
|
253
|
+
if (s === stubs.projectsJsonPath) {
|
|
254
|
+
const content = stubs.getProjectsJson();
|
|
255
|
+
if (content === null)
|
|
256
|
+
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
257
|
+
return content;
|
|
258
|
+
}
|
|
259
|
+
return real.readFileSync(p, opts);
|
|
260
|
+
}
|
|
261
|
+
function writeFileSync(p, data) {
|
|
262
|
+
const s = String(p);
|
|
263
|
+
if (s === stubs.projectsJsonPath) {
|
|
264
|
+
stubs.setProjectsJson(typeof data === 'string' ? data : Buffer.from(data).toString('utf8'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const mocked = {
|
|
268
|
+
...real,
|
|
269
|
+
existsSync,
|
|
270
|
+
readFileSync,
|
|
271
|
+
writeFileSync,
|
|
272
|
+
mkdirSync: vi.fn(),
|
|
273
|
+
statSync: vi.fn(() => ({ isDirectory: () => true, isFile: () => false, size: 0, mtimeMs: 0 })),
|
|
274
|
+
watch: vi.fn(),
|
|
275
|
+
realpathSync: vi.fn((p) => p),
|
|
276
|
+
};
|
|
277
|
+
return { ...mocked, default: mocked };
|
|
278
|
+
});
|
|
279
|
+
const devkitStubs = vi.hoisted(() => ({
|
|
280
|
+
sessionClose: vi.fn(() => Promise.resolve()),
|
|
281
|
+
}));
|
|
282
|
+
vi.mock('@dimina-kit/devkit', () => ({
|
|
283
|
+
openProject: vi.fn(() => Promise.resolve({
|
|
284
|
+
port: 12345,
|
|
285
|
+
appInfo: { appId: 'fakeApp' },
|
|
286
|
+
close: devkitStubs.sessionClose,
|
|
287
|
+
})),
|
|
288
|
+
}));
|
|
289
|
+
// ── view-manager spy shim ───────────────────────────────────────────────
|
|
290
|
+
// `createDevtoolsBackend` keeps its assembled `instance` in a private
|
|
291
|
+
// closure — the returned `RuntimeBackend` exposes no getter for
|
|
292
|
+
// `instance.context.views`. Wrapping `createViewManager` here (real
|
|
293
|
+
// implementation, just observed) gives the test a handle on the exact
|
|
294
|
+
// `ViewManager` the backend's `assemble` wires up, equivalent to
|
|
295
|
+
// `vi.spyOn(instance.context.views, 'disposeAll')` without needing the
|
|
296
|
+
// backend to leak its internals.
|
|
297
|
+
const viewManagerStubs = vi.hoisted(() => ({
|
|
298
|
+
createdManagers: [],
|
|
299
|
+
}));
|
|
300
|
+
vi.mock('../services/views/view-manager.js', async (importOriginal) => {
|
|
301
|
+
const actual = await importOriginal();
|
|
302
|
+
return {
|
|
303
|
+
...actual,
|
|
304
|
+
createViewManager: (ctx) => {
|
|
305
|
+
const real = actual.createViewManager(ctx);
|
|
306
|
+
vi.spyOn(real, 'disposeAll');
|
|
307
|
+
viewManagerStubs.createdManagers.push(real);
|
|
308
|
+
return real;
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
export { stubs, devkitStubs, viewManagerStubs };
|
|
313
|
+
export function registerBackendTestLifecycle() {
|
|
314
|
+
const state = {};
|
|
315
|
+
beforeEach(async () => {
|
|
316
|
+
vi.resetModules();
|
|
317
|
+
stubs.reset();
|
|
318
|
+
devkitStubs.sessionClose.mockClear();
|
|
319
|
+
viewManagerStubs.createdManagers.length = 0;
|
|
320
|
+
state.electron = await import('electron');
|
|
321
|
+
({ createDevtoolsBackend: state.createDevtoolsBackend } = await import('./devtools-backend.js'));
|
|
322
|
+
});
|
|
323
|
+
return state;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Shared setup for the two tests (one in `devtools-backend-before-quit.test.ts`,
|
|
327
|
+
* one in `devtools-backend-shutdown-race.test.ts`) that need to observe
|
|
328
|
+
* `assemble()` PAUSED mid-flight, inside a still-pending `config.onSetup`:
|
|
329
|
+
* builds the backend with a gated `onSetup`, runs `beforeReady`/`assemble`,
|
|
330
|
+
* and waits until the assembled `ViewManager` is actually reachable (proof
|
|
331
|
+
* `createDevtoolsRuntime` has progressed past instance construction) before
|
|
332
|
+
* handing control back — the caller then fires whatever quit-path event it's
|
|
333
|
+
* testing while `onSetup` is still gated, and is responsible for calling
|
|
334
|
+
* `releaseOnSetup()` + awaiting `assemblePromise` itself.
|
|
335
|
+
*/
|
|
336
|
+
export async function startAssemblingWithGatedOnSetup(createDevtoolsBackend, extraConfig = {}) {
|
|
337
|
+
let releaseOnSetup = () => { };
|
|
338
|
+
const onSetupGate = new Promise((resolve) => {
|
|
339
|
+
releaseOnSetup = resolve;
|
|
340
|
+
});
|
|
341
|
+
const backend = createDevtoolsBackend({
|
|
342
|
+
...extraConfig,
|
|
343
|
+
onSetup: async () => {
|
|
344
|
+
await onSetupGate;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
backend.beforeReady?.({});
|
|
348
|
+
const assemblePromise = Promise.resolve(backend.assemble({}));
|
|
349
|
+
// Let `assemble` progress through `createDevtoolsRuntime` up to (and into)
|
|
350
|
+
// the pending `onSetup` gate — the instance must exist by then.
|
|
351
|
+
await vi.waitFor(() => {
|
|
352
|
+
expect(viewManagerStubs.createdManagers.length).toBeGreaterThan(0);
|
|
353
|
+
});
|
|
354
|
+
return { backend, assemblePromise, releaseOnSetup };
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=devtools-backend-before-quit.harness.js.map
|