@dimina-kit/devtools 0.3.2-dev.20260522110734 → 0.3.2-dev.20260522142649

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 (54) hide show
  1. package/README.md +90 -155
  2. package/dist/main/api.d.ts +3 -11
  3. package/dist/main/api.js +2 -13
  4. package/dist/main/app/app.d.ts +14 -1
  5. package/dist/main/app/app.js +93 -2
  6. package/dist/main/index.bundle.js +140 -52
  7. package/dist/main/ipc/app.d.ts +1 -1
  8. package/dist/main/ipc/app.js +3 -0
  9. package/dist/main/ipc/simulator.d.ts +1 -1
  10. package/dist/main/ipc/simulator.js +2 -3
  11. package/dist/main/ipc/toolbar.d.ts +1 -1
  12. package/dist/main/ipc/toolbar.js +14 -4
  13. package/dist/main/services/layout/index.d.ts +4 -10
  14. package/dist/main/services/layout/index.js +11 -19
  15. package/dist/main/services/module.d.ts +1 -2
  16. package/dist/main/services/simulator/custom-apis.d.ts +0 -1
  17. package/dist/main/services/simulator/custom-apis.js +0 -1
  18. package/dist/main/services/toolbar/toolbar-store.d.ts +23 -0
  19. package/dist/main/services/toolbar/toolbar-store.js +23 -0
  20. package/dist/main/services/views/view-manager.d.ts +6 -0
  21. package/dist/main/services/views/view-manager.js +6 -3
  22. package/dist/main/services/workbench-context.d.ts +40 -14
  23. package/dist/main/services/workbench-context.js +6 -1
  24. package/dist/main/utils/disposable.d.ts +5 -0
  25. package/dist/main/utils/disposable.js +7 -0
  26. package/dist/main/utils/ipc-registry.js +5 -1
  27. package/dist/main/utils/sender-policy.d.ts +4 -1
  28. package/dist/main/utils/sender-policy.js +6 -0
  29. package/dist/preload/runtime/custom-apis.js +72 -1
  30. package/dist/preload/windows/simulator.js +41 -1
  31. package/dist/renderer/assets/index-4lkyF644.js +46 -0
  32. package/dist/renderer/assets/{input-CoWcfa8u.js → input-D4byFOLS.js} +2 -2
  33. package/dist/renderer/assets/{ipc-transport-Cd2xPrsa.js → ipc-transport-BzPzxPwf.js} +2 -2
  34. package/dist/renderer/assets/{popover-Nq5gT9DO.js → popover-cxKrg_os.js} +2 -2
  35. package/dist/renderer/assets/{select-By5KH-UG.js → select-BgSkkdHW.js} +2 -2
  36. package/dist/renderer/assets/{settings-api-Do0_Smq1.js → settings-api-BXCw9vOZ.js} +2 -2
  37. package/dist/renderer/assets/{settings-Dj_N70qR.js → settings-oD2K1jdT.js} +2 -2
  38. package/dist/renderer/assets/{workbenchSettings-CazSLGYt.js → workbenchSettings-CTNBpTft.js} +2 -2
  39. package/dist/renderer/entries/main/index.html +4 -4
  40. package/dist/renderer/entries/popover/index.html +4 -4
  41. package/dist/renderer/entries/settings/index.html +4 -4
  42. package/dist/renderer/entries/workbench-settings/index.html +3 -3
  43. package/dist/shared/ipc-channels.d.ts +3 -2
  44. package/dist/shared/ipc-channels.js +4 -3
  45. package/dist/shared/ipc-schemas.d.ts +2 -0
  46. package/dist/shared/ipc-schemas.js +2 -0
  47. package/dist/shared/types.d.ts +55 -3
  48. package/dist/simulator/assets/simulator-CILbo0Ji.js +9 -0
  49. package/dist/simulator/simulator.html +1 -1
  50. package/package.json +3 -35
  51. package/dist/main/simulator-apis.d.ts +0 -23
  52. package/dist/main/simulator-apis.js +0 -24
  53. package/dist/renderer/assets/index-D8VFYBh-.js +0 -46
  54. package/dist/simulator/assets/simulator-CZc88Jeh.js +0 -9
