@dimina-kit/devtools 0.4.0-dev.20260711141929 → 0.4.0-dev.20260716163758
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main/app/app.js +39 -10
- package/dist/main/app/native-overview.d.ts +1 -1
- package/dist/main/index.bundle.js +189 -172
- package/dist/main/services/simulator-appdata/index.js +1 -1
- package/dist/main/services/views/native-simulator-view.d.ts +6 -0
- package/dist/main/services/views/native-simulator-view.js +16 -0
- package/dist/main/services/views/refresh-styles.d.ts +10 -0
- package/dist/main/services/views/refresh-styles.js +57 -0
- package/dist/main/services/views/view-manager.d.ts +26 -1
- package/dist/main/services/views/view-manager.js +7 -0
- package/dist/main/services/views/workbench-view.d.ts +2 -0
- package/dist/main/services/views/workbench-view.js +87 -7
- package/dist/main/services/workspace/open-project-guards.d.ts +16 -0
- package/dist/main/services/workspace/open-project-guards.js +42 -0
- package/dist/main/services/workspace/rebuild-status.d.ts +24 -0
- package/dist/main/services/workspace/rebuild-status.js +20 -0
- package/dist/main/services/workspace/workspace-lifecycle-race.testutil.d.ts +2 -0
- package/dist/main/services/workspace/workspace-lifecycle-race.testutil.js +4 -0
- package/dist/main/services/workspace/workspace-service.js +21 -36
- package/dist/native-host/common/common.js +1 -1
- package/dist/native-host/render/render.js +49 -34
- package/dist/native-host/service/service.js +2 -2
- package/dist/preload/instrumentation/app-data.d.ts +2 -2
- package/dist/preload/instrumentation/app-data.js +1 -1
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
- package/dist/preload/windows/main.cjs +96 -0
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +162 -140
- package/dist/preload/windows/simulator.cjs.map +3 -3
- package/dist/preload/windows/simulator.js +162 -140
- package/dist/render-host/render-inspect.js +15 -47
- package/dist/renderer/assets/index-DQnIsRhp.js +49 -0
- package/dist/renderer/assets/{input-COtRON-n.js → input-7P0WbXyg.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BGtDnv6p.css +1 -0
- package/dist/renderer/assets/{ipc-transport-SuI2rOB_.js → ipc-transport-DTEmLpxx.js} +2 -2
- package/dist/renderer/assets/popover-DsOcyIS6.js +2 -0
- package/dist/renderer/assets/{select-Bwb8O0Mf.js → select-Bzbhbg9Z.js} +2 -2
- package/dist/renderer/assets/{settings-BispnvJU.js → settings-CR1TyBWu.js} +2 -2
- package/dist/renderer/assets/{settings-api-HIBz8BFz.js → settings-api-B1wVFAPB.js} +2 -2
- package/dist/renderer/assets/workbenchSettings-CFbToT4Q.js +8 -0
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/shared/ipc-channels.d.ts +1 -0
- package/dist/shared/ipc-channels.js +7 -0
- package/dist/simulator/assets/device-shell-C-HZY3bL.css +1 -0
- package/dist/simulator/assets/device-shell-Cu1ZPLfe.js +2 -0
- package/dist/simulator/assets/{simulator-A1vGZVBM.js → simulator-Cy8c-eA2.js} +5 -5
- package/dist/simulator/simulator.html +1 -1
- package/package.json +7 -7
- package/dist/renderer/assets/index-WbvXdhWQ.js +0 -49
- package/dist/renderer/assets/ipc-transport-CNvORIEX.css +0 -1
- package/dist/renderer/assets/popover-B73T3z-m.js +0 -2
- package/dist/renderer/assets/workbenchSettings-CtRBr7gS.js +0 -8
- package/dist/shared/appdata-accumulator.d.ts +0 -95
- package/dist/shared/appdata-accumulator.js +0 -218
- package/dist/simulator/assets/device-shell-C-wcFq3Z.js +0 -2
- 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 '
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
|
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);
|