@dimina-kit/devtools 0.3.1 → 0.3.2-dev.20260522085907

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.
Files changed (190) hide show
  1. package/README.md +297 -133
  2. package/dist/main/api.d.ts +6 -2
  3. package/dist/main/api.js +5 -2
  4. package/dist/main/app/app.d.ts +1 -1
  5. package/dist/main/app/app.js +84 -36
  6. package/dist/main/app/bootstrap.js +2 -1
  7. package/dist/main/app/launch.d.ts +2 -2
  8. package/dist/main/app/launch.js +10 -8
  9. package/dist/main/index.bundle.js +1567 -477
  10. package/dist/main/ipc/app.d.ts +2 -1
  11. package/dist/main/ipc/app.js +5 -4
  12. package/dist/main/ipc/index.d.ts +5 -4
  13. package/dist/main/ipc/index.js +5 -4
  14. package/dist/main/ipc/panels.d.ts +2 -1
  15. package/dist/main/ipc/panels.js +16 -13
  16. package/dist/main/ipc/popover.d.ts +4 -1
  17. package/dist/main/ipc/popover.js +15 -5
  18. package/dist/main/ipc/projects.d.ts +6 -1
  19. package/dist/main/ipc/projects.js +127 -6
  20. package/dist/main/ipc/session.d.ts +4 -1
  21. package/dist/main/ipc/session.js +34 -6
  22. package/dist/main/ipc/settings.d.ts +4 -1
  23. package/dist/main/ipc/settings.js +51 -23
  24. package/dist/main/ipc/simulator-module.d.ts +8 -0
  25. package/dist/main/ipc/simulator-module.js +19 -0
  26. package/dist/main/ipc/simulator.d.ts +2 -1
  27. package/dist/main/ipc/simulator.js +23 -18
  28. package/dist/main/ipc/toolbar.d.ts +2 -1
  29. package/dist/main/ipc/toolbar.js +3 -2
  30. package/dist/main/menu/index.js +11 -0
  31. package/dist/main/services/automation/exec.d.ts +2 -0
  32. package/dist/main/services/automation/exec.js +7 -9
  33. package/dist/main/services/automation/handlers/app.js +37 -51
  34. package/dist/main/services/automation/handlers/element.js +27 -41
  35. package/dist/main/services/automation/index.js +88 -13
  36. package/dist/main/services/default-adapter.js +3 -3
  37. package/dist/main/services/mcp/index.d.ts +1 -0
  38. package/dist/main/services/mcp/index.js +1 -0
  39. package/dist/main/services/mcp/server.d.ts +2 -1
  40. package/dist/main/services/mcp/server.js +10 -0
  41. package/dist/main/services/mcp/status.d.ts +10 -0
  42. package/dist/main/services/mcp/status.js +14 -0
  43. package/dist/main/services/mcp/target-manager.js +2 -1
  44. package/dist/main/services/module.d.ts +18 -0
  45. package/dist/main/services/module.js +2 -0
  46. package/dist/main/services/notifications/renderer-notifier.d.ts +5 -3
  47. package/dist/main/services/notifications/renderer-notifier.js +3 -6
  48. package/dist/main/services/projects/builtin-templates.d.ts +3 -0
  49. package/dist/main/services/projects/builtin-templates.js +24 -0
  50. package/dist/main/services/projects/create-project-service.d.ts +7 -0
  51. package/dist/main/services/projects/create-project-service.js +84 -0
  52. package/dist/main/services/projects/index.d.ts +7 -0
  53. package/dist/main/services/projects/index.js +9 -0
  54. package/dist/main/services/projects/local-provider.d.ts +3 -0
  55. package/dist/main/services/projects/local-provider.js +25 -0
  56. package/dist/main/services/projects/project-repository.d.ts +1 -1
  57. package/dist/main/services/projects/project-repository.js +3 -0
  58. package/dist/main/services/projects/templates.d.ts +21 -0
  59. package/dist/main/services/projects/templates.js +39 -0
  60. package/dist/main/services/projects/thumbnail.d.ts +8 -0
  61. package/dist/main/services/projects/thumbnail.js +37 -0
  62. package/dist/main/services/projects/types.d.ts +118 -0
  63. package/dist/main/services/projects/types.js +19 -0
  64. package/dist/main/services/settings/index.d.ts +11 -0
  65. package/dist/main/services/settings/index.js +18 -2
  66. package/dist/main/services/simulator/custom-apis.d.ts +10 -0
  67. package/dist/main/services/simulator/custom-apis.js +26 -0
  68. package/dist/main/services/simulator-storage/index.d.ts +22 -1
  69. package/dist/main/services/simulator-storage/index.js +334 -97
  70. package/dist/main/services/update/update-manager.d.ts +16 -1
  71. package/dist/main/services/update/update-manager.js +18 -15
  72. package/dist/main/services/views/view-manager.d.ts +7 -7
  73. package/dist/main/services/views/view-manager.js +53 -29
  74. package/dist/main/services/window-service.d.ts +24 -0
  75. package/dist/main/services/window-service.js +26 -0
  76. package/dist/main/services/workbench-context.d.ts +49 -2
  77. package/dist/main/services/workbench-context.js +15 -2
  78. package/dist/main/services/workspace/workspace-service.d.ts +9 -6
  79. package/dist/main/services/workspace/workspace-service.js +71 -11
  80. package/dist/main/simulator-apis.d.ts +23 -0
  81. package/dist/main/simulator-apis.js +24 -0
  82. package/dist/main/utils/disposable.d.ts +13 -0
  83. package/dist/main/utils/disposable.js +60 -0
  84. package/dist/main/utils/ipc-registry.d.ts +39 -0
  85. package/dist/main/utils/ipc-registry.js +109 -0
  86. package/dist/main/utils/ipc-schema.d.ts +28 -0
  87. package/dist/main/utils/ipc-schema.js +38 -0
  88. package/dist/main/utils/paths.d.ts +6 -0
  89. package/dist/main/utils/paths.js +6 -0
  90. package/dist/main/utils/sender-policy.d.ts +25 -0
  91. package/dist/main/utils/sender-policy.js +44 -0
  92. package/dist/main/utils/theme.d.ts +24 -0
  93. package/dist/main/utils/theme.js +38 -1
  94. package/dist/main/windows/main-window/create.d.ts +6 -0
  95. package/dist/main/windows/main-window/create.js +66 -10
  96. package/dist/main/windows/main-window/events.d.ts +2 -1
  97. package/dist/main/windows/main-window/events.js +25 -2
  98. package/dist/main/windows/navigation-hardening.d.ts +24 -0
  99. package/dist/main/windows/navigation-hardening.js +55 -0
  100. package/dist/main/windows/settings-window/create.js +11 -1
  101. package/dist/preload/index.d.ts +22 -0
  102. package/dist/preload/index.js +19 -0
  103. package/dist/preload/instrumentation/app-data.d.ts +32 -2
  104. package/dist/preload/instrumentation/app-data.js +253 -79
  105. package/dist/preload/instrumentation/index.d.ts +2 -2
  106. package/dist/preload/instrumentation/index.js +2 -2
  107. package/dist/preload/instrumentation/wxml.d.ts +16 -4
  108. package/dist/preload/instrumentation/wxml.js +188 -178
  109. package/dist/preload/miniapp-snapshot/host.d.ts +12 -0
  110. package/dist/preload/miniapp-snapshot/host.js +117 -0
  111. package/dist/preload/miniapp-snapshot/types.d.ts +62 -0
  112. package/dist/preload/miniapp-snapshot/types.js +15 -0
  113. package/dist/preload/runtime/bridge.js +5 -24
  114. package/dist/preload/runtime/custom-apis.d.ts +6 -0
  115. package/dist/preload/runtime/custom-apis.js +43 -0
  116. package/dist/preload/runtime/host.d.ts +1 -0
  117. package/dist/preload/runtime/host.js +13 -0
  118. package/dist/preload/shared/expose.d.ts +15 -0
  119. package/dist/preload/shared/expose.js +33 -0
  120. package/dist/preload/shared/page-iframe.d.ts +3 -0
  121. package/dist/preload/shared/page-iframe.js +6 -0
  122. package/dist/preload/shared/types.d.ts +2 -0
  123. package/dist/preload/windows/main.cjs +47 -0
  124. package/dist/preload/windows/main.cjs.map +7 -0
  125. package/dist/preload/windows/simulator.js +501 -272
  126. package/dist/renderer/assets/constants-DI382mTP.js +2 -0
  127. package/dist/renderer/assets/index-D8VFYBh-.js +46 -0
  128. package/dist/renderer/assets/{input-DxfYnzX_.js → input-CoWcfa8u.js} +2 -2
  129. package/dist/renderer/assets/ipc-transport-Cd2xPrsa.js +9 -0
  130. package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +1 -0
  131. package/dist/renderer/assets/popover-Nq5gT9DO.js +2 -0
  132. package/dist/renderer/assets/select-By5KH-UG.js +2 -0
  133. package/dist/renderer/assets/{settings-DteGraEU.js → settings-Dj_N70qR.js} +2 -2
  134. package/dist/renderer/assets/settings-api-Do0_Smq1.js +2 -0
  135. package/dist/renderer/assets/workbenchSettings-CazSLGYt.js +8 -0
  136. package/dist/renderer/entries/main/index.html +13 -5
  137. package/dist/renderer/entries/popover/index.html +8 -5
  138. package/dist/renderer/entries/settings/index.html +7 -5
  139. package/dist/renderer/entries/workbench-settings/index.html +7 -4
  140. package/dist/shared/ipc-channels.d.ts +46 -13
  141. package/dist/shared/ipc-channels.js +60 -14
  142. package/dist/shared/ipc-schemas.d.ts +118 -0
  143. package/dist/shared/ipc-schemas.js +133 -0
  144. package/dist/shared/simulator-route.d.ts +72 -0
  145. package/dist/shared/simulator-route.js +169 -0
  146. package/dist/shared/types.d.ts +82 -8
  147. package/dist/simulator/assets/simulator-DJCozrU5.js +9 -0
  148. package/dist/simulator/simulator.html +6 -43
  149. package/package.json +18 -29
  150. package/templates/blank/app.js +6 -0
  151. package/templates/blank/app.json +11 -0
  152. package/templates/blank/app.wxss +5 -0
  153. package/templates/blank/pages/index/index.js +6 -0
  154. package/templates/blank/pages/index/index.json +3 -0
  155. package/templates/blank/pages/index/index.wxml +3 -0
  156. package/templates/blank/pages/index/index.wxss +11 -0
  157. package/templates/blank/project.config.json +13 -0
  158. package/templates/blank/sitemap.json +9 -0
  159. package/templates/taro-todo/app.js +1 -0
  160. package/templates/taro-todo/app.json +1 -0
  161. package/templates/taro-todo/app.wxss +1 -0
  162. package/templates/taro-todo/base.wxml +851 -0
  163. package/templates/taro-todo/common.js +1 -0
  164. package/templates/taro-todo/comp.js +1 -0
  165. package/templates/taro-todo/comp.json +1 -0
  166. package/templates/taro-todo/comp.wxml +2 -0
  167. package/templates/taro-todo/custom-wrapper.js +1 -0
  168. package/templates/taro-todo/custom-wrapper.json +1 -0
  169. package/templates/taro-todo/custom-wrapper.wxml +4 -0
  170. package/templates/taro-todo/pages/index/index.js +2 -0
  171. package/templates/taro-todo/pages/index/index.json +1 -0
  172. package/templates/taro-todo/pages/index/index.wxml +2 -0
  173. package/templates/taro-todo/pages/index/index.wxss +1 -0
  174. package/templates/taro-todo/project.config.json +44 -0
  175. package/templates/taro-todo/runtime.js +1 -0
  176. package/templates/taro-todo/taro.js +2 -0
  177. package/templates/taro-todo/utils.wxs +39 -0
  178. package/templates/taro-todo/vendors.js +2 -0
  179. package/dist/preload/instrumentation/disposable.d.ts +0 -11
  180. package/dist/preload/instrumentation/disposable.js +0 -15
  181. package/dist/preload/windows/main.d.ts +0 -6
  182. package/dist/preload/windows/main.js +0 -6
  183. package/dist/renderer/assets/index-D1p6crtf.js +0 -46
  184. package/dist/renderer/assets/ipc-transport-ClIvDcYY.js +0 -9
  185. package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +0 -1
  186. package/dist/renderer/assets/popover-CtXcBJtM.js +0 -2
  187. package/dist/renderer/assets/select-DMfp49f1.js +0 -2
  188. package/dist/renderer/assets/settings-api-CY84UMpN.js +0 -2
  189. package/dist/renderer/assets/workbenchSettings-DjELLnSJ.js +0 -8
  190. package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
