@dimina-kit/devtools 0.4.0-dev.20260711140249 → 0.4.0-dev.20260716153350

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 (59) hide show
  1. package/dist/main/app/app.js +39 -10
  2. package/dist/main/app/native-overview.d.ts +1 -1
  3. package/dist/main/index.bundle.js +189 -172
  4. package/dist/main/services/simulator-appdata/index.js +1 -1
  5. package/dist/main/services/views/native-simulator-view.d.ts +6 -0
  6. package/dist/main/services/views/native-simulator-view.js +16 -0
  7. package/dist/main/services/views/refresh-styles.d.ts +10 -0
  8. package/dist/main/services/views/refresh-styles.js +57 -0
  9. package/dist/main/services/views/view-manager.d.ts +26 -1
  10. package/dist/main/services/views/view-manager.js +7 -0
  11. package/dist/main/services/views/workbench-view.d.ts +2 -0
  12. package/dist/main/services/views/workbench-view.js +87 -7
  13. package/dist/main/services/workspace/open-project-guards.d.ts +16 -0
  14. package/dist/main/services/workspace/open-project-guards.js +42 -0
  15. package/dist/main/services/workspace/rebuild-status.d.ts +24 -0
  16. package/dist/main/services/workspace/rebuild-status.js +20 -0
  17. package/dist/main/services/workspace/workspace-lifecycle-race.testutil.d.ts +2 -0
  18. package/dist/main/services/workspace/workspace-lifecycle-race.testutil.js +4 -0
  19. package/dist/main/services/workspace/workspace-service.js +21 -36
  20. package/dist/native-host/common/common.js +1 -1
  21. package/dist/native-host/render/render.js +49 -34
  22. package/dist/native-host/service/service.js +2 -2
  23. package/dist/preload/instrumentation/app-data.d.ts +2 -2
  24. package/dist/preload/instrumentation/app-data.js +1 -1
  25. package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
  26. package/dist/preload/windows/main.cjs +96 -0
  27. package/dist/preload/windows/main.cjs.map +2 -2
  28. package/dist/preload/windows/simulator.cjs +162 -140
  29. package/dist/preload/windows/simulator.cjs.map +3 -3
  30. package/dist/preload/windows/simulator.js +162 -140
  31. package/dist/render-host/render-inspect.js +15 -47
  32. package/dist/renderer/assets/index-CiIkMrdt.js +49 -0
  33. package/dist/renderer/assets/{input-COtRON-n.js → input-7P0WbXyg.js} +2 -2
  34. package/dist/renderer/assets/ipc-transport-BGtDnv6p.css +1 -0
  35. package/dist/renderer/assets/{ipc-transport-SuI2rOB_.js → ipc-transport-DTEmLpxx.js} +2 -2
  36. package/dist/renderer/assets/popover-DsOcyIS6.js +2 -0
  37. package/dist/renderer/assets/{select-Bwb8O0Mf.js → select-Bzbhbg9Z.js} +2 -2
  38. package/dist/renderer/assets/{settings-BispnvJU.js → settings-CR1TyBWu.js} +2 -2
  39. package/dist/renderer/assets/{settings-api-HIBz8BFz.js → settings-api-B1wVFAPB.js} +2 -2
  40. package/dist/renderer/assets/workbenchSettings-CFbToT4Q.js +8 -0
  41. package/dist/renderer/entries/main/index.html +6 -6
  42. package/dist/renderer/entries/popover/index.html +5 -5
  43. package/dist/renderer/entries/settings/index.html +5 -5
  44. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  45. package/dist/shared/ipc-channels.d.ts +1 -0
  46. package/dist/shared/ipc-channels.js +7 -0
  47. package/dist/simulator/assets/device-shell-C-HZY3bL.css +1 -0
  48. package/dist/simulator/assets/device-shell-Cu1ZPLfe.js +2 -0
  49. package/dist/simulator/assets/{simulator-A1vGZVBM.js → simulator-Cy8c-eA2.js} +5 -5
  50. package/dist/simulator/simulator.html +1 -1
  51. package/package.json +7 -7
  52. package/dist/renderer/assets/index-WbvXdhWQ.js +0 -49
  53. package/dist/renderer/assets/ipc-transport-CNvORIEX.css +0 -1
  54. package/dist/renderer/assets/popover-B73T3z-m.js +0 -2
  55. package/dist/renderer/assets/workbenchSettings-CtRBr7gS.js +0 -8
  56. package/dist/shared/appdata-accumulator.d.ts +0 -95
  57. package/dist/shared/appdata-accumulator.js +0 -218
  58. package/dist/simulator/assets/device-shell-C-wcFq3Z.js +0 -2
  59. package/dist/simulator/assets/device-shell-CS1rqAQz.css +0 -1
@@ -1,5 +1,5 @@
1
1
  import { SimulatorAppDataChannel } from '../../../shared/ipc-channels.js';
