@dimina-kit/devtools 0.3.0 → 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.
Files changed (116) hide show
  1. package/README.md +103 -41
  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 +71 -36
  6. package/dist/main/app/launch.d.ts +2 -2
  7. package/dist/main/app/launch.js +6 -3
  8. package/dist/main/index.bundle.js +1134 -300
  9. package/dist/main/ipc/app.d.ts +2 -1
  10. package/dist/main/ipc/app.js +5 -4
  11. package/dist/main/ipc/index.d.ts +5 -4
  12. package/dist/main/ipc/index.js +5 -4
  13. package/dist/main/ipc/panels.d.ts +2 -1
  14. package/dist/main/ipc/panels.js +14 -9
  15. package/dist/main/ipc/popover.d.ts +4 -1
  16. package/dist/main/ipc/popover.js +15 -5
  17. package/dist/main/ipc/projects.d.ts +4 -1
  18. package/dist/main/ipc/projects.js +42 -5
  19. package/dist/main/ipc/session.d.ts +4 -1
  20. package/dist/main/ipc/session.js +26 -6
  21. package/dist/main/ipc/settings.d.ts +4 -1
  22. package/dist/main/ipc/settings.js +30 -17
  23. package/dist/main/ipc/simulator-module.d.ts +8 -0
  24. package/dist/main/ipc/simulator-module.js +19 -0
  25. package/dist/main/ipc/simulator.d.ts +2 -1
  26. package/dist/main/ipc/simulator.js +29 -15
  27. package/dist/main/ipc/toolbar.d.ts +2 -1
  28. package/dist/main/ipc/toolbar.js +3 -2
  29. package/dist/main/services/automation/index.js +88 -13
  30. package/dist/main/services/default-adapter.js +1 -3
  31. package/dist/main/services/mcp/server.d.ts +2 -1
  32. package/dist/main/services/mcp/server.js +5 -0
  33. package/dist/main/services/module.d.ts +18 -0
  34. package/dist/main/services/module.js +2 -0
  35. package/dist/main/services/notifications/renderer-notifier.d.ts +2 -0
  36. package/dist/main/services/projects/project-repository.d.ts +1 -0
  37. package/dist/main/services/projects/project-repository.js +3 -0
  38. package/dist/main/services/settings/index.d.ts +4 -0
  39. package/dist/main/services/settings/index.js +12 -1
  40. package/dist/main/services/simulator/custom-apis.d.ts +10 -0
  41. package/dist/main/services/simulator/custom-apis.js +26 -0
  42. package/dist/main/services/simulator/dir.d.ts +1 -1
  43. package/dist/main/services/simulator/dir.js +1 -4
  44. package/dist/main/services/simulator-storage/index.d.ts +22 -1
  45. package/dist/main/services/simulator-storage/index.js +390 -84
  46. package/dist/main/services/update/update-manager.d.ts +16 -1
  47. package/dist/main/services/update/update-manager.js +18 -15
  48. package/dist/main/services/views/view-manager.d.ts +4 -0
  49. package/dist/main/services/views/view-manager.js +33 -2
  50. package/dist/main/services/window-service.d.ts +24 -0
  51. package/dist/main/services/window-service.js +26 -0
  52. package/dist/main/services/workbench-context.d.ts +15 -0
  53. package/dist/main/services/workbench-context.js +6 -0
  54. package/dist/main/services/workspace/workspace-service.d.ts +1 -0
  55. package/dist/main/services/workspace/workspace-service.js +7 -3
  56. package/dist/main/simulator-apis.d.ts +23 -0
  57. package/dist/main/simulator-apis.js +24 -0
  58. package/dist/main/utils/disposable.d.ts +13 -0
  59. package/dist/main/utils/disposable.js +60 -0
  60. package/dist/main/utils/ipc-registry.d.ts +39 -0
  61. package/dist/main/utils/ipc-registry.js +109 -0
  62. package/dist/main/utils/ipc-schema.d.ts +28 -0
  63. package/dist/main/utils/ipc-schema.js +38 -0
  64. package/dist/main/utils/paths.d.ts +8 -0
  65. package/dist/main/utils/paths.js +33 -4
  66. package/dist/main/utils/sender-policy.d.ts +23 -0
  67. package/dist/main/utils/sender-policy.js +42 -0
  68. package/dist/main/windows/main-window/create.d.ts +6 -0
  69. package/dist/main/windows/main-window/create.js +65 -9
  70. package/dist/main/windows/main-window/events.d.ts +2 -1
  71. package/dist/main/windows/main-window/events.js +25 -2
  72. package/dist/main/windows/navigation-hardening.d.ts +24 -0
  73. package/dist/main/windows/navigation-hardening.js +55 -0
  74. package/dist/main/windows/settings-window/create.js +11 -1
  75. package/dist/preload/index.d.ts +17 -0
  76. package/dist/preload/index.js +17 -0
  77. package/dist/preload/instrumentation/app-data.js +178 -19
  78. package/dist/preload/instrumentation/wxml-extract.js +134 -3
  79. package/dist/preload/runtime/bridge.d.ts +3 -1
  80. package/dist/preload/runtime/bridge.js +55 -4
  81. package/dist/preload/runtime/custom-apis.d.ts +6 -0
  82. package/dist/preload/runtime/custom-apis.js +18 -0
  83. package/dist/preload/runtime/host.d.ts +1 -0
  84. package/dist/preload/runtime/host.js +13 -0
  85. package/dist/preload/shared/types.d.ts +2 -1
  86. package/dist/preload/windows/main.cjs +47 -0
  87. package/dist/preload/windows/main.cjs.map +7 -0
  88. package/dist/preload/windows/simulator.js +274 -25
  89. package/dist/renderer/assets/index-DW_0thr-.js +46 -0
  90. package/dist/renderer/assets/{input-VLotwer2.js → input-C5M5Wxn6.js} +2 -2
  91. package/dist/renderer/assets/{ipc-transport-COKIC6Ny.js → ipc-transport-CUsl-fS2.js} +6 -6
  92. package/dist/renderer/assets/ipc-transport-CltRuvhi.css +1 -0
  93. package/dist/renderer/assets/{popover-Dn0RwMLI.js → popover-BSBPVO0o.js} +2 -2
  94. package/dist/renderer/assets/{select-DzIsLaKX.js → select-B1tF3UAc.js} +2 -2
  95. package/dist/renderer/assets/{settings-CD-dacWB.js → settings-7w54H6p-.js} +2 -2
  96. package/dist/renderer/assets/settings-api-BjJalghB.js +2 -0
  97. package/dist/renderer/assets/workbenchSettings-q2_-nc8t.js +8 -0
  98. package/dist/renderer/entries/main/index.html +12 -5
  99. package/dist/renderer/entries/popover/index.html +7 -5
  100. package/dist/renderer/entries/settings/index.html +7 -5
  101. package/dist/renderer/entries/workbench-settings/index.html +6 -4
  102. package/dist/shared/ipc-channels.d.ts +45 -0
  103. package/dist/shared/ipc-channels.js +28 -0
  104. package/dist/shared/ipc-schemas.d.ts +113 -0
  105. package/dist/shared/ipc-schemas.js +128 -0
  106. package/dist/shared/types.d.ts +2 -0
  107. package/dist/simulator/assets/simulator-DL6kOCq-.js +9 -0
  108. package/dist/simulator/simulator.html +1 -1
  109. package/package.json +11 -27
  110. package/dist/preload/windows/main.d.ts +0 -6
  111. package/dist/preload/windows/main.js +0 -6
  112. package/dist/renderer/assets/index-D_HFRqZA.js +0 -46
  113. package/dist/renderer/assets/ipc-transport-6Gy3FXIL.css +0 -1
  114. package/dist/renderer/assets/settings-api-BV9MPckG.js +0 -2
  115. package/dist/renderer/assets/workbenchSettings-ClwgsvJL.js +0 -8
  116. package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