@@ -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;
@@ -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,25 @@
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. Anything it
14
+ * needs from main (currently just the custom-apis bridge — see
15
+ * `installCustomApisBridge` in `src/preload/runtime/custom-apis.ts` and the
16
+ * matching `useCustomApiProxy` host hook) proxies through the trusted
17
+ * main-window renderer via `ipcRenderer.sendToHost` + `<webview>.send`.
18
+ * Keeping the guest off this list contains the blast radius if the
19
+ * simulator content is ever compromised.
20
+ *
21
+ * Any other sender — including a stale/destroyed sender or an unknown
22
+ * iframe — is rejected.
23
+ */
24
+ export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
25
+ //# sourceMappingURL=sender-policy.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Default workbench sender white-list.
3
+ *
4
+ * An IPC message is accepted only when its `event.sender` matches one of
5
+ * the WebContents the main process itself created and trusts:
6
+ * - the main window's renderer
7
+ * - the optional workbench settings BrowserWindow's renderer (when open)
8
+ * - the settings overlay view (when open)
9
+ * - the popover overlay view (when open)
10
+ *
11
+ * The simulator webview is intentionally NOT on this list. Anything it
12
+ * needs from main (currently just the custom-apis bridge — see
13
+ * `installCustomApisBridge` in `src/preload/runtime/custom-apis.ts` and the
14
+ * matching `useCustomApiProxy` host hook) proxies through the trusted
15
+ * main-window renderer via `ipcRenderer.sendToHost` + `<webview>.send`.
16
+ * Keeping the guest off this list contains the blast radius if the
17
+ * simulator content is ever compromised.
18
+ *
19
+ * Any other sender — including a stale/destroyed sender or an unknown
20
+ * iframe — is rejected.
21
+ */
22
+ export function createWorkbenchSenderPolicy(ctx) {
23
+ return (sender) => {
24
+ if (sender.isDestroyed())
25
+ return false;
26
+ // Main window renderer
27
+ if (ctx.windows.isMainSender(sender.id))
28
+ return true;
29
+ // Standalone settings BrowserWindow renderer
30
+ if (ctx.windows.isSettingsWindowSender(sender.id))
31
+ return true;
32
+ // Settings overlay view (mounted inside the main window)
33
+ const settingsViewId = ctx.views.getSettingsWebContentsId();
34
+ if (settingsViewId != null && sender.id === settingsViewId)
35
+ return true;
36
+ // Popover overlay view
37
+ const popoverViewId = ctx.views.getPopoverWebContentsId();
38
+ if (popoverViewId != null && sender.id === popoverViewId)
39
+ return true;
40
+ // simulator <webview> proxies through main-window renderer (see file header).
41
+ return false;
42
+ };
43
+ }
44
+ //# sourceMappingURL=sender-policy.js.map
@@ -1,3 +1,4 @@
1
+ import { type Disposable } from './disposable.js';
1
2
  /**
2
3
  * Background color that matches the current system color scheme.
3
4
  * Pass as `backgroundColor` when creating BrowserWindows to prevent
@@ -7,4 +8,27 @@
7
8
  * Light: hsl(0 0% 98%) ≈ #fafafa (--color-bg)
8
9
  */