@@ -1,11 +1,13 @@
1
1
  import type { BrowserWindow } from 'electron';
2
- import type { CompilationAdapter } from '../../shared/types.js';
2
+ import type { CompilationAdapter, WorkbenchConfig } from '../../shared/types.js';
3
3
  import { DisposableRegistry } from '../utils/disposable.js';
4
4
  import type { SenderPolicy } from '../utils/ipc-registry.js';
5
5
  import { type RendererNotifier } from './notifications/renderer-notifier.js';
6
6
  import { type ViewManager } from './views/view-manager.js';
7
7
  import { type WindowService } from './window-service.js';
8
8
  import { type WorkspaceService } from './workspace/workspace-service.js';
9
+ import { type SimulatorApiRegistry } from './simulator/custom-apis.js';
10
+ import { type ToolbarStore } from './toolbar/toolbar-store.js';
9
11
  import { sanitizeTemplates } from './projects/templates.js';
10
12
  import type { ProjectsProvider, ProjectTemplate } from './projects/types.js';
11
13
  import type { CustomCreateProjectDialogResult } from '../../shared/types.js';
@@ -25,14 +27,8 @@ export interface WorkbenchContext {
25
27
  apiNamespaces: string[];
26
28
  /** Branding name shown in title bar and getBranding IPC */
27
29
  appName: string;
28
- /** Host-injected provider for toolbar actions (overrides default empty list) */
29
- toolbarActions?: () => Promise<Array<{
30
- id: string;
31
- label: string;
32
- }>> | Array<{
33
- id: string;
34
- label: string;
35
- }>;
30
+ /** Header bar height in px, used for view layout and exposed to the renderer */
31
+ headerHeight: number;
36
32
  /** Host-injected provider for branding info (overrides default appName) */
37
33
  brandingProvider?: () => Promise<{
38
34
  appName: string;
@@ -77,18 +73,48 @@ export interface WorkbenchContext {
77
73
  * lazily so it stays correct as windows/views come and go.
78
74
  */
79
75
  senderPolicy: SenderPolicy;
76
+ /**
77
+ * Reference-counted map of `webContents.id` → live registration count for
78
+ * host-owned BrowserWindows registered as trusted senders via
79
+ * `instance.registerTrustedWindow`. A window stays trusted while its count
80
+ * is > 0; registering the same window N times requires N disposals (or a
81
+ * single `closed` event, which zeroes the count outright) to un-trust it.
82
+ * Consulted by `createWorkbenchSenderPolicy` in addition to the static
83
+ * main-window / overlay checks.
84
+ */
85
+ trustedWindowSenderIds: Map<number, number>;
86
+ /**
87
+ * Per-context registry of host-registered simulator custom APIs. Populated
88
+ * via `instance.registerSimulatorApi`; read by the simulator IPC handlers.
89
+ * One registry per context — no process-global crosstalk.
90
+ */
91
+ simulatorApis: SimulatorApiRegistry;
92
+ /**
93
+ * Per-context store of host-registered toolbar actions. Populated via
94
+ * `instance.toolbar.set`; read by the toolbar IPC handlers. One store per
95
+ * context — no process-global crosstalk.
96
+ */
97
+ toolbar: ToolbarStore;
80
98
  /** Aggregates dispose handlers for every IPC handler, listener, watcher, and CDP session registered by the workbench. */
81
99
  registry: DisposableRegistry;
82
100
  }
83
- export interface CreateContextOptions {
101
+ /**
102
+ * Inputs for `createWorkbenchContext`. The scalar config fields it shares with
103
+ * `WorkbenchConfig` (`adapter` / `apiNamespaces` / `appName` / `headerHeight`)
104
+ * are derived from there via `Pick` so the two stay in lockstep — no
105
+ * field-by-field re-declaration. The remaining fields are kept explicit
106
+ * because their shapes intentionally differ from the config:
107
+ * - `preloadPath` / `rendererDir` are REQUIRED here (the caller resolves the
108
+ * defaults before constructing the context) but optional in the config;
109
+ * - `panels` is the structural `string[]` (the context stores raw ids);
110
+ * - `projectsProvider` / `projectTemplates` / `customCreateProjectDialog` use
111
+ * the main-process types, not the structural mirrors in `shared/types`.
112
+ */
113
+ export interface CreateContextOptions extends Pick<WorkbenchConfig, 'adapter' | 'apiNamespaces' | 'appName' | 'headerHeight'> {
84
114
  mainWindow: BrowserWindow;
85
- adapter?: CompilationAdapter;
86
115
  preloadPath: string;
87
116
  rendererDir: string;
88
117
  panels?: string[];
89
- apiNamespaces?: string[];
90
- appName?: string;
91
- toolbarActions?: WorkbenchContext['toolbarActions'];
92
118
  brandingProvider?: WorkbenchContext['brandingProvider'];
93
119
  /** Host-supplied project list backend. Defaults to LocalProjectsProvider. */
94
120
  projectsProvider?: ProjectsProvider;
@@ -6,6 +6,8 @@ import { createViewManager } from './views/view-manager.js';
6
6
  import { createWindowService } from './window-service.js';
7
7
  import { createWorkspaceService, } from './workspace/workspace-service.js';
8
8
  import { createLocalProjectsProvider } from './projects/local-provider.js';
9
+ import { createSimulatorApiRegistry, } from './simulator/custom-apis.js';
10
+ import { createToolbarStore } from './toolbar/toolbar-store.js';
9
11
  import { resolveTemplates, sanitizeTemplates } from './projects/templates.js';
10
12
  import { BUILTIN_TEMPLATES } from './projects/builtin-templates.js';
11
13
  export function hasBuiltinPanel(ctx, panelId) {
@@ -26,10 +28,13 @@ export function createWorkbenchContext(opts) {
26
28
  panels: opts.panels ?? ['wxml', 'console', 'appdata', 'storage'],
27
29
  apiNamespaces: opts.apiNamespaces ?? [],
28
30
  appName: opts.appName ?? 'Dimina DevTools',
29
- toolbarActions: opts.toolbarActions,
31
+ headerHeight: opts.headerHeight ?? 40,
30
32
  brandingProvider: opts.brandingProvider,
31
33
  };
32
34
  ctx.registry = new DisposableRegistry();
35
+ ctx.trustedWindowSenderIds = new Map();
36
+ ctx.simulatorApis = createSimulatorApiRegistry();
37
+ ctx.toolbar = createToolbarStore();
33
38
  ctx.windows = createWindowService(opts.mainWindow);
34
39
  ctx.views = createViewManager(ctx);
35
40
  ctx.notify = createRendererNotifier(ctx);
@@ -6,6 +6,11 @@ export declare function toDisposable(fn: DisposeFn): Disposable;
6
6
  export declare class DisposableRegistry implements Disposable {
7
7
  private entries;
8
8
  private _disposed;
9
+ /**
10
+ * Number of live entries. A wrapper's `dispose` splices its entry out and
11
+ * `disposeAll` clears the array, so `entries.length` is the live count.
12
+ */
13
+ get size(): number;
9
14
  add(d: Disposable | DisposeFn): Disposable;
10
15
  disposeAll(): Promise<void>;
11
16
  dispose(): Promise<void>;
@@ -12,6 +12,13 @@ export function toDisposable(fn) {
12
12
  export class DisposableRegistry {
13
13
  entries = [];
14
14
  _disposed = false;
15
+ /**
16
+ * Number of live entries. A wrapper's `dispose` splices its entry out and
17
+ * `disposeAll` clears the array, so `entries.length` is the live count.
18
+ */
19
+ get size() {
20
+ return this.entries.length;
21
+ }
15
22
  add(d) {
16
23
  if (this._disposed) {
17
24
  throw new Error('cannot add to disposed registry');
@@ -55,8 +55,12 @@ export class IpcRegistry {
55
55
  }
56
56
  handle(channel, fn) {
57
57
  const policy = this.policy;
58
+ // The gate is `async` so a rejected sender surfaces as a *rejected
59
+ // promise* (the invoke-result contract) rather than a synchronous throw —
60
+ // synchronous throws can escape callers that wrap the result in
61
+ // `Promise.resolve(...)` instead of `await`-ing it directly.
58
62
  const guarded = policy
59
- ? (event, ...args) => {
63
+ ? async (event, ...args) => {
60
64
  const sender = event.sender;
61
65
  if (!policy(sender)) {
62
66
  console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
@@ -18,8 +18,11 @@ import type { SenderPolicy } from './ipc-registry.js';
18
18
  * Keeping the guest off this list contains the blast radius if the
19
19
  * simulator content is ever compromised.
20
20
  *
21
+ * Host-owned BrowserWindows registered via `instance.registerTrustedWindow`
22
+ * are additionally accepted via `ctx.trustedWindowSenderIds`.
23
+ *
21
24
  * Any other sender — including a stale/destroyed sender or an unknown
22
25
  * iframe — is rejected.
23
26
  */
24
- export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
27
+ export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views' | 'trustedWindowSenderIds'>): SenderPolicy;
25
28
  //# sourceMappingURL=sender-policy.d.ts.map
@@ -16,6 +16,9 @@
16
16
  * Keeping the guest off this list contains the blast radius if the
17
17
  * simulator content is ever compromised.
18
18
  *
19
+ * Host-owned BrowserWindows registered via `instance.registerTrustedWindow`
20
+ * are additionally accepted via `ctx.trustedWindowSenderIds`.
21
+ *
19
22
  * Any other sender — including a stale/destroyed sender or an unknown
20
23
  * iframe — is rejected.
21
24
  */
@@ -29,6 +32,9 @@ export function createWorkbenchSenderPolicy(ctx) {
29
32
  // Standalone settings BrowserWindow renderer
30
33
  if (ctx.windows.isSettingsWindowSender(sender.id))
31
34
  return true;
35
+ // Host-registered trusted windows (registerTrustedWindow)
36
+ if (ctx.trustedWindowSenderIds.has(sender.id))
37
+ return true;
32
38
  // Settings overlay view (mounted inside the main window)
33
39
  const settingsViewId = ctx.views.getSettingsWebContentsId();
34
40
  if (settingsViewId != null && sender.id === settingsViewId)
@@ -1,6 +1,23 @@
1
1
  import { ipcRenderer } from 'electron';
2
2
  import { SimulatorCustomApiBridgeChannel } from '../../shared/ipc-channels.js';
3
3
  import { exposeOnMainWorld } from '../shared/expose.js';
4
+ /**
5
+ * Re-send interval and ceiling for `list()` (see `listWithRetry`).
6
+ *
7
+ * The simulator boots fast enough that `__diminaCustomApis.list()` can fire
8
+ * its `sendToHost` request *before* the embedding main-window renderer has
9
+ * attached its `ipc-message` proxy listener (`useCustomApiProxy`). `sendToHost`
10
+ * is fire-and-forget — a request sent into that gap is lost, and the bridge
11
+ * call would otherwise hang until `custom-api-boot.ts`'s 3s deadlock-breaker.
12
+ *
13
+ * `list` is an idempotent read, so it is safe to re-send until a response
14
+ * arrives. The retry resolves within one embedder attach cycle (~200ms) in
15
+ * practice; the ceiling only bounds the leak if the proxy never attaches at
16
+ * all, and is kept below `custom-api-boot.ts`'s `CUSTOM_API_LIST_TIMEOUT_MS`
17
+ * so the retry exhausts before — not after — that outer timeout would fire.
18
+ */
19
+ const LIST_RETRY_INTERVAL_MS = 150;
20
+ const LIST_RETRY_CEILING_MS = 2500;
4
21
  // The simulator <webview> is intentionally kept off the workbench sender-policy
5
22
  // white-list, so it cannot reach `ipcMain.handle` directly. Instead the bridge
6
23
  // asks the trusted main-window renderer to proxy the call: webview sends via
@@ -31,8 +48,62 @@ function buildBridge() {
31
48
  ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
32
49
  });
33
50
  };
51
+ /**
52
+ * `list()` with re-send, to survive the embedder proxy attaching after the
53
+ * first request (see `LIST_RETRY_INTERVAL_MS`). Each attempt uses a fresh id
54
+ * and its own `pending` entry; whichever response lands first settles the
55
+ * call. Stale entries for the other attempts are dropped so a late duplicate
56
+ * response cannot resolve nothing — or, worse, leak.
57
+ */
58
+ const listWithRetry = () => {
59
+ return new Promise((resolve, reject) => {
60
+ const attemptIds = new Set();
61
+ // Boxed so `cleanup` (defined before the timers are created) can clear
62
+ // them; the box itself is `const`, only its fields are assigned later.
63
+ const state = { settled: false };
64
+ const cleanup = () => {
65
+ state.settled = true;
66
+ if (state.retryTimer)
67
+ clearInterval(state.retryTimer);
68
+ if (state.ceilingTimer)
69
+ clearTimeout(state.ceilingTimer);
70
+ for (const id of attemptIds)
71
+ pending.delete(id);
72
+ attemptIds.clear();
73
+ };
74
+ const attempt = () => {
75
+ if (state.settled)
76
+ return;
77
+ const id = nextId++;
78
+ attemptIds.add(id);
79
+ pending.set(id, {
80
+ resolve: (value) => {
81
+ if (state.settled)
82
+ return;
83
+ cleanup();
84
+ resolve(value);
85
+ },
86
+ reject: (reason) => {
87
+ if (state.settled)
88
+ return;
89
+ cleanup();
90
+ reject(reason);
91
+ },
92
+ });
93
+ ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, { id, op: 'list' });
94
+ };
95
+ state.retryTimer = setInterval(attempt, LIST_RETRY_INTERVAL_MS);
96
+ state.ceilingTimer = setTimeout(() => {
97
+ if (state.settled)
98
+ return;
99
+ cleanup();
100
+ reject(new Error('custom-apis bridge list() got no response from the host renderer'));
101
+ }, LIST_RETRY_CEILING_MS);
102
+ attempt();
103
+ });
104
+ };
34
105
  return {
35
- list: () => send({ id: nextId++, op: 'list' }),
106
+ list: listWithRetry,
36
107
  invoke: (name, params) => send({ id: nextId++, op: 'invoke', name, params }),
37
108
  };
38
109
  }
@@ -198,6 +198,8 @@ var MiniappSnapshotChannel = {
198
198
  };
199
199
 
200
200
  // src/preload/runtime/custom-apis.ts
201
+ var LIST_RETRY_INTERVAL_MS = 150;
202
+ var LIST_RETRY_CEILING_MS = 2500;
201
203
  function buildBridge() {
202
204
  let nextId = 1;
203
205
  const pending = /* @__PURE__ */ new Map();
@@ -220,8 +222,46 @@ function buildBridge() {
220
222
  import_electron2.ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, req);
221
223
  });
222
224
  };
225
+ const listWithRetry = () => {
226
+ return new Promise((resolve, reject) => {
227
+ const attemptIds = /* @__PURE__ */ new Set();
228
+ const state2 = { settled: false };
229
+ const cleanup = () => {
230
+ state2.settled = true;
231
+ if (state2.retryTimer) clearInterval(state2.retryTimer);
232
+ if (state2.ceilingTimer) clearTimeout(state2.ceilingTimer);
233
+ for (const id of attemptIds) pending.delete(id);
234
+ attemptIds.clear();
235
+ };
236
+ const attempt = () => {
237
+ if (state2.settled) return;
238
+ const id = nextId++;
239
+ attemptIds.add(id);
240
+ pending.set(id, {
241
+ resolve: (value) => {
242
+ if (state2.settled) return;
243
+ cleanup();
244
+ resolve(value);
245
+ },
246
+ reject: (reason) => {
247
+ if (state2.settled) return;
248
+ cleanup();
249
+ reject(reason);
250
+ }
251
+ });
252
+ import_electron2.ipcRenderer.sendToHost(SimulatorCustomApiBridgeChannel.Request, { id, op: "list" });
253
+ };
254
+ state2.retryTimer = setInterval(attempt, LIST_RETRY_INTERVAL_MS);
255
+ state2.ceilingTimer = setTimeout(() => {
256
+ if (state2.settled) return;
257
+ cleanup();
258
+ reject(new Error("custom-apis bridge list() got no response from the host renderer"));
259
+ }, LIST_RETRY_CEILING_MS);
260
+ attempt();
261
+ });
262
+ };
223
263
  return {
224
- list: () => send({ id: nextId++, op: "list" }),
264
+ list: listWithRetry,
225
265
  invoke: (name, params) => send({ id: nextId++, op: "invoke", name, params })
226
266
  };
227
267
  }