2
- import { AppDataAccumulator, decodeWorkerMessage, decodedToInput, } from '../../../shared/appdata-accumulator.js';
2
+ import { AppDataAccumulator, decodeWorkerMessage, decodedToInput, } from '@dimina-kit/inspect';
3
3
  import { DisposableRegistry } from '@dimina-kit/electron-deck/main';
4
4
  import { IpcRegistry } from '../../utils/ipc-registry.js';
5
5
  const EMPTY_SNAPSHOT = { bridges: [], entries: {} };
@@ -15,6 +15,12 @@ import type { ViewManagerContext } from './view-manager.js';
15
15
  export interface NativeSimulatorView {
16
16
  attachNativeSimulator(simulatorUrl: string, simWidth: number): Promise<void>;
17
17
  softReloadNativeSimulator(simulatorUrl: string): boolean;
18
+ /**
19
+ * Hot-swap stylesheets in every live render-host guest WITHOUT respawning the
20
+ * shell (page stack / form state / focus survive). Returns false when no live
21
+ * guest exists, so the caller falls back to a full reload.
22
+ */
23
+ refreshSimulatorStyles(): boolean;
18
24
  detachSimulator(): void;
19
25
  getSimulatorWebContentsId(): number | null;
20
26
  getSimulatorWebContents(): WebContents | null;
@@ -6,6 +6,7 @@ import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js
6
6
  import * as layout from '../layout/index.js';
7
7
  import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
8
8
  import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
9
+ import { refreshGuestStylesheets } from './refresh-styles.js';
9
10
  import { parseRoute } from '../../../shared/simulator-route.js';
10
11
  import { SIMULATOR_EVENTS } from '../../../shared/bridge-channels.js';
11
12
  import { VIEW_ID } from '../../../shared/view-ids.js';
@@ -377,6 +378,20 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
377
378
  nativeSimulatorView.webContents.send(SIMULATOR_EVENTS.RELAUNCH, payload);
378
379
  return true;
379
380
  }