9
10
  export declare function themeBg(): string;
11
+ /**
12
+ * Install a single process-wide listener that keeps every window's native
13
+ * `backgroundColor` in sync with the active color scheme.
14
+ *
15
+ * A window's `backgroundColor` is otherwise frozen at the value passed to its
16
+ * constructor: after a theme switch the stale color bleeds through wherever
17
+ * native chrome and the WebContents meet. On Windows/Linux (in-window menu
18
+ * bar) it shows as a light hairline between the menu bar and the page; on
19
+ * macOS (global menu bar) it is latent — only a wrong-color flash on resize.
20
+ * The defect is platform-agnostic, so the fix is too: one `nativeTheme`
21
+ * `updated` listener re-syncs every current and future BrowserWindow.
22
+ *
23
+ * Known limitation: on Linux the `updated` event does not fire for OS-level
24
+ * system theme changes (electron/electron#25925) — only for explicit
25
+ * `nativeTheme.themeSource` assignments. In-app theme switches go through
26
+ * `applyTheme()` and are unaffected on every platform; the gap is limited to
27
+ * Linux + `theme: 'system'` + an OS theme change, an upstream Electron
28
+ * limitation that also leaves the renderer's `prefers-color-scheme` stale.
29
+ *
30
+ * Returns a Disposable that detaches the listener. Install it once during
31
+ * app setup and hand the Disposable to the workbench registry.
32
+ */
33
+ export declare function installThemeBackgroundSync(): Disposable;
10
34
  //# sourceMappingURL=theme.d.ts.map