@@ -0,0 +1,42 @@
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: its preload
12
+ * (`src/preload/windows/simulator.ts`) uses `ipcRenderer.sendToHost`
13
+ * exclusively, never `invoke`/`send`, so it can't legitimately reach an
14
+ * ipcMain handler. Including it would only widen the attack surface if
15
+ * the guest were ever compromised.
16
+ *
17
+ * Any other sender — including a stale/destroyed sender or an unknown
18
+ * iframe — is rejected.
19
+ */
20
+ export function createWorkbenchSenderPolicy(ctx) {
21
+ return (sender) => {
22
+ if (sender.isDestroyed())
23
+ return false;
24
+ // Main window renderer
25
+ if (ctx.windows.isMainSender(sender.id))
26
+ return true;
27
+ // Standalone settings BrowserWindow renderer
28
+ if (ctx.windows.isSettingsWindowSender(sender.id))
29
+ return true;
30
+ // Settings overlay view (mounted inside the main window)
31
+ const settingsViewId = ctx.views.getSettingsWebContentsId();
32
+ if (settingsViewId != null && sender.id === settingsViewId)
33
+ return true;
34
+ // Popover overlay view
35
+ const popoverViewId = ctx.views.getPopoverWebContentsId();
36
+ if (popoverViewId != null && sender.id === popoverViewId)
37
+ return true;
38
+ // simulator preload uses sendToHost only; never reaches ipcMain handlers.
39
+ return false;
40
+ };
41
+ }
42
+ //# sourceMappingURL=sender-policy.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) {
@@ -25,7 +34,7 @@ function configureSimulatorSession() {
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
@@ -1,6 +1,8 @@
1
1
  import { BrowserWindow } from 'electron';
2
2
  import path from 'path';
3
+ import { mainPreloadPath } from '../../utils/paths.js';
3
4
  import { themeBg } from '../../utils/theme.js';
5
+ import { applyNavigationHardening } from '../navigation-hardening.js';
4
6
  export async function createSettingsWindow(parent, rendererDir) {
5
7
  const win = new BrowserWindow({
6
8
  width: 420,
@@ -10,8 +12,16 @@ export async function createSettingsWindow(parent, rendererDir) {
10
12
  parent: parent ?? undefined,
11
13
  title: '开发工具设置',
12
14
  backgroundColor: themeBg(),
13
- webPreferences: { nodeIntegration: true, contextIsolation: false },
15
+ webPreferences: {
16
+ nodeIntegration: false,
17
+ contextIsolation: true,
18
+ sandbox: false,
19
+ preload: mainPreloadPath,
20
+ },
14
21
  });
22
+ // Standalone settings window loads mainPreloadPath, so the same
23
+ // navigation rules as the main window apply — see navigation-hardening.ts.
24
+ applyNavigationHardening(win.webContents, rendererDir);
15
25
  await win.loadFile(path.join(rendererDir, 'entries/workbench-settings/index.html'));
16
26
  return win;
17
27
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Public preload API surface for `@dimina-kit/devtools/preload`.
3
+ *
4
+ * External consumers compose their own simulator <webview> preload by
5
+ * importing the `install*` helpers from here. The built-in simulator
6
+ * preload at `windows/simulator.ts` is one such composition.
7
+ *
8
+ * NOTE: do NOT add main-window preload concerns (contextBridge / ipc
9
+ * exposure) to this file — that lives in `windows/main.ts` and is
10
+ * bundled by esbuild, never re-exported through this barrel.
11
+ */
12
+ export { installConsoleInstrumentation } from './instrumentation/console.js';
13
+ export { installAppDataInstrumentation, sendAllAppData } from './instrumentation/app-data.js';
14
+ export { installWxmlInstrumentation, sendWxmlTree, setupWxmlObserver } from './instrumentation/wxml.js';
15
+ export { installSimulatorBridge } from './runtime/bridge.js';
16
+ export { setupApiCompatHook } from './shared/api-compat.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Public preload API surface for `@dimina-kit/devtools/preload`.
3
+ *
4
+ * External consumers compose their own simulator <webview> preload by
5
+ * importing the `install*` helpers from here. The built-in simulator
6
+ * preload at `windows/simulator.ts` is one such composition.
7
+ *
8
+ * NOTE: do NOT add main-window preload concerns (contextBridge / ipc
9
+ * exposure) to this file — that lives in `windows/main.ts` and is
10
+ * bundled by esbuild, never re-exported through this barrel.
11
+ */
12
+ export { installConsoleInstrumentation } from './instrumentation/console.js';
13
+ export { installAppDataInstrumentation, sendAllAppData } from './instrumentation/app-data.js';
14
+ export { installWxmlInstrumentation, sendWxmlTree, setupWxmlObserver } from './instrumentation/wxml.js';
15
+ export { installSimulatorBridge } from './runtime/bridge.js';
16
+ export { setupApiCompatHook } from './shared/api-compat.js';
17
+ //# sourceMappingURL=index.js.map
@@ -1,15 +1,69 @@
1
1
  import { SimulatorChannel, BridgeChannel } from '../../shared/ipc-channels.js';
2
- import { sendToHost } from '../runtime/host.js';
3
- import { simulatorBridge, clearAppDataSnapshot, setAppDataSnapshot, } from '../runtime/bridge.js';
2
+ import { onHostMessage, sendToHost } from '../runtime/host.js';
3
+ import { clearAppDataSnapshot, setAppDataSnapshot, } from '../runtime/bridge.js';
4
4
  import { createDisposableSet } from './disposable.js';
5
5
  const appDataCache = new Map();
6
+ // Bridges in insertion order — drives the `bridges` array in snapshots so
7
+ // renderer tabs render in a stable, chronological sequence.
8
+ const bridgeOrder = [];
9
+ // Page path per bridge: set from `page_*` init's body.path. Component init
10
+ // also carries a path but it's the component's own path, not the page route.
11
+ const bridgePagePath = new Map();
12
+ function recordBridge(bridgeId) {
13
+ if (!bridgeOrder.includes(bridgeId))
14
+ bridgeOrder.push(bridgeId);
15
+ }
16
+ function buildSnapshot() {
17
+ const bridges = [];
18
+ for (const id of bridgeOrder) {
19
+ bridges.push({ id, pagePath: bridgePagePath.get(id) ?? null });
20
+ }
21
+ const entries = {};
22
+ for (const [key, entry] of appDataCache) {
23
+ const slash = key.indexOf('/');
24
+ if (slash < 0)
25
+ continue;
26
+ const bridgeId = key.slice(0, slash);
27
+ const moduleId = key.slice(slash + 1);
28
+ if (!entries[bridgeId])
29
+ entries[bridgeId] = {};
30
+ const displayKey = entry.componentPath ?? moduleId;
31
+ entries[bridgeId][displayKey] = entry.data;
32
+ }
33
+ return { bridges, entries };
34
+ }
6
35
  function publishSnapshot() {
7
36
  const data = {};
8
- for (const [key, value] of appDataCache) {
9
- data[key] = value;
37
+ for (const [key, entry] of appDataCache) {
38
+ data[key] = entry.data;
10
39
  }
11
40
  setAppDataSnapshot(data);
12
41
  }
42
+ function clearBridge(bridgeId) {
43
+ const prefix = `${bridgeId}/`;
44
+ for (const key of [...appDataCache.keys()]) {
45
+ if (key.startsWith(prefix))
46
+ appDataCache.delete(key);
47
+ }
48
+ const idx = bridgeOrder.indexOf(bridgeId);
49
+ if (idx >= 0)
50
+ bridgeOrder.splice(idx, 1);
51
+ bridgePagePath.delete(bridgeId);
52
+ publishSnapshot();
53
+ // Push a refreshed snapshot so the renderer can drop the evicted bridge
54
+ // immediately, without waiting for the user to hit the refresh button.
55
+ sendToHost(SimulatorChannel.AppDataAll, buildSnapshot());
56
+ }
57
+ // dimina runtime emits two kinds of service→main messages relevant to the
58
+ // AppData panel:
59
+ // • update batches `{type:'ub', body:{bridgeId, updates:[{moduleId,data}]}}`
60
+ // where each `data` is a partial setData patch.
61
+ // • instance init: type === instance id (prefixed `page_`/`component_`),
62
+ // body `{bridgeId, path, data}` with the COMPLETE initial state.
63
+ //
64
+ // Policy: surface PAGE entries only. Component entries are dropped — they
65
+ // sometimes flow on a bridge id distinct from their owning page's and never
66
+ // receive pageUnload, which would manifest as ghost tabs in the panel.
13
67
  function decodeWorkerMessage(message) {
14
68
  let payload = message;
15
69
  if (typeof payload === 'string') {
@@ -20,10 +74,73 @@ function decodeWorkerMessage(message) {
20
74
  return null;
21
75
  }
22
76
  }
77
+ if (!payload || typeof payload !== 'object')
78
+ return null;
23
79
  const record = payload;
24
- if (record?.type !== 'u' || !record.body)
80
+ if (record.type === 'ub') {
81
+ const body = record.body;
82
+ if (!body || typeof body !== 'object')
83
+ return null;
84
+ if (typeof body.bridgeId !== 'string' || !Array.isArray(body.updates))
85
+ return null;
86
+ const out = [];
87
+ for (const u of body.updates) {
88
+ if (!u || typeof u.moduleId !== 'string')
89
+ continue;
90
+ if (!u.moduleId.startsWith('page_'))
91
+ continue;
92
+ out.push({ mode: 'patch', bridgeId: body.bridgeId, moduleId: u.moduleId, data: u.data });
93
+ }
94
+ return out.length > 0 ? out : null;
95
+ }
96
+ if (typeof record.type === 'string' && record.type.startsWith('page_')) {
97
+ const body = record.body;
98
+ if (!body || typeof body !== 'object')
99
+ return null;
100
+ if (typeof body.bridgeId !== 'string' || typeof body.path !== 'string')
101
+ return null;
102
+ if (!body.data || typeof body.data !== 'object')
103
+ return null;
104
+ return [{
105
+ mode: 'init',
106
+ bridgeId: body.bridgeId,
107
+ moduleId: record.type,
108
+ componentPath: body.path,
109
+ data: body.data,
110
+ }];
111
+ }
112
+ return null;
113
+ }
114
+ // main→worker direction: container signals page teardown so cache can evict.
115
+ function decodeOutgoingMessage(message) {
116
+ let payload = message;
117
+ if (typeof payload === 'string') {
118
+ try {
119
+ payload = JSON.parse(payload);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ if (!payload || typeof payload !== 'object')
126
+ return null;
127
+ const r = payload;
128
+ if (typeof r.type !== 'string')
25
129
  return null;
26
- return record.body;
130
+ return {
131
+ type: r.type,
132
+ bridgeId: typeof r.body?.bridgeId === 'string' ? r.body.bridgeId : undefined,
133
+ };
134
+ }
135
+ function instrumentPostMessage(worker) {
136
+ const orig = worker.postMessage.bind(worker);
137
+ worker.postMessage = function (msg, ...rest) {
138
+ const decoded = decodeOutgoingMessage(msg);
139
+ if (decoded?.type === 'pageUnload' && decoded.bridgeId) {
140
+ clearBridge(decoded.bridgeId);
141
+ }
142
+ return orig(msg, ...rest);
143
+ };
27
144
  }
28
145
  function createInstrumentedWorker(OriginalWorker) {
29
146
  function InstrumentedWorker(scriptURL, options) {
@@ -31,15 +148,22 @@ function createInstrumentedWorker(OriginalWorker) {
31
148
  ? scriptURL
32
149
  : new URL(scriptURL, window.location.href);
33
150
  const worker = Reflect.construct(OriginalWorker, [resolvedScriptURL, options], new.target ?? InstrumentedWorker);
151
+ instrumentPostMessage(worker);
34
152
  worker.addEventListener('message', (event) => {
35
- const body = decodeWorkerMessage(event.data);
36
- if (!body)
153
+ const entries = decodeWorkerMessage(event.data);
154
+ if (!entries)
37
155
  return;
38
- window.__simulatorHook?.appData({
39
- bridgeId: body.bridgeId,
40
- moduleId: body.moduleId,
41
- data: body.data,
42
- });
156
+ for (const entry of entries) {
157
+ const hookBody = {
158
+ bridgeId: entry.bridgeId,
159
+ moduleId: entry.moduleId,
160
+ data: entry.data,
161
+ mode: entry.mode,
162
+ };
163
+ if (entry.mode === 'init')
164
+ hookBody.componentPath = entry.componentPath;
165
+ window.__simulatorHook?.appData(hookBody);
166
+ }
43
167
  });
44
168
  return worker;
45
169
  }
@@ -57,12 +181,44 @@ export function installAppDataInstrumentation() {
57
181
  window.__simulatorHook = {
58
182
  appData: (body) => {
59
183
  const record = body;
60
- if (record?.bridgeId && record?.moduleId) {
61
- const key = `${record.bridgeId}/${record.moduleId}`;
62
- appDataCache.set(key, record.data);
63
- publishSnapshot();
184
+ if (!record?.bridgeId || !record?.moduleId)
185
+ return;
186
+ // R6: drop ub patches whose bridge has never been initialised. dimina
187
+ // dispatches pageUnload to the service worker, which fires onUnload —
188
+ // and onUnload's setData (e.g. console-test's stopTimer) produces a
189
+ // late `ub` that arrives AFTER our clearBridge. Without this gate the
190
+ // late patch would resurrect the unloaded bridge as a ghost tab.
191
+ if (record.mode !== 'init' && !bridgePagePath.has(record.bridgeId))
192
+ return;
193
+ const key = `${record.bridgeId}/${record.moduleId}`;
194
+ const prev = appDataCache.get(key);
195
+ const incoming = (record.data && typeof record.data === 'object'
196
+ ? record.data
197
+ : {});
198
+ // init = full-state replace; patch = merge into previous (setData semantics)
199
+ const merged = record.mode === 'init'
200
+ ? { ...incoming }
201
+ : { ...(prev?.data ?? {}), ...incoming };
202
+ const componentPath = record.componentPath ?? prev?.componentPath;
203
+ const next = componentPath !== undefined
204
+ ? { componentPath, data: merged }
205
+ : { data: merged };
206
+ appDataCache.set(key, next);
207
+ recordBridge(record.bridgeId);
208
+ if (record.mode === 'init'
209
+ && record.moduleId.startsWith('page_')
210
+ && record.componentPath) {
211
+ bridgePagePath.set(record.bridgeId, record.componentPath);
64
212
  }
65
- sendToHost(SimulatorChannel.AppData, body);
213
+ publishSnapshot();
214
+ const payload = {
215
+ bridgeId: record.bridgeId,
216
+ moduleId: record.moduleId,
217
+ data: merged,
218
+ };
219
+ if (componentPath !== undefined)
220
+ payload.componentPath = componentPath;
221
+ sendToHost(SimulatorChannel.AppData, payload);
66
222
  },
67
223
  };
68
224
  Object.defineProperty(window, 'Worker', {
@@ -70,6 +226,7 @@ export function installAppDataInstrumentation() {
70
226
  writable: true,
71
227
  value: createInstrumentedWorker(OriginalWorker),
72
228
  });
229
+ disposables.add(onHostMessage(BridgeChannel.AppDataGetAllRequest, () => sendAllAppData()));
73
230
  publishSnapshot();
74
231
  disposables.add(() => {
75
232
  if (originalDescriptor) {
@@ -89,6 +246,8 @@ export function installAppDataInstrumentation() {
89
246
  });
90
247
  disposables.add(() => {
91
248
  appDataCache.clear();
249
+ bridgeOrder.length = 0;
250
+ bridgePagePath.clear();
92
251
  clearAppDataSnapshot();
93
252
  publishSnapshot();
94
253
  });
@@ -98,6 +257,6 @@ export function installAppDataInstrumentation() {
98
257
  }
99
258
  export function sendAllAppData() {
100
259
  publishSnapshot();
101
- sendToHost(BridgeChannel.AppDataGetAllResponse, simulatorBridge.appdata.data);
260
+ sendToHost(SimulatorChannel.AppDataAll, buildSnapshot());
102
261
  }
103
262
  //# sourceMappingURL=app-data.js.map