@dimina-kit/devtools 0.3.1-dev.20260513123738 → 0.3.1-dev.20260515105504
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 +103 -41
- package/dist/main/api.d.ts +6 -2
- package/dist/main/api.js +5 -2
- package/dist/main/app/app.d.ts +1 -1
- package/dist/main/app/app.js +71 -36
- package/dist/main/app/launch.d.ts +2 -2
- package/dist/main/app/launch.js +6 -3
- package/dist/main/index.bundle.js +1074 -289
- package/dist/main/ipc/app.d.ts +2 -1
- package/dist/main/ipc/app.js +5 -4
- package/dist/main/ipc/index.d.ts +5 -4
- package/dist/main/ipc/index.js +5 -4
- package/dist/main/ipc/panels.d.ts +2 -1
- package/dist/main/ipc/panels.js +14 -9
- package/dist/main/ipc/popover.d.ts +4 -1
- package/dist/main/ipc/popover.js +15 -5
- package/dist/main/ipc/projects.d.ts +4 -1
- package/dist/main/ipc/projects.js +42 -5
- package/dist/main/ipc/session.d.ts +4 -1
- package/dist/main/ipc/session.js +26 -6
- package/dist/main/ipc/settings.d.ts +4 -1
- package/dist/main/ipc/settings.js +30 -17
- package/dist/main/ipc/simulator-module.d.ts +8 -0
- package/dist/main/ipc/simulator-module.js +19 -0
- package/dist/main/ipc/simulator.d.ts +2 -1
- package/dist/main/ipc/simulator.js +29 -15
- package/dist/main/ipc/toolbar.d.ts +2 -1
- package/dist/main/ipc/toolbar.js +3 -2
- package/dist/main/services/automation/index.js +88 -13
- package/dist/main/services/mcp/server.d.ts +2 -1
- package/dist/main/services/mcp/server.js +5 -0
- package/dist/main/services/module.d.ts +18 -0
- package/dist/main/services/module.js +2 -0
- package/dist/main/services/notifications/renderer-notifier.d.ts +2 -0
- package/dist/main/services/projects/project-repository.d.ts +1 -0
- package/dist/main/services/projects/project-repository.js +3 -0
- package/dist/main/services/settings/index.d.ts +4 -0
- package/dist/main/services/settings/index.js +12 -1
- package/dist/main/services/simulator/custom-apis.d.ts +10 -0
- package/dist/main/services/simulator/custom-apis.js +26 -0
- package/dist/main/services/simulator-storage/index.d.ts +22 -1
- package/dist/main/services/simulator-storage/index.js +365 -98
- package/dist/main/services/update/update-manager.d.ts +16 -1
- package/dist/main/services/update/update-manager.js +18 -15
- package/dist/main/services/views/view-manager.d.ts +4 -0
- package/dist/main/services/views/view-manager.js +33 -2
- package/dist/main/services/window-service.d.ts +24 -0
- package/dist/main/services/window-service.js +26 -0
- package/dist/main/services/workbench-context.d.ts +15 -0
- package/dist/main/services/workbench-context.js +6 -0
- package/dist/main/services/workspace/workspace-service.d.ts +1 -0
- package/dist/main/services/workspace/workspace-service.js +7 -3
- package/dist/main/simulator-apis.d.ts +23 -0
- package/dist/main/simulator-apis.js +24 -0
- package/dist/main/utils/disposable.d.ts +13 -0
- package/dist/main/utils/disposable.js +60 -0
- package/dist/main/utils/ipc-registry.d.ts +39 -0
- package/dist/main/utils/ipc-registry.js +109 -0
- package/dist/main/utils/ipc-schema.d.ts +28 -0
- package/dist/main/utils/ipc-schema.js +38 -0
- package/dist/main/utils/paths.d.ts +6 -0
- package/dist/main/utils/paths.js +6 -0
- package/dist/main/utils/sender-policy.d.ts +23 -0
- package/dist/main/utils/sender-policy.js +42 -0
- package/dist/main/windows/main-window/create.d.ts +6 -0
- package/dist/main/windows/main-window/create.js +65 -9
- package/dist/main/windows/main-window/events.d.ts +2 -1
- package/dist/main/windows/main-window/events.js +25 -2
- package/dist/main/windows/navigation-hardening.d.ts +24 -0
- package/dist/main/windows/navigation-hardening.js +55 -0
- package/dist/main/windows/settings-window/create.js +11 -1
- package/dist/preload/index.d.ts +17 -0
- package/dist/preload/index.js +17 -0
- package/dist/preload/instrumentation/app-data.js +178 -19
- package/dist/preload/runtime/custom-apis.d.ts +6 -0
- package/dist/preload/runtime/custom-apis.js +18 -0
- package/dist/preload/runtime/host.d.ts +1 -0
- package/dist/preload/runtime/host.js +13 -0
- package/dist/preload/windows/main.cjs +47 -0
- package/dist/preload/windows/main.cjs.map +7 -0
- package/dist/preload/windows/simulator.js +174 -18
- package/dist/renderer/assets/index-DW_0thr-.js +46 -0
- package/dist/renderer/assets/{input-DxfYnzX_.js → input-C5M5Wxn6.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-ClIvDcYY.js → ipc-transport-CUsl-fS2.js} +2 -2
- package/dist/renderer/assets/ipc-transport-CltRuvhi.css +1 -0
- package/dist/renderer/assets/{popover-CtXcBJtM.js → popover-BSBPVO0o.js} +2 -2
- package/dist/renderer/assets/{select-DMfp49f1.js → select-B1tF3UAc.js} +2 -2
- package/dist/renderer/assets/{settings-DteGraEU.js → settings-7w54H6p-.js} +2 -2
- package/dist/renderer/assets/settings-api-BjJalghB.js +2 -0
- package/dist/renderer/assets/workbenchSettings-q2_-nc8t.js +8 -0
- package/dist/renderer/entries/main/index.html +12 -5
- package/dist/renderer/entries/popover/index.html +7 -5
- package/dist/renderer/entries/settings/index.html +7 -5
- package/dist/renderer/entries/workbench-settings/index.html +6 -4
- package/dist/shared/ipc-channels.d.ts +22 -0
- package/dist/shared/ipc-channels.js +23 -0
- package/dist/shared/ipc-schemas.d.ts +113 -0
- package/dist/shared/ipc-schemas.js +128 -0
- package/dist/shared/types.d.ts +2 -0
- package/dist/simulator/assets/simulator-DL6kOCq-.js +9 -0
- package/dist/simulator/simulator.html +1 -1
- package/package.json +11 -27
- package/dist/preload/windows/main.d.ts +0 -6
- package/dist/preload/windows/main.js +0 -6
- package/dist/renderer/assets/index-D1p6crtf.js +0 -46
- package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +0 -1
- package/dist/renderer/assets/settings-api-CY84UMpN.js +0 -2
- package/dist/renderer/assets/workbenchSettings-DjELLnSJ.js +0 -8
- package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { WebContentsView, webContents } from 'electron';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { mainPreloadPath } from '../../utils/paths.js';
|
|
4
|
+
import { applyNavigationHardening } from '../../windows/navigation-hardening.js';
|
|
3
5
|
import * as layout from '../layout/index.js';
|
|
4
6
|
import { getDefaultTab } from '../workbench-context.js';
|
|
5
7
|
/**
|
|
@@ -143,8 +145,16 @@ export function createViewManager(ctx) {
|
|
|
143
145
|
async function showSettings() {
|
|
144
146
|
if (!settingsView) {
|
|
145
147
|
settingsView = new WebContentsView({
|
|
146
|
-
webPreferences: {
|
|
148
|
+
webPreferences: {
|
|
149
|
+
nodeIntegration: false,
|
|
150
|
+
contextIsolation: true,
|
|
151
|
+
sandbox: false,
|
|
152
|
+
preload: mainPreloadPath,
|
|
153
|
+
},
|
|
147
154
|
});
|
|
155
|
+
// Overlay loads mainPreloadPath, so the same navigation rules as the
|
|
156
|
+
// main window apply — see navigation-hardening.ts.
|
|
157
|
+
applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
|
|
148
158
|
await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
|
|
149
159
|
}
|
|
150
160
|
if (!settingsViewAdded) {
|
|
@@ -165,8 +175,15 @@ export function createViewManager(ctx) {
|
|
|
165
175
|
function showPopover(data) {
|
|
166
176
|
hidePopover();
|
|
167
177
|
const popover = new WebContentsView({
|
|
168
|
-
webPreferences: {
|
|
178
|
+
webPreferences: {
|
|
179
|
+
nodeIntegration: false,
|
|
180
|
+
contextIsolation: true,
|
|
181
|
+
sandbox: false,
|
|
182
|
+
preload: mainPreloadPath,
|
|
183
|
+
},
|
|
169
184
|
});
|
|
185
|
+
// Popover overlay loads mainPreloadPath — same navigation rules apply.
|
|
186
|
+
applyNavigationHardening(popover.webContents, ctx.rendererDir);
|
|
170
187
|
popoverView = popover;
|
|
171
188
|
popover.setBackgroundColor('#00000000');
|
|
172
189
|
ctx.mainWindow.contentView.addChildView(popover);
|
|
@@ -247,6 +264,20 @@ export function createViewManager(ctx) {
|
|
|
247
264
|
return null;
|
|
248
265
|
return settingsView.webContents;
|
|
249
266
|
},
|
|
267
|
+
getSettingsWebContentsId: () => {
|
|
268
|
+
if (!settingsView)
|
|
269
|
+
return null;
|
|
270
|
+
if (settingsView.webContents.isDestroyed())
|
|
271
|
+
return null;
|
|
272
|
+
return settingsView.webContents.id;
|
|
273
|
+
},
|
|
274
|
+
getPopoverWebContentsId: () => {
|
|
275
|
+
if (!popoverView)
|
|
276
|
+
return null;
|
|
277
|
+
if (popoverView.webContents.isDestroyed())
|
|
278
|
+
return null;
|
|
279
|
+
return popoverView.webContents.id;
|
|
280
|
+
},
|
|
250
281
|
resize,
|
|
251
282
|
setVisible,
|
|
252
283
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BrowserWindow } from 'electron';
|
|
2
|
+
/**
|
|
3
|
+
* Owns every top-level BrowserWindow the workbench is responsible for.
|
|
4
|
+
*
|
|
5
|
+
* Callers should prefer this service over reading bare `mainWindow` /
|
|
6
|
+
* `workbenchSettingsWindow` fields off `WorkbenchContext` — the latter
|
|
7
|
+
* remain as @deprecated synchronized mirrors during the migration window.
|
|
8
|
+
*/
|
|
9
|
+
export interface WindowService {
|
|
10
|
+
/** The primary devtools window. Created during app bootstrap. */
|
|
11
|
+
readonly mainWindow: BrowserWindow;
|
|
12
|
+
/** Standalone workbench-settings BrowserWindow, or null when closed. */
|
|
13
|
+
readonly settingsWindow: BrowserWindow | null;
|
|
14
|
+
/** Update the tracked settings window (null when closed/destroyed). */
|
|
15
|
+
setSettingsWindow(win: BrowserWindow | null): void;
|
|
16
|
+
/** Close the settings window if it is open and clear the reference. */
|
|
17
|
+
closeSettingsWindow(): void;
|
|
18
|
+
/** True when `webContentsId` belongs to the (alive) main window renderer. */
|
|
19
|
+
isMainSender(webContentsId: number): boolean;
|
|
20
|
+
/** True when `webContentsId` belongs to the (alive) settings window renderer. */
|
|
21
|
+
isSettingsWindowSender(webContentsId: number): boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function createWindowService(mainWindow: BrowserWindow): WindowService;
|
|
24
|
+
//# sourceMappingURL=window-service.d.ts.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createWindowService(mainWindow) {
|
|
2
|
+
let settingsWin = null;
|
|
3
|
+
return {
|
|
4
|
+
get mainWindow() {
|
|
5
|
+
return mainWindow;
|
|
6
|
+
},
|
|
7
|
+
get settingsWindow() {
|
|
8
|
+
return settingsWin;
|
|
9
|
+
},
|
|
10
|
+
setSettingsWindow(win) {
|
|
11
|
+
settingsWin = win;
|
|
12
|
+
},
|
|
13
|
+
closeSettingsWindow() {
|
|
14
|
+
if (settingsWin && !settingsWin.isDestroyed())
|
|
15
|
+
settingsWin.close();
|
|
16
|
+
settingsWin = null;
|
|
17
|
+
},
|
|
18
|
+
isMainSender(id) {
|
|
19
|
+
return !mainWindow.isDestroyed() && mainWindow.webContents.id === id;
|
|
20
|
+
},
|
|
21
|
+
isSettingsWindowSender(id) {
|
|
22
|
+
return !!settingsWin && !settingsWin.isDestroyed() && settingsWin.webContents.id === id;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=window-service.js.map
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import type { BrowserWindow } from 'electron';
|
|
2
2
|
import type { CompilationAdapter } from '../../shared/types.js';
|
|
3
|
+
import { DisposableRegistry } from '../utils/disposable.js';
|
|
4
|
+
import type { SenderPolicy } from '../utils/ipc-registry.js';
|
|
3
5
|
import { type RendererNotifier } from './notifications/renderer-notifier.js';
|
|
4
6
|
import { type ViewManager } from './views/view-manager.js';
|
|
7
|
+
import { type WindowService } from './window-service.js';
|
|
5
8
|
import { type WorkspaceService } from './workspace/workspace-service.js';
|
|
6
9
|
/**
|
|
7
10
|
* Shared mutable state for the workbench application.
|
|
8
11
|
* Passed to each IPC module so they can read/write shared state without closures.
|
|
9
12
|
*/
|
|
10
13
|
export interface WorkbenchContext {
|
|
14
|
+
/** @deprecated use ctx.windows.mainWindow */
|
|
11
15
|
mainWindow: BrowserWindow;
|
|
12
16
|
adapter: CompilationAdapter;
|
|
13
17
|
/** Absolute path to the preload script loaded into the simulator webview */
|
|
14
18
|
preloadPath: string;
|
|
15
19
|
/** Absolute path to the renderer dist directory */
|
|
16
20
|
rendererDir: string;
|
|
21
|
+
/** @deprecated use ctx.windows.settingsWindow / setSettingsWindow / closeSettingsWindow */
|
|
17
22
|
workbenchSettingsWindow: BrowserWindow | null;
|
|
18
23
|
/** Built-in panel IDs to display (default: all) */
|
|
19
24
|
panels: string[];
|
|
@@ -37,10 +42,20 @@ export interface WorkbenchContext {
|
|
|
37
42
|
};
|
|
38
43
|
/** Unified lifecycle manager for all overlay WebContentsViews */
|
|
39
44
|
views: ViewManager;
|
|
45
|
+
/** Owns BrowserWindow instances (main + settings) and their sender-id checks. */
|
|
46
|
+
windows: WindowService;
|
|
40
47
|
/** Unified main → renderer event dispatcher */
|
|
41
48
|
notify: RendererNotifier;
|
|
42
49
|
/** Single source of truth for project + session + per-project settings */
|
|
43
50
|
workspace: WorkspaceService;
|
|
51
|
+
/**
|
|
52
|
+
* Trust predicate consulted by `IpcRegistry` for every incoming IPC.
|
|
53
|
+
* Resolves the currently-trusted senders (main renderer + overlays)
|
|
54
|
+
* lazily so it stays correct as windows/views come and go.
|
|
55
|
+
*/
|
|
56
|
+
senderPolicy: SenderPolicy;
|
|
57
|
+
/** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
|
|
58
|
+
registry: DisposableRegistry;
|
|
44
59
|
}
|
|
45
60
|
export interface CreateContextOptions {
|
|
46
61
|
mainWindow: BrowserWindow;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { DisposableRegistry } from '../utils/disposable.js';
|
|
2
|
+
import { createWorkbenchSenderPolicy } from '../utils/sender-policy.js';
|
|
1
3
|
import { defaultAdapter } from './default-adapter.js';
|
|
2
4
|
import { createRendererNotifier, } from './notifications/renderer-notifier.js';
|
|
3
5
|
import { createViewManager } from './views/view-manager.js';
|
|
6
|
+
import { createWindowService } from './window-service.js';
|
|
4
7
|
import { createWorkspaceService, } from './workspace/workspace-service.js';
|
|
5
8
|
export function hasBuiltinPanel(ctx, panelId) {
|
|
6
9
|
return ctx.panels.includes(panelId);
|
|
@@ -25,9 +28,12 @@ export function createWorkbenchContext(opts) {
|
|
|
25
28
|
brandingProvider: opts.brandingProvider,
|
|
26
29
|
workbenchSettingsWindow: null,
|
|
27
30
|
};
|
|
31
|
+
ctx.registry = new DisposableRegistry();
|
|
32
|
+
ctx.windows = createWindowService(ctx.mainWindow);
|
|
28
33
|
ctx.views = createViewManager(ctx);
|
|
29
34
|
ctx.notify = createRendererNotifier(ctx);
|
|
30
35
|
ctx.workspace = createWorkspaceService(ctx);
|
|
36
|
+
ctx.senderPolicy = createWorkbenchSenderPolicy(ctx);
|
|
31
37
|
return ctx;
|
|
32
38
|
}
|
|
33
39
|
//# sourceMappingURL=workbench-context.js.map
|
|
@@ -28,6 +28,7 @@ export interface WorkspaceService {
|
|
|
28
28
|
listProjects(): Project[];
|
|
29
29
|
addProject(dirPath: string): Project;
|
|
30
30
|
removeProject(dirPath: string): void;
|
|
31
|
+
hasProject(dirPath: string): boolean;
|
|
31
32
|
validateProjectDir(dirPath: string): string | null;
|
|
32
33
|
openProject(projectPath: string): Promise<OpenProjectResult>;
|
|
33
34
|
closeProject(): Promise<void>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as repo from '../projects/project-repository.js';
|
|
2
2
|
import { clearSimulatorServicewechatReferer, setSimulatorServicewechatReferer, } from '../simulator/referer.js';
|
|
3
|
+
import { loadWorkbenchSettings } from '../settings/index.js';
|
|
3
4
|
/** Build a workspace service bound to the given workbench context. */
|
|
4
5
|
export function createWorkspaceService(ctx) {
|
|
5
6
|
let currentSession = null;
|
|
6
7
|
let currentProjectPath = '';
|
|
7
|
-
function sendStatus(status, message) {
|
|
8
|
-
ctx.notify.projectStatus({ status, message });
|
|
8
|
+
function sendStatus(status, message, hotReload) {
|
|
9
|
+
ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
|
|
9
10
|
}
|
|
10
11
|
function bestEffort(label, fn) {
|
|
11
12
|
try {
|
|
@@ -40,6 +41,7 @@ export function createWorkspaceService(ctx) {
|
|
|
40
41
|
listProjects: () => repo.listProjects(),
|
|
41
42
|
addProject: (dirPath) => repo.addProject(dirPath),
|
|
42
43
|
removeProject: (dirPath) => repo.removeProject(dirPath),
|
|
44
|
+
hasProject: (dirPath) => repo.hasProject(dirPath),
|
|
43
45
|
validateProjectDir: (dirPath) => repo.validateProjectDir(dirPath),
|
|
44
46
|
async openProject(projectPath) {
|
|
45
47
|
clearSimulatorServicewechatReferer();
|
|
@@ -58,12 +60,14 @@ export function createWorkspaceService(ctx) {
|
|
|
58
60
|
return { success: false, error: dirError };
|
|
59
61
|
}
|
|
60
62
|
sendStatus('compiling', '正在编译...');
|
|
63
|
+
const { compile } = loadWorkbenchSettings();
|
|
61
64
|
let session;
|
|
62
65
|
try {
|
|
63
66
|
session = await ctx.adapter.openProject({
|
|
64
67
|
projectPath,
|
|
65
68
|
sourcemap: true,
|
|
66
|
-
|
|
69
|
+
watch: compile.watch,
|
|
70
|
+
onRebuild: () => sendStatus('ready', '编译完成,已热更新', true),
|
|
67
71
|
onBuildError: (err) => sendStatus('error', String(err)),
|
|
68
72
|
});
|
|
69
73
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SimulatorApiHandler } from './services/simulator/custom-apis.js';
|
|
2
|
+
export type { SimulatorApiHandler } from './services/simulator/custom-apis.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register a handler for a simulator API callable from mini-program code.
|
|
5
|
+
*
|
|
6
|
+
* The handler runs in the Electron main process (Node.js context) and may
|
|
7
|
+
* use any main-process capability (filesystem, network, native deps). When
|
|
8
|
+
* the mini-program invokes `wx.<name>(params)` inside the simulator, the
|
|
9
|
+
* simulator forwards the call over IPC and resolves to the handler's return
|
|
10
|
+
* value. Errors thrown by the handler propagate back as a rejection.
|
|
11
|
+
*
|
|
12
|
+
* Re-registering the same name silently replaces the previous handler.
|
|
13
|
+
* The returned disposer removes only the registration it created — if the
|
|
14
|
+
* name was overwritten by a later `registerSimulatorApi`, calling the
|
|
15
|
+
* earlier disposer is a no-op.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* registerSimulatorApi('myCompany.login', async ({ username }) => {
|
|
19
|
+
* return await callInternalAuth(username)
|
|
20
|
+
* })
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerSimulatorApi(name: string, handler: SimulatorApiHandler): () => void;
|
|
23
|
+
//# sourceMappingURL=simulator-apis.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { simulatorApiRegistry } from './services/simulator/custom-apis.js';
|
|
2
|
+
/**
|
|
3
|
+
* Register a handler for a simulator API callable from mini-program code.
|
|
4
|
+
*
|
|
5
|
+
* The handler runs in the Electron main process (Node.js context) and may
|
|
6
|
+
* use any main-process capability (filesystem, network, native deps). When
|
|
7
|
+
* the mini-program invokes `wx.<name>(params)` inside the simulator, the
|
|
8
|
+
* simulator forwards the call over IPC and resolves to the handler's return
|
|
9
|
+
* value. Errors thrown by the handler propagate back as a rejection.
|
|
10
|
+
*
|
|
11
|
+
* Re-registering the same name silently replaces the previous handler.
|
|
12
|
+
* The returned disposer removes only the registration it created — if the
|
|
13
|
+
* name was overwritten by a later `registerSimulatorApi`, calling the
|
|
14
|
+
* earlier disposer is a no-op.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* registerSimulatorApi('myCompany.login', async ({ username }) => {
|
|
18
|
+
* return await callInternalAuth(username)
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
export function registerSimulatorApi(name, handler) {
|
|
22
|
+
return simulatorApiRegistry.register(name, handler);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=simulator-apis.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Disposable {
|
|
2
|
+
dispose(): void | Promise<void>;
|
|
3
|
+
}
|
|
4
|
+
export type DisposeFn = () => void | Promise<void>;
|
|
5
|
+
export declare function toDisposable(fn: DisposeFn): Disposable;
|
|
6
|
+
export declare class DisposableRegistry implements Disposable {
|
|
7
|
+
private entries;
|
|
8
|
+
private _disposed;
|
|
9
|
+
add(d: Disposable | DisposeFn): Disposable;
|
|
10
|
+
disposeAll(): Promise<void>;
|
|
11
|
+
dispose(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=disposable.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function toDisposable(fn) {
|
|
2
|
+
let done = false;
|
|
3
|
+
return {
|
|
4
|
+
dispose() {
|
|
5
|
+
if (done)
|
|
6
|
+
return;
|
|
7
|
+
done = true;
|
|
8
|
+
return fn();
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export class DisposableRegistry {
|
|
13
|
+
entries = [];
|
|
14
|
+
_disposed = false;
|
|
15
|
+
add(d) {
|
|
16
|
+
if (this._disposed) {
|
|
17
|
+
throw new Error('cannot add to disposed registry');
|
|
18
|
+
}
|
|
19
|
+
const fn = typeof d === 'function' ? d : () => d.dispose();
|
|
20
|
+
const entry = { fn, released: false };
|
|
21
|
+
this.entries.push(entry);
|
|
22
|
+
return {
|
|
23
|
+
dispose: () => {
|
|
24
|
+
if (entry.released)
|
|
25
|
+
return;
|
|
26
|
+
entry.released = true;
|
|
27
|
+
const i = this.entries.indexOf(entry);
|
|
28
|
+
if (i >= 0)
|
|
29
|
+
this.entries.splice(i, 1);
|
|
30
|
+
return fn();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async disposeAll() {
|
|
35
|
+
if (this._disposed)
|
|
36
|
+
return;
|
|
37
|
+
this._disposed = true;
|
|
38
|
+
const items = this.entries.slice().reverse();
|
|
39
|
+
this.entries = [];
|
|
40
|
+
const errors = [];
|
|
41
|
+
for (const entry of items) {
|
|
42
|
+
if (entry.released)
|
|
43
|
+
continue;
|
|
44
|
+
entry.released = true;
|
|
45
|
+
try {
|
|
46
|
+
await entry.fn();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
errors.push(e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
throw new AggregateError(errors, 'DisposableRegistry encountered errors during disposeAll');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
return this.disposeAll();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=disposable.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WebContents } from 'electron';
|
|
2
|
+
import { ipcMain } from 'electron';
|
|
3
|
+
import { type Disposable } from './disposable.js';
|
|
4
|
+
type HandleFn = Parameters<typeof ipcMain.handle>[1];
|
|
5
|
+
type ListenerFn = Parameters<typeof ipcMain.on>[1];
|
|
6
|
+
/**
|
|
7
|
+
* Trust predicate used to gate IPC delivery on the main process.
|
|
8
|
+
*
|
|
9
|
+
* Returns true when the calling WebContents is allowed to invoke / emit on
|
|
10
|
+
* the channel, false otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export type SenderPolicy = (sender: WebContents) => boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
|
|
15
|
+
* matching removeHandler/removeListener registered into an internal registry.
|
|
16
|
+
*
|
|
17
|
+
* Each `register*Ipc(ctx)` returns one of these as a Disposable so the
|
|
18
|
+
* workbench-level registry can dispose all built-in handlers in one shot.
|
|
19
|
+
*
|
|
20
|
+
* If a {@link SenderPolicy} is provided, every incoming invocation is gated:
|
|
21
|
+
* - `handle`: rejected senders cause the invoke promise to reject with
|
|
22
|
+
* `Error('IPC sender rejected for channel <channel>')`.
|
|
23
|
+
* - `on`: rejected senders are silently dropped (the original listener is
|
|
24
|
+
* never called).
|
|
25
|
+
* In both cases a single `console.warn` is emitted with the channel name
|
|
26
|
+
* and a short sender summary. Without a policy the wrapper is a no-op,
|
|
27
|
+
* preserving backwards compatibility for unit tests and callers that
|
|
28
|
+
* opted out.
|
|
29
|
+
*/
|
|
30
|
+
export declare class IpcRegistry implements Disposable {
|
|
31
|
+
private policy?;
|
|
32
|
+
private registry;
|
|
33
|
+
constructor(policy?: SenderPolicy | undefined);
|
|
34
|
+
handle(channel: string, fn: HandleFn): this;
|
|
35
|
+
on(channel: string, fn: ListenerFn): this;
|
|
36
|
+
dispose(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=ipc-registry.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ipcMain } from 'electron';
|
|
2
|
+
import { DisposableRegistry } from './disposable.js';
|
|
3
|
+
import { IpcValidationError } from './ipc-schema.js';
|
|
4
|
+
import { createLogger } from './logger.js';
|
|
5
|
+
const log = createLogger('ipc');
|
|
6
|
+
/**
|
|
7
|
+
* Funnels errors thrown by an `on()` listener into the logger so they don't
|
|
8
|
+
* escape into Electron's event loop. Validation errors get a compact `warn`
|
|
9
|
+
* (channel + zod paths), other errors get a full `error` with stack.
|
|
10
|
+
*/
|
|
11
|
+
function reportListenerError(channel, err) {
|
|
12
|
+
if (err instanceof IpcValidationError) {
|
|
13
|
+
log.warn(`schema reject on '${channel}' at [${err.paths.join(', ')}]`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (err instanceof Error) {
|
|
17
|
+
log.error(`listener threw on '${channel}': ${err.message}`, err.stack);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
log.error(`listener threw on '${channel}'`, err);
|
|
21
|
+
}
|
|
22
|
+
function isThenable(v) {
|
|
23
|
+
return (v != null &&
|
|
24
|
+
(typeof v === 'object' || typeof v === 'function') &&
|
|
25
|
+
typeof v.then === 'function');
|
|
26
|
+
}
|
|
27
|
+
function summarizeSender(sender) {
|
|
28
|
+
if (sender.isDestroyed())
|
|
29
|
+
return '<destroyed>';
|
|
30
|
+
const url = sender.getURL();
|
|
31
|
+
return `id=${sender.id} ${url.slice(0, 120)}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
|
|
35
|
+
* matching removeHandler/removeListener registered into an internal registry.
|
|
36
|
+
*
|
|
37
|
+
* Each `register*Ipc(ctx)` returns one of these as a Disposable so the
|
|
38
|
+
* workbench-level registry can dispose all built-in handlers in one shot.
|
|
39
|
+
*
|
|
40
|
+
* If a {@link SenderPolicy} is provided, every incoming invocation is gated:
|
|
41
|
+
* - `handle`: rejected senders cause the invoke promise to reject with
|
|
42
|
+
* `Error('IPC sender rejected for channel <channel>')`.
|
|
43
|
+
* - `on`: rejected senders are silently dropped (the original listener is
|
|
44
|
+
* never called).
|
|
45
|
+
* In both cases a single `console.warn` is emitted with the channel name
|
|
46
|
+
* and a short sender summary. Without a policy the wrapper is a no-op,
|
|
47
|
+
* preserving backwards compatibility for unit tests and callers that
|
|
48
|
+
* opted out.
|
|
49
|
+
*/
|
|
50
|
+
export class IpcRegistry {
|
|
51
|
+
policy;
|
|
52
|
+
registry = new DisposableRegistry();
|
|
53
|
+
constructor(policy) {
|
|
54
|
+
this.policy = policy;
|
|
55
|
+
}
|
|
56
|
+
handle(channel, fn) {
|
|
57
|
+
const policy = this.policy;
|
|
58
|
+
const guarded = policy
|
|
59
|
+
? (event, ...args) => {
|
|
60
|
+
const sender = event.sender;
|
|
61
|
+
if (!policy(sender)) {
|
|
62
|
+
console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
|
|
63
|
+
throw new Error(`IPC sender rejected for channel ${channel}`);
|
|
64
|
+
}
|
|
65
|
+
return fn(event, ...args);
|
|
66
|
+
}
|
|
67
|
+
: fn;
|
|
68
|
+
ipcMain.handle(channel, guarded);
|
|
69
|
+
this.registry.add(() => ipcMain.removeHandler(channel));
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
on(channel, fn) {
|
|
73
|
+
const policy = this.policy;
|
|
74
|
+
const raw = fn;
|
|
75
|
+
const safeInvoke = (event, args) => {
|
|
76
|
+
try {
|
|
77
|
+
const ret = raw(event, ...args);
|
|
78
|
+
if (isThenable(ret)) {
|
|
79
|
+
// Async listeners would otherwise leak rejections into Electron's
|
|
80
|
+
// event loop as `UnhandledPromiseRejection`. Funnel them into the
|
|
81
|
+
// same logger path as sync throws.
|
|
82
|
+
Promise.resolve(ret).catch((err) => reportListenerError(channel, err));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
reportListenerError(channel, err);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const guarded = policy
|
|
90
|
+
? (event, ...args) => {
|
|
91
|
+
const sender = event.sender;
|
|
92
|
+
if (!policy(sender)) {
|
|
93
|
+
console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
safeInvoke(event, args);
|
|
97
|
+
}
|
|
98
|
+
: (event, ...args) => safeInvoke(event, args);
|
|
99
|
+
ipcMain.on(channel, guarded);
|
|
100
|
+
this.registry.add(() => {
|
|
101
|
+
ipcMain.removeListener(channel, guarded);
|
|
102
|
+
});
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
dispose() {
|
|
106
|
+
return this.registry.dispose();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=ipc-registry.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link validate} when an IPC payload fails its zod schema.
|
|
4
|
+
*
|
|
5
|
+
* Carries the channel name plus a compact list of failing zod paths so
|
|
6
|
+
* the IpcRegistry wrapper can log a short, actionable summary without
|
|
7
|
+
* dumping the full payload.
|
|
8
|
+
*/
|
|
9
|
+
export declare class IpcValidationError extends Error {
|
|
10
|
+
readonly channel: string;
|
|
11
|
+
readonly paths: string[];
|
|
12
|
+
constructor(channel: string, issues: ReadonlyArray<{
|
|
13
|
+
path: ReadonlyArray<PropertyKey>;
|
|
14
|
+
}>);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse an IPC handler's argument tuple against a zod schema.
|
|
18
|
+
*
|
|
19
|
+
* On failure: warns to console with the channel name + issue list, then throws
|
|
20
|
+
* an {@link IpcValidationError} so that:
|
|
21
|
+
* - `ipcMain.handle` invocations reject the renderer's invoke promise
|
|
22
|
+
* without leaking schema internals.
|
|
23
|
+
* - `ipcMain.on` listeners (wrapped by IpcRegistry.on) can recognise the
|
|
24
|
+
* error type and log a structured warning instead of letting the throw
|
|
25
|
+
* escape into Electron's event loop.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validate<T extends z.ZodTypeAny>(channel: string, schema: T, args: unknown[]): z.infer<T>;
|
|
28
|
+
//# sourceMappingURL=ipc-schema.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by {@link validate} when an IPC payload fails its zod schema.
|
|
3
|
+
*
|
|
4
|
+
* Carries the channel name plus a compact list of failing zod paths so
|
|
5
|
+
* the IpcRegistry wrapper can log a short, actionable summary without
|
|
6
|
+
* dumping the full payload.
|
|
7
|
+
*/
|
|
8
|
+
export class IpcValidationError extends Error {
|
|
9
|
+
channel;
|
|
10
|
+
paths;
|
|
11
|
+
constructor(channel, issues) {
|
|
12
|
+
const paths = issues.map((i) => i.path.map((p) => String(p)).join('.') || '<root>');
|
|
13
|
+
super(`Invalid IPC arguments for ${channel}`);
|
|
14
|
+
this.name = 'IpcValidationError';
|
|
15
|
+
this.channel = channel;
|
|
16
|
+
this.paths = paths;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse an IPC handler's argument tuple against a zod schema.
|
|
21
|
+
*
|
|
22
|
+
* On failure: warns to console with the channel name + issue list, then throws
|
|
23
|
+
* an {@link IpcValidationError} so that:
|
|
24
|
+
* - `ipcMain.handle` invocations reject the renderer's invoke promise
|
|
25
|
+
* without leaking schema internals.
|
|
26
|
+
* - `ipcMain.on` listeners (wrapped by IpcRegistry.on) can recognise the
|
|
27
|
+
* error type and log a structured warning instead of letting the throw
|
|
28
|
+
* escape into Electron's event loop.
|
|
29
|
+
*/
|
|
30
|
+
export function validate(channel, schema, args) {
|
|
31
|
+
const r = schema.safeParse(args);
|
|
32
|
+
if (!r.success) {
|
|
33
|
+
console.warn('[ipc] schema reject', channel, r.error.issues);
|
|
34
|
+
throw new IpcValidationError(channel, r.error.issues);
|
|
35
|
+
}
|
|
36
|
+
return r.data;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=ipc-schema.js.map
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export declare const devtoolsPackageRoot: string;
|
|
2
2
|
export declare const rendererDir: string;
|
|
3
3
|
export declare const defaultPreloadPath: string;
|
|
4
|
+
/**
|
|
5
|
+
* Preload bundle for the main window, settings window, and the settings /
|
|
6
|
+
* popover overlay WebContentsViews. Exposes `window.devtools.ipc` via
|
|
7
|
+
* contextBridge so the renderer never needs `window.require('electron')`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const mainPreloadPath: string;
|
|
4
10
|
export declare const simulatorDir: string;
|
|
5
11
|
export declare function getRendererDir(): string;
|
|
6
12
|
export declare function getPreloadDir(): string;
|
package/dist/main/utils/paths.js
CHANGED
|
@@ -25,6 +25,12 @@ function resolveDevtoolsPackageRoot() {
|
|
|
25
25
|
export const devtoolsPackageRoot = resolveDevtoolsPackageRoot();
|
|
26
26
|
export const rendererDir = path.join(devtoolsPackageRoot, 'dist/renderer');
|
|
27
27
|
export const defaultPreloadPath = path.join(devtoolsPackageRoot, 'dist/preload/windows/simulator.js');
|
|
28
|
+
/**
|
|
29
|
+
* Preload bundle for the main window, settings window, and the settings /
|
|
30
|
+
* popover overlay WebContentsViews. Exposes `window.devtools.ipc` via
|
|
31
|
+
* contextBridge so the renderer never needs `window.require('electron')`.
|
|
32
|
+
*/
|
|
33
|
+
export const mainPreloadPath = path.join(devtoolsPackageRoot, 'dist/preload/windows/main.cjs');
|
|
28
34
|
export const simulatorDir = path.join(devtoolsPackageRoot, 'dist/simulator');
|
|
29
35
|
export function getRendererDir() {
|
|
30
36
|
return rendererDir;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
|
+
import type { SenderPolicy } from './ipc-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Default workbench sender white-list.
|
|
5
|
+
*
|
|
6
|
+
* An IPC message is accepted only when its `event.sender` matches one of
|
|
7
|
+
* the WebContents the main process itself created and trusts:
|
|
8
|
+
* - the main window's renderer
|
|
9
|
+
* - the optional workbench settings BrowserWindow's renderer (when open)
|
|
10
|
+
* - the settings overlay view (when open)
|
|
11
|
+
* - the popover overlay view (when open)
|
|
12
|
+
*
|
|
13
|
+
* The simulator webview is intentionally NOT on this list: its preload
|
|
14
|
+
* (`src/preload/windows/simulator.ts`) uses `ipcRenderer.sendToHost`
|
|
15
|
+
* exclusively, never `invoke`/`send`, so it can't legitimately reach an
|
|
16
|
+
* ipcMain handler. Including it would only widen the attack surface if
|
|
17
|
+
* the guest were ever compromised.
|
|
18
|
+
*
|
|
19
|
+
* Any other sender — including a stale/destroyed sender or an unknown
|
|
20
|
+
* iframe — is rejected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
|
|
23
|
+
//# sourceMappingURL=sender-policy.d.ts.map
|