@@ -1,4 +1,5 @@
1
- import { nativeTheme } from 'electron';
1
+ import { BrowserWindow, nativeTheme } from 'electron';
2
+ import { toDisposable } from './disposable.js';
2
3
  /**
3
4
  * Background color that matches the current system color scheme.
4
5
  * Pass as `backgroundColor` when creating BrowserWindows to prevent
@@ -10,4 +11,40 @@ import { nativeTheme } from 'electron';
10
11
  export function themeBg() {
11
12
  return nativeTheme.shouldUseDarkColors ? '#1a1a1a' : '#fafafa';
12
13
  }
14
+ /**
15
+ * Install a single process-wide listener that keeps every window's native
16
+ * `backgroundColor` in sync with the active color scheme.
17
+ *
18
+ * A window's `backgroundColor` is otherwise frozen at the value passed to its
19
+ * constructor: after a theme switch the stale color bleeds through wherever
20
+ * native chrome and the WebContents meet. On Windows/Linux (in-window menu
21
+ * bar) it shows as a light hairline between the menu bar and the page; on
22
+ * macOS (global menu bar) it is latent — only a wrong-color flash on resize.
23
+ * The defect is platform-agnostic, so the fix is too: one `nativeTheme`
24
+ * `updated` listener re-syncs every current and future BrowserWindow.
25
+ *
26
+ * Known limitation: on Linux the `updated` event does not fire for OS-level
27
+ * system theme changes (electron/electron#25925) — only for explicit
28
+ * `nativeTheme.themeSource` assignments. In-app theme switches go through
29
+ * `applyTheme()` and are unaffected on every platform; the gap is limited to
30
+ * Linux + `theme: 'system'` + an OS theme change, an upstream Electron
31
+ * limitation that also leaves the renderer's `prefers-color-scheme` stale.
32
+ *
33
+ * Returns a Disposable that detaches the listener. Install it once during
34
+ * app setup and hand the Disposable to the workbench registry.
35
+ */
36
+ export function installThemeBackgroundSync() {
37
+ const apply = () => {
38
+ const bg = themeBg();
39
+ for (const win of BrowserWindow.getAllWindows()) {
40
+ if (!win.isDestroyed()) {
41
+ win.setBackgroundColor(bg);
42
+ }
43
+ }
44
+ };
45
+ nativeTheme.on('updated', apply);
46
+ return toDisposable(() => {
47
+ nativeTheme.removeListener('updated', apply);
48
+ });
49
+ }
13
50
  //# sourceMappingURL=theme.js.map