381
+ /**
382
+ * Style-only hot swap: cache-bust every render-host stylesheet in place so a
383
+ * recompiled `.css` re-applies against the already-mounted page WITHOUT
384
+ * respawning the DeviceShell — page stack / form state / scroll / focus all
385
+ * survive (unlike `softReloadNativeSimulator`, which reboots the app session).
386
+ * Returns false when the shell isn't ready or no guest is live, so the caller
387
+ * falls back to a full reload rather than silently swallowing the rebuild.
388
+ */
389
+ function refreshSimulatorStyles() {
390
+ const view = nativeSimulatorView;
391
+ if (!view || view.webContents.isDestroyed() || !nativeSimulatorShellReady)
392
+ return false;
393
+ return refreshGuestStylesheets(view);
394
+ }
380
395
  function detachSimulator() {
381
396
  settleNativeSimulatorReady?.();
382
397
  settleNativeSimulatorReady = null;
@@ -414,6 +429,7 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
414
429
  return {
415
430
  attachNativeSimulator,
416
431
  softReloadNativeSimulator,
432
+ refreshSimulatorStyles,
417
433
  detachSimulator,
418
434
  getSimulatorWebContentsId: () => simulatorWebContentsId,
419
435
  getSimulatorWebContents: () => {
@@ -0,0 +1,10 @@
1
+ import { type WebContentsView } from 'electron';
2
+ /**
3
+ * Hot-swap the stylesheets of every live render-host guest of `view` in place
4
+ * (no shell respawn — page stack / form state / scroll / focus survive). Guests
5
+ * are found via `hostWebContents === view.webContents` — the same nesting
6
+ * pattern the zoom propagation uses. Returns false when no live guest is found,
7
+ * so the caller can fall back to a full reload rather than dropping the edit.
8
+ */
9
+ export declare function refreshGuestStylesheets(view: WebContentsView): boolean;
10
+ //# sourceMappingURL=refresh-styles.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { webContents } from 'electron';
2
+ /**
3
+ * Injected into each live render-host guest to hot-swap its stylesheets in
4
+ * place: for every `<link rel=stylesheet>`, insert a cache-busted clone
5
+ * (`?__hmr=<ts>`) so the browser re-fetches the recompiled `.css` and re-applies
6
+ * it against the already-mounted DOM, then drop the stale sheet once the fresh
7
+ * one loads (no unstyled flash). Recurses into same-origin iframes because a
8
+ * render-host page nests its content in an iframe. Self-contained (runs in the
9
+ * guest realm via `executeJavaScript`), defensive (never throws). Mirrors
10
+ * `@dimina-kit/devkit`'s `refreshStylesheets` (the SSE web-preview equivalent).
11
+ */
12
+ const REFRESH_STYLES_JS = `(function refresh(doc){
13
+ try {
14
+ var links = doc.querySelectorAll('link[rel="stylesheet"]');
15
+ for (var i = 0; i < links.length; i++) {
16
+ var link = links[i];
17
+ try {
18
+ var u = new URL(link.href, doc.baseURI || undefined);
19
+ u.searchParams.set('__hmr', String(Date.now()));
20
+ var next = link.cloneNode();
21
+ next.href = u.href;
22
+ next.addEventListener('load', (function(stale){ return function(){ stale.remove(); }; })(link));
23
+ link.parentNode.insertBefore(next, link.nextSibling);
24
+ } catch (e) {}
25
+ }
26
+ var frames = doc.querySelectorAll('iframe');
27
+ for (var j = 0; j < frames.length; j++) {
28
+ try { if (frames[j].contentDocument) refresh(frames[j].contentDocument); } catch (e) {}
29
+ }
30
+ } catch (e) {}
31
+ })(document);`;
32
+ /**
33
+ * Hot-swap the stylesheets of every live render-host guest of `view` in place
34
+ * (no shell respawn — page stack / form state / scroll / focus survive). Guests
35
+ * are found via `hostWebContents === view.webContents` — the same nesting
36
+ * pattern the zoom propagation uses. Returns false when no live guest is found,
37
+ * so the caller can fall back to a full reload rather than dropping the edit.
38
+ */
39
+ export function refreshGuestStylesheets(view) {
40
+ const simWc = view.webContents;
41
+ let refreshed = 0;
42
+ try {
43
+ for (const wc of webContents.getAllWebContents()) {
44
+ if (wc.isDestroyed())
45
+ continue;
46
+ if (wc.hostWebContents !== simWc)
47
+ continue;
48
+ wc.executeJavaScript(REFRESH_STYLES_JS).catch(() => { });
49
+ refreshed++;
50
+ }
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ return refreshed > 0;
56
+ }
57
+ //# sourceMappingURL=refresh-styles.js.map
@@ -104,6 +104,13 @@ export interface ViewManager {
104
104
  * back to the hard attachNativeSimulator rebuild.
105
105
  */
106
106
  softReloadNativeSimulator(simulatorUrl: string): boolean;
107
+ /**
108
+ * Style-only fast path after a watcher rebuild: hot-swap the render-host
109
+ * stylesheets in place (no shell respawn), preserving the page stack / form
110
+ * state / focus. Returns false when no live+ready guest exists so the caller
111
+ * falls back to the full soft reload.
112
+ */
113
+ refreshSimulatorStyles(): boolean;
107
114
  /**
108
115
  * Destroy and null out the simulator view (e.g. on simulator detach).
109
116
  * Also destroys the cached settings view and hides the popover —
@@ -195,11 +202,29 @@ export interface ViewManager {
195
202
  /**
196
203
  * Store the workbench COI base URL without loading it. The heavy
197
204
  * WebContentsView load is deferred to the first visible
198
- * `setWorkbenchBounds`, keeping it off the app boot critical path.
205
+ * `setWorkbenchBounds` and further deferred while `holdWorkbenchAttach`
206
+ * is in force — keeping it off the app boot critical path.
199
207
  */
200
208
  setWorkbenchSource(url: string): void;
201
209
  /** Destroy the workbench editor view (teardown). No-op if never attached. */
202
210
  detachWorkbench(): void;
211
+ /**
212
+ * Close the workbench attach gate: while held (and the view does not exist
213
+ * yet), the lazy attach is deferred so the editor's heavy load stays out of
214
+ * a project open's boot-critical window (teardown + first compile). Returns
215
+ * an idempotent release fn bound to this hold; a newer hold supersedes it
216
+ * (its release becomes a no-op), explicit user intent (openFileInWorkbench)
217
+ * opens the gate, and a 3s cap self-releases with a warn so the editor is
218
+ * never deferred unboundedly. The gate never hides or destroys a live view.
219
+ */
220
+ holdWorkbenchAttach(): () => void;
221
+ /**
222
+ * Void the current workbench attach hold WITHOUT the release replay. For
223
+ * teardown paths that preempt an in-flight open (closeProject, disposeAll):
224
+ * the superseded open's late release or its cap firing must not rebuild the
225
+ * editor view after the project is gone. No-op when nothing is held.
226
+ */
227
+ cancelWorkbenchAttachHold(): void;
203
228
  /**
204
229
  * Reveal a project file in the embedded workbench at a 1-based line/column
205
230
  * (the open-in-editor target coordinate convention). Drives the workbench's
@@ -58,6 +58,10 @@ export function createViewManager(ctx) {
58
58
  safeArea.dispose();
59
59
  }
60
60
  function disposeAll() {
61
+ // App-level teardown invalidates any in-flight open's attach hold first,
62
+ // so the held-detach preserve path below can't keep a desired around for
63
+ // a release/cap replay to rebuild.
64
+ workbench.cancelWorkbenchAttachHold();
61
65
  disposeProjectViews();
62
66
  // Host-scoped teardown: the toolbar view, its port channel, and the
63
67
  // ref-counted session-runtime preload registration.
@@ -66,6 +70,7 @@ export function createViewManager(ctx) {
66
70
  return {
67
71
  attachNativeSimulator: nativeSimulator.attachNativeSimulator,
68
72
  softReloadNativeSimulator: nativeSimulator.softReloadNativeSimulator,
73
+ refreshSimulatorStyles: nativeSimulator.refreshSimulatorStyles,
69
74
  detachSimulator: nativeSimulator.detachSimulator,
70
75
  reapplySafeArea: (device) => safeArea.reapplyAll(device),
71
76
  showSettings: overlayPanels.showSettings,
@@ -89,6 +94,8 @@ export function createViewManager(ctx) {
89
94
  setWorkbenchSource: workbench.setWorkbenchSource,
90
95
  detachWorkbench: workbench.detachWorkbench,
91
96
  openFileInWorkbench: workbench.openFileInWorkbench,
97
+ holdWorkbenchAttach: workbench.holdWorkbenchAttach,
98
+ cancelWorkbenchAttachHold: workbench.cancelWorkbenchAttachHold,
92
99
  setHostToolbarHeight: hostToolbar.setHostToolbarHeight,
93
100
  hostToolbar: hostToolbar.control,
94
101
  };
@@ -10,6 +10,8 @@ export interface WorkbenchView {
10
10
  setWorkbenchSource(url: string): void;
11
11
  detachWorkbench(): void;
12
12
  openFileInWorkbench(relPath: string, line: number, column: number): boolean;
13
+ holdWorkbenchAttach(): () => void;
14
+ cancelWorkbenchAttachHold(): void;
13
15
  }
14
16
  export declare function createWorkbenchView(ctx: ViewManagerContext, reconciler: PlacementReconciler): WorkbenchView;
15
17
  //# sourceMappingURL=workbench-view.d.ts.map
@@ -2,6 +2,12 @@ import { nativeTheme, WebContentsView } from 'electron';
2
2
  import { handleWindowOpenExternal } from '../../windows/navigation-hardening.js';
3
3
  import { VIEW_ID } from '../../../shared/view-ids.js';
4
4
  import { destroyChildView } from './destroy-child-view.js';
5
+ /**
6
+ * Upper bound on how long a hold may defer the workbench attach. A hung or
7
+ * very slow first compile (or a lost release on some open path) degrades to a
8
+ * bounded editor delay plus a warn — never a permanently blank editor slot.
9
+ */
10
+ const ATTACH_HOLD_CAP_MS = 3000;
5
11
  export function createWorkbenchView(ctx, reconciler) {
6
12
  let workbenchView = null;
7
13
  // Whether the devtools-theme → workbench-theme `nativeTheme` listener is live.
@@ -11,8 +17,20 @@ export function createWorkbenchView(ctx, reconciler) {
11
17
  // heavy WebContentsView load is deferred until the 'editor' slot first becomes
12
18
  // visible (first non-zero `setWorkbenchBounds`) so it never sits on the app
13
19
  // boot critical path (which would delay preload/window-ready and trip the e2e
14
- // health check into a relaunch).
20
+ // health check into a relaunch), and further deferred while an attach hold
21
+ // (below) is in force.
15
22
  let workbenchUrl = null;
23
+ // Attach gate: while held (and the view does not exist yet), the lazy attach
24
+ // is deferred so the workbench's heavy load never competes with a project
25
+ // open's boot-critical window (old-session teardown + first compile) for
26
+ // CPU. Generation-tagged: a newer hold supersedes an older one (latest-wins,
27
+ // mirroring the open op-lock), so a superseded open's late release cannot
28
+ // open a newer open's gate. The gate only postpones creation — it never
29
+ // hides or destroys an existing view, and detachWorkbench leaves it intact
30
+ // (an in-flight open holds across the old view's teardown).
31
+ let attachHeld = false;
32
+ let attachHoldGeneration = 0;
33
+ let attachHoldTimer = null;
16
34
  /** Current devtools color scheme, mirrored into the workbench's theme. */
17
35
  function workbenchThemeScheme() {
18
36
  return nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
@@ -70,6 +88,53 @@ export function createWorkbenchView(ctx, reconciler) {
70
88
  function setWorkbenchSource(url) {
71
89
  workbenchUrl = url;
72
90
  }
91
+ // Release the gate for `generation` (stale generations no-op) and replay the
92
+ // reconcile so a pending visible desired attaches through the normal lazy
93
+ // path. `viaCap` marks the self-release at the hold cap — loud, so a slow
94
+ // compile and a lost release stay distinguishable from a normal settle.
95
+ function releaseAttachHold(generation, viaCap) {
96
+ if (!attachHeld || generation !== attachHoldGeneration)
97
+ return;
98
+ attachHeld = false;
99
+ if (attachHoldTimer) {
100
+ clearTimeout(attachHoldTimer);
101
+ attachHoldTimer = null;
102
+ }
103
+ if (viaCap) {
104
+ console.warn(`[workbench] attach gate: hold not released within ${ATTACH_HOLD_CAP_MS}ms (compile still running or a release was lost) — attaching now`);
105
+ }
106
+ reconciler.reconcileNow();
107
+ }
108
+ /**
109
+ * Close the attach gate until the returned release fn runs, a newer hold
110
+ * supersedes it, explicit user intent (openFileInWorkbench) opens it, or the
111
+ * cap timer fires. The release fn is idempotent and generation-bound.
112
+ */
113
+ function holdWorkbenchAttach() {
114
+ const generation = ++attachHoldGeneration;
115
+ attachHeld = true;
116
+ if (attachHoldTimer)
117
+ clearTimeout(attachHoldTimer);
118
+ attachHoldTimer = setTimeout(() => releaseAttachHold(generation, true), ATTACH_HOLD_CAP_MS);
119
+ return () => releaseAttachHold(generation, false);
120
+ }
121
+ /**
122
+ * Void the current hold WITHOUT the release replay: the gate belongs to an
123
+ * open request, and a teardown that preempts it (closeProject / app-level
124
+ * disposeAll) invalidates that request — its late release or cap firing must
125
+ * not rebuild the view after the project is gone. Bumping the generation
126
+ * turns every pending release fn into a no-op; no-op when nothing is held.
127
+ */
128
+ function cancelWorkbenchAttachHold() {
129
+ if (!attachHeld && !attachHoldTimer)
130
+ return;
131
+ attachHeld = false;
132
+ attachHoldGeneration++;
133
+ if (attachHoldTimer) {
134
+ clearTimeout(attachHoldTimer);
135
+ attachHoldTimer = null;
136
+ }
137
+ }
73
138
  function detachWorkbench() {
74
139
  if (workbenchThemeSyncBound) {
75
140
  nativeTheme.removeListener('updated', pushWorkbenchTheme);
@@ -77,7 +142,17 @@ export function createWorkbenchView(ctx, reconciler) {
77
142
  }
78
143
  destroyChildView(ctx.windows.mainWindow, workbenchView);
79
144
  workbenchView = null;
80
- reconciler.deleteBaseDesired(VIEW_ID.workbench);
145
+ if (attachHeld) {
146
+ // A held detach is a project-switch teardown: the renderer published the
147
+ // incoming project's placement before the open started, so that desired
148
+ // is the ONLY thing the release replay can rebuild the view from — keep
149
+ // it (gateHidden hides it while held) and just forget the destroyed
150
+ // instance so the rebuilt view is treated as a fresh attach.
151
+ reconciler.forgetActual(VIEW_ID.workbench);
152
+ }
153
+ else {
154
+ reconciler.deleteBaseDesired(VIEW_ID.workbench);
155
+ }
81
156
  reconciler.reconcileNow();
82
157
  }
83
158
  // Build the `file:///workspace/<rel>` URI string with each path SEGMENT
@@ -123,6 +198,11 @@ export function createWorkbenchView(ctx, reconciler) {
123
198
  // the document is actually shown; false when there is no workbench view or
124
199
  // every attempt failed.
125
200
  function openFileInWorkbench(relPath, line, column) {
201
+ // Explicit user intent (a source-link click) beats the boot-priority
202
+ // scheduling: open the gate first so the release's reconcile replay can
203
+ // lazy-create the view synchronously before the liveness check below.
204
+ if (attachHeld)
205
+ releaseAttachHold(attachHoldGeneration, false);
126
206
  if (!workbenchView || workbenchView.webContents.isDestroyed())
127
207
  return false;
128
208
  // The workbench mirrors the active project under file:///workspace/<rel>; the
@@ -148,12 +228,12 @@ export function createWorkbenchView(ctx, reconciler) {
148
228
  return true;
149
229
  }
150
230
  // workbench is gated until it has a source or a live view (it lazy-creates
151
- // from the URL in the attach op).
231
+ // from the URL in the attach op), and while an attach hold is in force.
152
232
  reconciler.registerView(VIEW_ID.workbench, {
153
233
  getView: () => workbenchView,
154
- gateHidden: () => !workbenchView && !workbenchUrl,
234
+ gateHidden: () => !workbenchView && (!workbenchUrl || attachHeld),
155
235
  beforeAttach: () => {
156
- if (!workbenchView && workbenchUrl) {
236
+ if (!workbenchView && workbenchUrl && !attachHeld) {
157
237
  // Lazy-load the workbench; attachWorkbench adds it to the contentView.
158
238
  void attachWorkbench(workbenchUrl);
159
239
  return true;
@@ -161,10 +241,10 @@ export function createWorkbenchView(ctx, reconciler) {
161
241
  return false;
162
242
  },
163
243
  ensureLazy: (desired) => {
164
- if (desired?.placement.visible && !workbenchView && workbenchUrl)
244
+ if (desired?.placement.visible && !workbenchView && workbenchUrl && !attachHeld)
165
245
  void attachWorkbench(workbenchUrl);
166
246
  },
167
247
  });
168
- return { attachWorkbench, setWorkbenchSource, detachWorkbench, openFileInWorkbench };
248
+ return { attachWorkbench, setWorkbenchSource, detachWorkbench, openFileInWorkbench, holdWorkbenchAttach, cancelWorkbenchAttachHold };
169
249
  }
170
250
  //# sourceMappingURL=workbench-view.js.map
@@ -0,0 +1,16 @@
1
+ import type { ProjectSession } from '../../../shared/types.js';
2
+ import type { ProjectsProvider } from '../projects/types.js';
3
+ /**
4
+ * Run the provider's project-dir validation, folding a throwing/rejecting
5
+ * validator into the same dirError failure path as a returned error string —
6
+ * a broken validator is a failed validation, not a crashed open.
7
+ */
8
+ export declare function validateProjectDirSafe(provider: ProjectsProvider, projectPath: string): Promise<string | null>;
9
+ /**
10
+ * Adapter-return boundary: a session without a string appId can't be driven by
11
+ * the renderer, so it must never become active. Close the live resources the
12
+ * adapter already spun up (best-effort) and return the error message; null when
13
+ * valid.
14
+ */
15
+ export declare function rejectInvalidAppId(session: ProjectSession): Promise<string | null>;
16
+ //# sourceMappingURL=open-project-guards.d.ts.map
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Run the provider's project-dir validation, folding a throwing/rejecting
4
+ * validator into the same dirError failure path as a returned error string —
5
+ * a broken validator is a failed validation, not a crashed open.
6
+ */
7
+ export async function validateProjectDirSafe(provider, projectPath) {
8
+ try {
9
+ return provider.validateProjectDir
10
+ ? await provider.validateProjectDir(projectPath)
11
+ : null;
12
+ }
13
+ catch (err) {
14
+ return err instanceof Error ? err.message : String(err);
15
+ }
16
+ }
17
+ /**
18
+ * Runtime guard for the adapter-return boundary: `session.appInfo` must carry a
19
+ * NON-EMPTY string `appId` (the renderer scopes IPC by it; the bridge's
20
+ * handleSpawn also rejects empty appIds, so accepting `''` would desync layers).
21
+ * Loose: extra fields pass through — only the contract-critical `appId` is enforced.
22
+ */
23
+ const SessionAppInfoSchema = z.looseObject({ appId: z.string().min(1) });
24
+ /**
25
+ * Adapter-return boundary: a session without a string appId can't be driven by
26
+ * the renderer, so it must never become active. Close the live resources the
27
+ * adapter already spun up (best-effort) and return the error message; null when
28
+ * valid.
29
+ */
30
+ export async function rejectInvalidAppId(session) {
31
+ if (SessionAppInfoSchema.safeParse(session.appInfo).success)
32
+ return null;
33
+ try {
34
+ await session.close();
35
+ }
36
+ catch (closeErr) {
37
+ console.warn('[workspace] closing appId-less adapter session failed (non-fatal):', closeErr);
38
+ }
39
+ return 'adapter returned session.appInfo without a string appId — '
40
+ + 'the CompilationAdapter must supply appInfo.appId';
41
+ }
42
+ //# sourceMappingURL=open-project-guards.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Decide and emit the post-rebuild status for one watcher rebuild. Style-only
3
+ * rebuild fast path: hot-swap the render-host stylesheets in place instead of
4
+ * respawning the DeviceShell — page stack / form state / scroll / focus survive
5
+ * (no jitter, no focus steal); `hotReload: false` keeps the renderer from bumping
6
+ * its reload token (the respawn signal). Falls through to the full reload when
7
+ * the swap can't run (no live render guest yet) so an edit is never dropped.
8
+ * `getProjectPages` never throws; empty pages are withheld so the renderer keeps
9
+ * its previous launch dropdown.
10
+ */
11
+ export declare function reportRebuildStatus(info: {
12
+ styleOnly?: boolean;
13
+ } | undefined, deps: {
14
+ projectPath: string;
15
+ repo: {
16
+ getProjectPages: (p: string) => {
17
+ pages: string[];
18
+ };
19
+ };
20
+ autoReload: boolean;
21
+ refreshSimulatorStyles: () => boolean;
22
+ sendStatus: (status: string, message: string, hotReload?: boolean, pages?: string[]) => void;
23
+ }): void;
24
+ //# sourceMappingURL=rebuild-status.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Decide and emit the post-rebuild status for one watcher rebuild. Style-only
3
+ * rebuild fast path: hot-swap the render-host stylesheets in place instead of
4
+ * respawning the DeviceShell — page stack / form state / scroll / focus survive
5
+ * (no jitter, no focus steal); `hotReload: false` keeps the renderer from bumping
6
+ * its reload token (the respawn signal). Falls through to the full reload when
7
+ * the swap can't run (no live render guest yet) so an edit is never dropped.
8
+ * `getProjectPages` never throws; empty pages are withheld so the renderer keeps
9
+ * its previous launch dropdown.
10
+ */
11
+ export function reportRebuildStatus(info, deps) {
12
+ const { pages } = deps.repo.getProjectPages(deps.projectPath);
13
+ const pageList = pages.length ? pages : undefined;
14
+ if (info?.styleOnly && deps.autoReload && deps.refreshSimulatorStyles()) {
15
+ deps.sendStatus('ready', '样式已热更新', false, pageList);
16
+ return;
17
+ }
18
+ deps.sendStatus('ready', deps.autoReload ? '编译完成,已重启' : '编译完成', deps.autoReload, pageList);
19
+ }
20
+ //# sourceMappingURL=rebuild-status.js.map
@@ -4,6 +4,8 @@ export declare function stubProjectsProvider(): import('../projects/types.js').P
4
4
  export declare function makeViewsSpy(): {
5
5
  views: {
6
6
  disposeAll: import("vitest").Mock<() => void>;
7
+ holdWorkbenchAttach: import("vitest").Mock<() => (() => void)>;
8
+ cancelWorkbenchAttachHold: import("vitest").Mock<() => void>;
7
9
  disposeProjectViews: import("vitest").Mock<() => void>;
8
10
  detachWorkbench: import("vitest").Mock<() => void>;
9
11
  detachSimulator: import("vitest").Mock<() => void>;
@@ -15,6 +15,8 @@ export function makeViewsSpy() {
15
15
  const events = [];
16
16
  const views = {
17
17
  disposeAll: vi.fn(() => { events.push('views.disposeAll'); }),
18
+ holdWorkbenchAttach: vi.fn(() => vi.fn()),
19
+ cancelWorkbenchAttachHold: vi.fn(() => { }),
18
20
  // Project-scoped teardown: closeProject must call THIS, never disposeAll
19
21
  // (which would also kill the host toolbar — see view-manager-dispose-scopes.test.ts).
20
22
  disposeProjectViews: vi.fn(() => { events.push('views.disposeProjectViews'); }),
@@ -42,6 +44,8 @@ export function makeClosingHarness(closeBehavior) {
42
44
  };
43
45
  const views = {
44
46
  disposeAll: vi.fn(),
47
+ holdWorkbenchAttach: vi.fn(() => vi.fn()),
48
+ cancelWorkbenchAttachHold: vi.fn(() => { }),
45
49
  disposeProjectViews: vi.fn(),
46
50
  detachWorkbench: vi.fn(),
47
51
  detachSimulator: vi.fn(),
@@ -1,16 +1,10 @@
1
- import { z } from 'zod';
1
+ import { rejectInvalidAppId, validateProjectDirSafe } from './open-project-guards.js';
2
2
  import * as repo from '../projects/project-repository.js';
3
+ import { reportRebuildStatus } from './rebuild-status.js';
3
4
  import { DEFAULT_COMPILE_CONFIG } from '../projects/types.js';
4
5
  import { clearSimulatorServicewechatReferer, setSimulatorServicewechatReferer, } from '../simulator/referer.js';
5
6
  import { loadWorkbenchSettings } from '../settings/index.js';
6
7
  import { createOpLock } from './op-lock.js';
7
- /**
8
- * Runtime guard for the adapter-return boundary: `session.appInfo` must carry a
9
- * NON-EMPTY string `appId` (the renderer scopes IPC by it; the bridge's
10
- * handleSpawn also rejects empty appIds, so accepting `''` would desync layers).
11
- * Loose: extra fields pass through — only the contract-critical `appId` is enforced.
12
- */
13
- const SessionAppInfoSchema = z.looseObject({ appId: z.string().min(1) });
14
8
  /** Build a workspace service bound to the given workbench context. */
15
9
  export function createWorkspaceService(ctx) {
16
10
  let currentSession = null;
@@ -126,15 +120,10 @@ export function createWorkspaceService(ctx) {
126
120
  projectPath,
127
121
  sourcemap: true,
128
122
  fileTypes: ctx.fileTypes,
129
- // Two independent gates: autoBuild = recompile on save; autoReload = refresh the simulator afterwards (off ⇒ page/form state survives; hotReload below stays false so the native simulator, which has no SSE reload, isn't refreshed).
123
+ // Two independent gates: autoBuild = recompile on save; autoReload = reflect the rebuild in the simulator afterwards (off ⇒ page/form state survives). A style-only rebuild takes the in-place stylesheet hot-swap path (see reportRebuildStatus); any other change does a full reload.
130
124
  watch: compile.autoBuild,
131
125
  autoReload: preview.autoReload,
132
- onRebuild: () => {
133
- // getProjectPages never throws; empty pages (read failed / degenerate
134
- // project) are withheld so the renderer keeps its previous dropdown.
135
- const { pages } = repo.getProjectPages(projectPath);
136
- sendStatus('ready', preview.autoReload ? '编译完成,已重启' : '编译完成', preview.autoReload, pages.length ? pages : undefined);
137
- },
126
+ onRebuild: info => reportRebuildStatus(info, { projectPath, repo, autoReload: preview.autoReload, refreshSimulatorStyles: ctx.views.refreshSimulatorStyles, sendStatus }),
138
127
  onBuildError: (err) => sendStatus('error', String(err)),
139
128
  // Watcher died mid-session (EMFILE, permission loss, …): non-fatal, so 'ready' stays but `watcher: 'dead'` flags that saves no longer auto-rebuild.
140
129
  onWatcherError: () => sendStatus('ready', '文件监听已停止,保存不再触发自动编译', false, undefined, true),
@@ -151,24 +140,6 @@ export function createWorkspaceService(ctx) {
151
140
  return { error: String(err) };
152
141
  }
153
142
  }
154
- /**
155
- * Adapter-return boundary: a session without a string appId can't be driven by
156
- * the renderer, so it must never become active. Close the live resources the
157
- * adapter already spun up (best-effort) and return the error message; null when
158
- * valid.
159
- */
160
- async function rejectInvalidAppId(session) {
161
- if (SessionAppInfoSchema.safeParse(session.appInfo).success)
162
- return null;
163
- try {
164
- await session.close();
165
- }
166
- catch (closeErr) {
167
- console.warn('[workspace] closing appId-less adapter session failed (non-fatal):', closeErr);
168
- }
169
- return 'adapter returned session.appInfo without a string appId — '
170
- + 'the CompilationAdapter must supply appInfo.appId';
171
- }
172
143
  function applyRefererFromSession(session) {
173
144
  const appInfo = session.appInfo;
174
145
  if (appInfo && typeof appInfo.appId === 'string' && appInfo.appId.length > 0) {
@@ -215,7 +186,14 @@ export function createWorkspaceService(ctx) {
215
186
  // in-flight one; ownerSeq only grows, so a late-resolving earlier request
216
187
  // can't clobber a later one that already took over).
217
188
  opLock.takeOwnership(mySeq);
189
+ // Defer the workbench editor's heavy attach while this open's
190
+ // boot-critical window (old-session teardown + first compile) runs.
191
+ // The release is idempotent and bound to this hold, so every exit path
192
+ // below releases unconditionally; the gate's own cap timer backstops
193
+ // any future path that forgets to.
194
+ const releaseWorkbenchHold = ctx.views.holdWorkbenchAttach();
218
195
  if (await runOpenTeardown(mySeq)) {
196
+ releaseWorkbenchHold();
219
197
  return { success: false, error: 'superseded by a newer project open/close' };
220
198
  }
221
199
  // Reject obviously broken projects up front so we never spin up a
@@ -225,10 +203,9 @@ export function createWorkspaceService(ctx) {
225
203
  // `fetch(…).then(r=>r.text()).then(JSON.parse)` pipeline dies on the
226
204
  // dev server's HTML SPA fallback with
227
205
  // `SyntaxError: Unexpected token '<', "<!doctype "…`.
228
- const dirError = provider.validateProjectDir
229
- ? await provider.validateProjectDir(projectPath)
230
- : null;
206
+ const dirError = await validateProjectDirSafe(provider, projectPath);
231
207
  if (dirError) {
208
+ releaseWorkbenchHold();
232
209
  sendStatus('error', dirError);
233
210
  return { success: false, error: dirError };
234
211
  }
@@ -237,6 +214,9 @@ export function createWorkspaceService(ctx) {
237
214
  // generation — runCompile's onLog closure checks it per line.
238
215
  const sessionGeneration = ++logGeneration;
239
216
  const compiled = await runCompile(projectPath, sessionGeneration);
217
+ // The compile settling — success OR failure — ends the boot-critical
218
+ // window (a failed compile is when the user needs the editor most).
219
+ releaseWorkbenchHold();
240
220
  if ('error' in compiled) {
241
221
  sendStatus('error', compiled.error);
242
222
  return { success: false, error: compiled.error };
@@ -286,6 +266,11 @@ export function createWorkspaceService(ctx) {
286
266
  // we meant to close.
287
267
  if (!opLock.isOwner(mySeq))
288
268
  return;
269
+ // Void any in-flight open's workbench attach hold BEFORE the teardown
270
+ // awaits below: the superseded open's compile can settle mid-close, and
271
+ // its release (or the gate's cap) must not rebuild the editor view for
272
+ // a project that is being closed.
273
+ ctx.views.cancelWorkbenchAttachHold();
289
274
  // Mark the close in progress BEFORE disposeSession nulls currentSession,
290
275
  // so bridge getters refuse to resolve the dying session during the
291
276
  // session.close() await (cleared in the finally once views are gone).
@@ -12,7 +12,7 @@ function r(e, t) {
12
12
  if (e === t) return !0;
13
13
  if (typeof e != "object" || typeof t != "object" || e == null || t == null) return !1;
14
14
  let n = Object.keys(e), i = Object.keys(t);
15
- return n.length === i.length ? n.every((n) => r(e[n], t[n])) : !1;
15
+ return n.length === i.length && n.every((n) => r(e[n], t[n]));
16
16
  }
17
17
  function i(e, t) {
18
18
  return Object.prototype.hasOwnProperty.call(e, t);