@@ -6,6 +6,12 @@ export interface WindowOptions {
6
6
  minWidth?: number;
7
7
  minHeight?: number;
8
8
  indexHtml: string;
9
+ /**
10
+ * Absolute path to the simulator preload bundle. Registered at the session
11
+ * level so every frame inside the simulator <webview> gets the preload that
12
+ * exposes the `wx` runtime to the mini-program.
13
+ */
14
+ simulatorPreloadPath: string;
9
15
  }
10
16
  export declare function createMainWindow(opts: WindowOptions): BrowserWindow;
11
17
  //# sourceMappingURL=create.d.ts.map
@@ -1,12 +1,21 @@
1
- import { app, BrowserWindow, View, globalShortcut, session } from 'electron';
1
+ import { app, BrowserWindow, View, session, shell } from 'electron';
2
+ import path from 'path';
2
3
  import { getSimulatorServicewechatReferer } from '../../services/simulator/referer.js';
4
+ import { mainPreloadPath } from '../../utils/paths.js';
3
5
  import { themeBg } from '../../utils/theme.js';
6
+ import { applyNavigationHardening, handleWindowOpenExternal, } from '../navigation-hardening.js';
4
7
  let simulatorSessionConfigured = false;
5
- function configureSimulatorSession() {
8
+ function configureSimulatorSession(simulatorPreloadPath) {
6
9
  if (simulatorSessionConfigured)
7
10
  return;
8
11
  simulatorSessionConfigured = true;
9
12
  const simulatorSession = session.fromPartition('persist:simulator');
13
+ // Inject the simulator preload on every frame in this session. This is the
14
+ // mini-program runtime entry point (exposes `wx`, page registers, etc.).
15
+ simulatorSession.registerPreloadScript({
16
+ type: 'frame',
17
+ filePath: simulatorPreloadPath,
18
+ });
10
19
  simulatorSession.webRequest.onBeforeSendHeaders((details, callback) => {
11
20
  const forcedReferer = getSimulatorServicewechatReferer();
12
21
  if (forcedReferer) {
@@ -20,12 +29,12 @@ function configureSimulatorSession() {
20
29
  headers['access-control-allow-headers'] = ['*'];
21
30
  headers['access-control-allow-methods'] = ['*'];
22
31
  headers['Cross-Origin-Opener-Policy'] = ['same-origin'];
23
- headers['Cross-Origin-Embedder-Policy'] = ['require-corp'];
32
+ headers['Cross-Origin-Embedder-Policy'] = ['credentialless'];
24
33
  callback({ responseHeaders: headers });
25
34
  });
26
35
  }
27
36
  export function createMainWindow(opts) {
28
- configureSimulatorSession();
37
+ configureSimulatorSession(opts.simulatorPreloadPath);
29
38
  const mainWindow = new BrowserWindow({
30
39
  width: opts.width ?? 1280,
31
40
  height: opts.height ?? 980,
@@ -35,8 +44,13 @@ export function createMainWindow(opts) {
35
44
  show: false,
36
45
  backgroundColor: themeBg(),
37
46
  webPreferences: {
38
- nodeIntegration: true,
39
- contextIsolation: false,
47
+ nodeIntegration: false,
48
+ contextIsolation: true,
49
+ // `sandbox: false` is required so the preload bundle can `require('electron')`
50
+ // to access contextBridge/ipcRenderer. The renderer itself runs isolated.
51
+ sandbox: false,
52
+ preload: mainPreloadPath,
53
+ // The simulator <webview> still relies on the webview tag; keep it enabled.
40
54
  webviewTag: true,
41
55
  },
42
56
  });
@@ -51,17 +65,59 @@ export function createMainWindow(opts) {
51
65
  }
52
66
  }
53
67
  });
54
- mainWindow.webContents.on('will-attach-webview', (_event, webPreferences) => {
68
+ const rendererDir = path.dirname(opts.indexHtml);
69
+ // Restrict the main renderer to file:// URLs under the renderer bundle and
70
+ // route every popup through the OS browser. See navigation-hardening.ts.
71
+ applyNavigationHardening(mainWindow.webContents, rendererDir);
72
+ mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
73
+ // Pin the simulator <webview> onto `persist:simulator` so it shares storage
74
+ // with the rest of the session and picks up the session-registered preload.
75
+ // Empirically (Electron 41) `webPreferences.partition` is the field the
76
+ // guest session actually consults; `params.partition` alone is a no-op,
77
+ // so we write both belt-and-braces.
78
+ ;
79
+ webPreferences.partition = 'persist:simulator';
80
+ params.partition = 'persist:simulator';
81
+ // The simulator <webview> intentionally runs with `contextIsolation: false`
82
+ // because the dimina runtime and the user-authored preload scripts share
83
+ // the same JavaScript realm as the page — the mini-program SDK reaches
84
+ // into the page's globals (e.g. AudioContext, custom registers) and
85
+ // attaches helpers via window-level injection. Flipping isolation on
86
+ // would break that contract.
55
87
  webPreferences.contextIsolation = false;
88
+ void event;
89
+ });
90
+ // The simulator webview's webContents only exists once attached; wire its
91
+ // window-open / will-navigate handlers here so popups + redirects don't
92
+ // pivot to file:// or arbitrary origins.
93
+ mainWindow.webContents.on('did-attach-webview', (_event, webviewWebContents) => {
94
+ webviewWebContents.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
95
+ webviewWebContents.on('will-navigate', (e, url) => {
96
+ try {
97
+ const u = new URL(url);
98
+ // Allow the simulator's own host (localhost dev server) plus the
99
+ // initial about:blank that Chromium uses before src loads.
100
+ if (u.protocol === 'about:')
101
+ return;
102
+ if ((u.protocol === 'http:' || u.protocol === 'https:') &&
103
+ (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
104
+ return;
105
+ }
106
+ e.preventDefault();
107
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
108
+ void shell.openExternal(url);
109
+ }
110
+ }
111
+ catch {
112
+ e.preventDefault();
113
+ }
114
+ });
56
115
  });
57
116
  mainWindow.loadFile(opts.indexHtml);
58
117
  const container = new View();
59
118
  const mainWebView = mainWindow.contentView;
60
119
  container.addChildView(mainWebView);
61
120
  mainWindow.contentView = container;
62
- globalShortcut.register('CommandOrControl+Shift+I', () => {
63
- mainWindow.webContents.openDevTools({ mode: 'detach' });
64
- });
65
121
  return mainWindow;
66
122
  }
67
123
  //# sourceMappingURL=create.js.map
@@ -1,9 +1,10 @@
1
1
  import type { BrowserWindow } from 'electron';
2
2
  import type { WorkbenchContext } from '../../services/workbench-context.js';
3
+ import { type Disposable } from '../../utils/disposable.js';
3
4
  export interface MainWindowEventState {
4
5
  context?: WorkbenchContext;
5
6
  onResize?: () => void;
6
7
  onClose?: (event: Electron.Event) => void | Promise<void>;
7
8
  }
8
- export declare function wireMainWindowEvents(win: BrowserWindow, state?: MainWindowEventState): void;
9
+ export declare function wireMainWindowEvents(win: BrowserWindow, state?: MainWindowEventState): Disposable;
9
10
  //# sourceMappingURL=events.d.ts.map
@@ -1,4 +1,7 @@
1
+ import { globalShortcut } from 'electron';
2
+ import { DisposableRegistry, toDisposable } from '../../utils/disposable.js';
1
3
  export function wireMainWindowEvents(win, state = {}) {
4
+ const registry = new DisposableRegistry();
2
5
  const mainWebView = win.contentView.children[0];
3
6
  const resizeMainWebView = () => {
4
7
  if (mainWebView) {
@@ -9,10 +12,30 @@ export function wireMainWindowEvents(win, state = {}) {
9
12
  };
10
13
  resizeMainWebView();
11
14
  win.on('resize', resizeMainWebView);
15
+ registry.add(toDisposable(() => {
16
+ win.removeListener('resize', resizeMainWebView);
17
+ }));
12
18
  if (state.onClose) {
13
- win.on('close', (event) => {
19
+ const onCloseHandler = (event) => {
14
20
  void state.onClose?.(event);
15
- });
21
+ };
22
+ win.on('close', onCloseHandler);
23
+ registry.add(toDisposable(() => {
24
+ win.removeListener('close', onCloseHandler);
25
+ }));
16
26
  }
27
+ // DevTools shortcut: scoped to this main window so its disposal is tied
28
+ // to ctx.registry. `unregisterAll()` in lifecycle remains as a process-exit
29
+ // safety net.
30
+ const devToolsAccelerator = 'CommandOrControl+Shift+I';
31
+ const registered = globalShortcut.register(devToolsAccelerator, () => {
32
+ win.webContents.openDevTools({ mode: 'detach' });
33
+ });
34
+ if (registered) {
35
+ registry.add(toDisposable(() => {
36
+ globalShortcut.unregister(devToolsAccelerator);
37
+ }));
38
+ }
39
+ return registry;
17
40
  }
18
41
  //# sourceMappingURL=events.js.map
@@ -0,0 +1,24 @@
1
+ import type { WebContents } from 'electron';
2
+ /**
3
+ * Navigation hardening for every WebContents created by the host application
4
+ * (main window renderer, settings overlay, popover overlay, standalone
5
+ * settings window). These WebContents load the contextBridge-exposing main
6
+ * preload, so an unintended navigation out of the renderer bundle would
7
+ * expose `window.devtools.ipc` to a foreign origin.
8
+ *
9
+ * `applyNavigationHardening` installs:
10
+ * - `setWindowOpenHandler`: deny popups; route http(s) through the OS browser.
11
+ * - `will-navigate`: cancel any in-place navigation outside `rendererDir`;
12
+ * route http(s) through the OS browser.
13
+ */
14
+ /** Default popup handler: open http(s) externally, deny everything else. */
15
+ export declare function handleWindowOpenExternal(url: string): {
16
+ action: 'deny';
17
+ };
18
+ /**
19
+ * Install `setWindowOpenHandler` + `will-navigate` on `wc` so it can only
20
+ * navigate to `file://` URLs under `rendererDir`. Safe to call once per
21
+ * WebContents.
22
+ */
23
+ export declare function applyNavigationHardening(wc: WebContents, rendererDir: string): void;
24
+ //# sourceMappingURL=navigation-hardening.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { shell } from 'electron';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ /**
5
+ * Navigation hardening for every WebContents created by the host application
6
+ * (main window renderer, settings overlay, popover overlay, standalone
7
+ * settings window). These WebContents load the contextBridge-exposing main
8
+ * preload, so an unintended navigation out of the renderer bundle would
9
+ * expose `window.devtools.ipc` to a foreign origin.
10
+ *
11
+ * `applyNavigationHardening` installs:
12
+ * - `setWindowOpenHandler`: deny popups; route http(s) through the OS browser.
13
+ * - `will-navigate`: cancel any in-place navigation outside `rendererDir`;
14
+ * route http(s) through the OS browser.
15
+ */
16
+ /** Default popup handler: open http(s) externally, deny everything else. */
17
+ export function handleWindowOpenExternal(url) {
18
+ try {
19
+ const u = new URL(url);
20
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
21
+ void shell.openExternal(url);
22
+ }
23
+ }
24
+ catch {
25
+ /* malformed URL — fall through to deny */
26
+ }
27
+ return { action: 'deny' };
28
+ }
29
+ /**
30
+ * Install `setWindowOpenHandler` + `will-navigate` on `wc` so it can only
31
+ * navigate to `file://` URLs under `rendererDir`. Safe to call once per
32
+ * WebContents.
33
+ */
34
+ export function applyNavigationHardening(wc, rendererDir) {
35
+ const rendererPrefix = pathToFileURL(rendererDir + path.sep).href;
36
+ wc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
37
+ wc.on('will-navigate', (event, url) => {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ event.preventDefault();
44
+ return;
45
+ }
46
+ if (parsed.protocol === 'file:' && parsed.href.startsWith(rendererPrefix)) {
47
+ return;
48
+ }
49
+ event.preventDefault();
50
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
51
+ void shell.openExternal(url);
52
+ }
53
+ });
54
+ }
55
+ //# sourceMappingURL=navigation-hardening.js.map