@dimina-kit/devtools 0.3.2-dev.20260611135124 → 0.4.0-dev.20260612025610
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/README.md +8 -6
- package/dist/main/api.d.ts +1 -1
- package/dist/main/app/app.js +22 -12
- package/dist/main/app/launch.d.ts +4 -4
- package/dist/main/app/launch.js +5 -17
- package/dist/main/index.bundle.js +309 -213
- package/dist/main/menu/index.d.ts +2 -2
- package/dist/main/menu/index.js +4 -2
- package/dist/main/runtime/miniapp-runtime.d.ts +79 -11
- package/dist/main/services/views/host-toolbar-port-channel.d.ts +10 -0
- package/dist/main/services/views/host-toolbar-port-channel.js +89 -10
- package/dist/main/services/views/view-manager.d.ts +12 -0
- package/dist/main/services/views/view-manager.js +10 -0
- package/dist/main/services/workbench-context.d.ts +7 -0
- package/dist/main/services/workbench-context.js +5 -0
- package/dist/main/services/workspace/workspace-service.d.ts +3 -7
- package/dist/main/services/workspace/workspace-service.js +27 -0
- package/dist/main/windows/settings-window/index.d.ts +33 -0
- package/dist/main/windows/settings-window/index.js +59 -0
- package/dist/preload/runtime/host-toolbar-port.d.ts +10 -5
- package/dist/preload/runtime/host-toolbar-port.js +24 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs +9 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/shared/types.d.ts +48 -11
- package/package.json +4 -4
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function installAppMenu(ctx:
|
|
1
|
+
import type { MenuContext } from '../../shared/types.js';
|
|
2
|
+
export declare function installAppMenu(ctx: MenuContext): void;
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/main/menu/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Menu } from 'electron';
|
|
2
|
-
|
|
2
|
+
// The built-in menu consumes the narrow MenuContext, the same surface a host
|
|
3
|
+
// menuBuilder receives — proof that the hand-written contract covers the
|
|
4
|
+
// real internal consumption (settings entry + navigate-back).
|
|
3
5
|
export function installAppMenu(ctx) {
|
|
4
6
|
const template = [
|
|
5
7
|
{
|
|
@@ -8,7 +10,7 @@ export function installAppMenu(ctx) {
|
|
|
8
10
|
{
|
|
9
11
|
label: '开发工具设置',
|
|
10
12
|
click: () => {
|
|
11
|
-
void
|
|
13
|
+
void ctx.openSettings().catch(() => { });
|
|
12
14
|
},
|
|
13
15
|
},
|
|
14
16
|
{ type: 'separator' },
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
* past the drift sentinel.
|
|
18
18
|
* - `workspace.openProject` stays writable (no `readonly`): qdmp gates
|
|
19
19
|
* project permissions by reassigning it (documented monkey-patch contract).
|
|
20
|
-
* - `windows` is an OPAQUE handle (`object`): hosts pass it through to
|
|
21
|
-
* framework helpers but never reach into it (no BrowserWindow leak).
|
|
22
20
|
*
|
|
23
21
|
* `asMiniappRuntime(ctx)` is an identity return — the contract is a typed
|
|
24
22
|
* VIEW onto the live context, not a snapshot/projection. That is what makes
|
|
@@ -35,6 +33,53 @@ export interface MiniappProjectStatusPayload {
|
|
|
35
33
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
36
34
|
hotReload?: boolean;
|
|
37
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Structured shape of `getSession().appInfo` — the doc-promised
|
|
38
|
+
* (`host-migration.md` "appInfo 已结构化") DTO, named so hosts can type what
|
|
39
|
+
* they receive without deep imports or casts.
|
|
40
|
+
*
|
|
41
|
+
* `appId` is REQUIRED: the renderer derives its IPC scoping from it and the
|
|
42
|
+
* default devkit adapter always supplies one (fallback included). A custom
|
|
43
|
+
* adapter that returns a session without a string `appId` is rejected at the
|
|
44
|
+
* `openProject` boundary (see workspace-service.ts) — it never becomes the
|
|
45
|
+
* active session. The decorative rest stays optional: `name`/`path` come from
|
|
46
|
+
* the devkit adapter, `appName` from the devtools-side mirror; minimal custom
|
|
47
|
+
* adapters may omit all three.
|
|
48
|
+
*/
|
|
49
|
+
export interface MiniappSessionAppInfo {
|
|
50
|
+
appId: string;
|
|
51
|
+
name?: string;
|
|
52
|
+
path?: string;
|
|
53
|
+
appName?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The page-side bridge the framework injects into the host-toolbar WCV as
|
|
57
|
+
* `window.diminaHostToolbar` (see src/preload/runtime/host-toolbar-port.ts).
|
|
58
|
+
* Declared here (the electron-free contract module) so TypeScript toolbar
|
|
59
|
+
* pages can type the bridge without hand-rolling — and without importing
|
|
60
|
+
* anything Electron-flavored.
|
|
61
|
+
*
|
|
62
|
+
* Shape mirrors the injected bridge EXACTLY:
|
|
63
|
+
* - `send` returns void (pre-handshake sends are queued, bounded at 128);
|
|
64
|
+
* - `onMessage` returns a BARE un-subscribe function — unlike the main-side
|
|
65
|
+
* control's `{ dispose }` — because that is what the preload exposes.
|
|
66
|
+
* Both throw a `TypeError` synchronously when `channel` is not a non-empty
|
|
67
|
+
* string (parity with the main side's `onMessage` guard).
|
|
68
|
+
*/
|
|
69
|
+
export interface DiminaHostToolbarPageBridge {
|
|
70
|
+
send: (channel: string, payload: unknown) => void;
|
|
71
|
+
onMessage: (channel: string, handler: (payload: unknown) => void) => () => void;
|
|
72
|
+
}
|
|
73
|
+
declare global {
|
|
74
|
+
interface Window {
|
|
75
|
+
/**
|
|
76
|
+
* Present ONLY inside the host-toolbar WebContentsView's main frame
|
|
77
|
+
* (guarded injection — see host-toolbar-runtime.ts); optional everywhere
|
|
78
|
+
* else, so page code must runtime-guard before use.
|
|
79
|
+
*/
|
|
80
|
+
diminaHostToolbar?: DiminaHostToolbarPageBridge;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
38
83
|
/**
|
|
39
84
|
* Host-facing control surface for the toolbar WebContentsView — the exact
|
|
40
85
|
* post-R2 message-channel surface (`send`/`onMessage`), with no `webContents`
|
|
@@ -67,9 +112,22 @@ export interface MiniappHostToolbar {
|
|
|
67
112
|
onMessage: (channel: string, handler: (payload: unknown) => void) => {
|
|
68
113
|
dispose: () => void;
|
|
69
114
|
};
|
|
115
|
+
/**
|
|
116
|
+
* Observe handshake readiness. Fires the handler once per load generation,
|
|
117
|
+
* exactly when the toolbar page's MessagePort handshake completes (i.e.
|
|
118
|
+
* when `send` flips to true). Registering while the channel is already
|
|
119
|
+
* ready fires once asynchronously on a microtask (missed-signal guard);
|
|
120
|
+
* a reload / re-handshake fires registered handlers again. `dispose()`
|
|
121
|
+
* detaches (idempotent).
|
|
122
|
+
*/
|
|
123
|
+
onReady: (handler: () => void) => {
|
|
124
|
+
dispose: () => void;
|
|
125
|
+
};
|
|
70
126
|
/**
|
|
71
127
|
* Pin (`{ fixed }`) or unpin (`'auto'`) the toolbar strip height. `'auto'`
|
|
72
128
|
* (default) lets the in-page height advertiser drive the placeholder.
|
|
129
|
+
* `{ fixed }` values must be finite and non-negative — anything else throws
|
|
130
|
+
* a `TypeError` synchronously and leaves the standing mode untouched.
|
|
73
131
|
*/
|
|
74
132
|
setHeightMode: (mode: 'auto' | {
|
|
75
133
|
fixed: number;
|
|
@@ -103,10 +161,11 @@ export interface MiniappWorkspace {
|
|
|
103
161
|
/**
|
|
104
162
|
* Minimal live-session DTO, or null when no session. Deliberately excludes
|
|
105
163
|
* the session's own `close` — hosts end sessions via `closeProject`, never
|
|
106
|
-
* behind the workspace's back.
|
|
164
|
+
* behind the workspace's back. `appInfo` is structured (see
|
|
165
|
+
* {@link MiniappSessionAppInfo}) — no casting required.
|
|
107
166
|
*/
|
|
108
167
|
getSession: () => {
|
|
109
|
-
appInfo:
|
|
168
|
+
appInfo: MiniappSessionAppInfo;
|
|
110
169
|
} | null;
|
|
111
170
|
}
|
|
112
171
|
/**
|
|
@@ -115,8 +174,6 @@ export interface MiniappWorkspace {
|
|
|
115
174
|
* plumbing is a deliberate semver decision, not an accident of projection.
|
|
116
175
|
*/
|
|
117
176
|
export interface MiniappRuntime {
|
|
118
|
-
/** Absolute path to the devtools renderer dist directory. */
|
|
119
|
-
rendererDir: string;
|
|
120
177
|
/** View layer — only the host-owned toolbar control is public. */
|
|
121
178
|
views: {
|
|
122
179
|
readonly hostToolbar: MiniappHostToolbar;
|
|
@@ -127,15 +184,26 @@ export interface MiniappRuntime {
|
|
|
127
184
|
notify: {
|
|
128
185
|
projectStatus: (payload: MiniappProjectStatusPayload) => void;
|
|
129
186
|
};
|
|
130
|
-
/**
|
|
187
|
+
/**
|
|
188
|
+
* Lifecycle sink: hosts register their own teardown via `registry.add`.
|
|
189
|
+
* Accepts BOTH disposal idioms — a `{ dispose }` object (what
|
|
190
|
+
* `hostToolbar.onMessage`/`onReady` return) or a bare `() => void` —
|
|
191
|
+
* matching the live registry, so `registry.add(sub)` works without the
|
|
192
|
+
* `registry.add(() => sub.dispose())` wrapper.
|
|
193
|
+
*/
|
|
131
194
|
registry: {
|
|
132
|
-
add: (
|
|
195
|
+
add: (d: {
|
|
196
|
+
dispose: () => void;
|
|
197
|
+
} | (() => void)) => void;
|
|
133
198
|
};
|
|
134
199
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
200
|
+
* Open (or re-focus) the standalone workbench-settings window. Replaces the
|
|
201
|
+
* retired `windows` opaque pass-through (whose only documented purpose —
|
|
202
|
+
* `openSettingsWindow(ctx)` — could never typecheck from the contract) and
|
|
203
|
+
* the `rendererDir` member that existed solely to feed it; hosts needing
|
|
204
|
+
* the renderer dist path use the `/paths` export instead.
|
|
137
205
|
*/
|
|
138
|
-
|
|
206
|
+
openSettings: () => Promise<void>;
|
|
139
207
|
}
|
|
140
208
|
/**
|
|
141
209
|
* View a full `WorkbenchContext` as its `MiniappRuntime` contract. Identity
|
|
@@ -68,6 +68,16 @@ export interface HostToolbarPortChannel {
|
|
|
68
68
|
invalidate(): void;
|
|
69
69
|
/** Register a control-level inbound handler for `channel`. */
|
|
70
70
|
onMessage(channel: string, handler: (payload: unknown) => void): HostToolbarMessageSubscription;
|
|
71
|
+
/**
|
|
72
|
+
* Observe handshake readiness. Fires `handler` once per load GENERATION,
|
|
73
|
+
* at the moment that generation's handshake completes (`send` flips true).
|
|
74
|
+
* Registering while already `ready` schedules a one-shot catch-up fire on a
|
|
75
|
+
* microtask — never synchronously — and the catch-up RE-CHECKS at fire time
|
|
76
|
+
* that (a) the subscription is still registered and (b) the generation is
|
|
77
|
+
* unchanged / the port is still live, so a same-frame `dispose()` or
|
|
78
|
+
* host-initiated load suppresses it. Inert (never fires) after `dispose()`.
|
|
79
|
+
*/
|
|
80
|
+
onReady(handler: () => void): HostToolbarMessageSubscription;
|
|
71
81
|
/**
|
|
72
82
|
* Post `{ channel, payload }` to the toolbar page over the live port.
|
|
73
83
|
* Returns false (delivering NOTHING, creating NOTHING) when there is no
|
|
@@ -50,10 +50,20 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
50
50
|
let activePort = null;
|
|
51
51
|
/** The wc that owns `activePort` (so a stale wc's `destroyed` can't drop a successor's port). */
|
|
52
52
|
let activeWc = null;
|
|
53
|
-
|
|
53
|
+
/** Named lifecycle state. INVARIANT: `state === 'ready'` ⟺ `activePort !== null`. */
|
|
54
|
+
let state = 'absent';
|
|
55
|
+
/**
|
|
56
|
+
* Monotonic per-handshake counter. Each completed handshake is one load
|
|
57
|
+
* GENERATION; `onReady` catch-up fires capture it at registration and
|
|
58
|
+
* re-check it at fire time so a fire scheduled for generation N can never
|
|
59
|
+
* deliver after a navigation/handshake moved the channel past N.
|
|
60
|
+
*/
|
|
61
|
+
let generation = 0;
|
|
54
62
|
// Array (not Map<channel, Set>) so the same handler function may be
|
|
55
63
|
// registered twice and each registration disposes independently.
|
|
56
64
|
const handlers = [];
|
|
65
|
+
// onReady registrations. Same array-of-entries discipline as `handlers`.
|
|
66
|
+
const readyHandlers = [];
|
|
57
67
|
function dispatch(data) {
|
|
58
68
|
// Inbound waist: the toolbar page is host-arbitrary content — anything
|
|
59
69
|
// that is not an object envelope with a string channel is dropped.
|
|
@@ -68,10 +78,16 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
68
78
|
entry.handler(payload);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Drop the live port and transition to `next` (`'absent'` for death paths,
|
|
83
|
+
* `'awaitingHandshake'` for navigation paths). Never demotes `disposed`.
|
|
84
|
+
*/
|
|
85
|
+
function dropActivePort(close, next) {
|
|
72
86
|
const port = activePort;
|
|
73
87
|
activePort = null;
|
|
74
88
|
activeWc = null;
|
|
89
|
+
if (state !== 'disposed')
|
|
90
|
+
state = next;
|
|
75
91
|
if (close && port) {
|
|
76
92
|
try {
|
|
77
93
|
port.close();
|
|
@@ -81,13 +97,39 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
81
97
|
}
|
|
82
98
|
}
|
|
83
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Invoke one onReady handler with exception ISOLATION: subscribers are
|
|
102
|
+
* arbitrary control-level code — one throwing must neither starve sibling
|
|
103
|
+
* registrations nor escape the surrounding event/microtask callback (a
|
|
104
|
+
* throw out of a `did-finish-load` listener or a `queueMicrotask` body is
|
|
105
|
+
* process-level `uncaughtException` territory). Report-and-continue.
|
|
106
|
+
*/
|
|
107
|
+
function invokeReadyHandler(handler) {
|
|
108
|
+
try {
|
|
109
|
+
handler();
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error('[host-toolbar] onReady handler threw:', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Fire every still-registered onReady handler exactly once (snapshot iteration). */
|
|
116
|
+
function fireReadyHandlers() {
|
|
117
|
+
for (const entry of [...readyHandlers]) {
|
|
118
|
+
// A handler may dispose a sibling registration mid-fire: re-check
|
|
119
|
+
// membership so a disposed entry never fires. Per-handler isolation
|
|
120
|
+
// (NOT around the loop): a throwing handler must not abort the fire
|
|
121
|
+
// for later-registered siblings.
|
|
122
|
+
if (readyHandlers.includes(entry))
|
|
123
|
+
invokeReadyHandler(entry.handler);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
84
126
|
function handshake(wc) {
|
|
85
|
-
if (disposed)
|
|
127
|
+
if (state === 'disposed')
|
|
86
128
|
return;
|
|
87
129
|
if (!opts.isCurrent(wc))
|
|
88
130
|
return;
|
|
89
131
|
// The previous load's document is gone; its port goes with it.
|
|
90
|
-
dropActivePort(true);
|
|
132
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
91
133
|
const { port1, port2 } = new MessageChannelMain();
|
|
92
134
|
port1.on('message', (event) => {
|
|
93
135
|
dispatch(event.data);
|
|
@@ -96,12 +138,17 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
96
138
|
// the reference so send() reports false instead of posting into the void.
|
|
97
139
|
port1.on('close', () => {
|
|
98
140
|
if (activePort === port1)
|
|
99
|
-
dropActivePort(false);
|
|
141
|
+
dropActivePort(false, 'absent');
|
|
100
142
|
});
|
|
101
143
|
wc.postMessage(ViewChannel.HostToolbarPort, null, [port2]);
|
|
102
144
|
port1.start();
|
|
103
145
|
activePort = port1;
|
|
104
146
|
activeWc = wc;
|
|
147
|
+
state = 'ready';
|
|
148
|
+
generation++;
|
|
149
|
+
// Readiness signal AFTER the port is live: a handler calling send() from
|
|
150
|
+
// inside its onReady fire must observe `true`.
|
|
151
|
+
fireReadyHandlers();
|
|
105
152
|
}
|
|
106
153
|
return {
|
|
107
154
|
attach(wc) {
|
|
@@ -129,15 +176,15 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
129
176
|
: isMainFramePositional;
|
|
130
177
|
if (isSameDocument || !isMainFrame)
|
|
131
178
|
return;
|
|
132
|
-
dropActivePort(true);
|
|
179
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
133
180
|
});
|
|
134
181
|
wc.on('destroyed', () => {
|
|
135
182
|
if (activeWc === wc)
|
|
136
|
-
dropActivePort(true);
|
|
183
|
+
dropActivePort(true, 'absent');
|
|
137
184
|
});
|
|
138
185
|
},
|
|
139
186
|
invalidate() {
|
|
140
|
-
dropActivePort(true);
|
|
187
|
+
dropActivePort(true, 'awaitingHandshake');
|
|
141
188
|
},
|
|
142
189
|
onMessage(channel, handler) {
|
|
143
190
|
if (typeof channel !== 'string' || channel === '') {
|
|
@@ -153,6 +200,37 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
153
200
|
},
|
|
154
201
|
};
|
|
155
202
|
},
|
|
203
|
+
onReady(handler) {
|
|
204
|
+
// Torn-down control: inert registration — never fires, never throws.
|
|
205
|
+
if (state === 'disposed') {
|
|
206
|
+
return { dispose() { } };
|
|
207
|
+
}
|
|
208
|
+
const entry = { handler };
|
|
209
|
+
readyHandlers.push(entry);
|
|
210
|
+
if (state === 'ready') {
|
|
211
|
+
// Missed-signal catch-up: the handshake already happened, so the
|
|
212
|
+
// subscriber would otherwise wait forever. Asynchronous on a
|
|
213
|
+
// microtask (never re-enter host code synchronously inside
|
|
214
|
+
// onReady()), and RE-CHECKED at fire time — both the subscription's
|
|
215
|
+
// liveness and the load generation can change between scheduling and
|
|
216
|
+
// the microtask (same-frame dispose() / same-frame loadFile).
|
|
217
|
+
const scheduledGeneration = generation;
|
|
218
|
+
queueMicrotask(() => {
|
|
219
|
+
if (state !== 'ready' || generation !== scheduledGeneration || !activePort)
|
|
220
|
+
return;
|
|
221
|
+
if (!readyHandlers.includes(entry))
|
|
222
|
+
return;
|
|
223
|
+
invokeReadyHandler(entry.handler);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
dispose() {
|
|
228
|
+
const i = readyHandlers.indexOf(entry);
|
|
229
|
+
if (i >= 0)
|
|
230
|
+
readyHandlers.splice(i, 1);
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
},
|
|
156
234
|
send(channel, payload) {
|
|
157
235
|
if (!activePort)
|
|
158
236
|
return false;
|
|
@@ -160,9 +238,10 @@ export function createHostToolbarPortChannel(opts) {
|
|
|
160
238
|
return true;
|
|
161
239
|
},
|
|
162
240
|
dispose() {
|
|
163
|
-
|
|
164
|
-
|
|
241
|
+
dropActivePort(true, 'absent');
|
|
242
|
+
state = 'disposed';
|
|
165
243
|
handlers.length = 0;
|
|
244
|
+
readyHandlers.length = 0;
|
|
166
245
|
},
|
|
167
246
|
};
|
|
168
247
|
}
|
|
@@ -224,6 +224,18 @@ export interface HostToolbarControl {
|
|
|
224
224
|
* (idempotent).
|
|
225
225
|
*/
|
|
226
226
|
onMessage(channel: string, handler: (payload: unknown) => void): HostToolbarMessageSubscription;
|
|
227
|
+
/**
|
|
228
|
+
* Observe handshake readiness — the push counterpart to polling `send()`
|
|
229
|
+
* for `true`. Fires the handler once per load generation, exactly when
|
|
230
|
+
* that load's MessagePort handshake completes; registering while the
|
|
231
|
+
* channel is ALREADY ready fires once asynchronously on a microtask
|
|
232
|
+
* (missed-signal race guard, re-validated at fire time). A reload /
|
|
233
|
+
* re-handshake fires registered handlers again; a host-initiated
|
|
234
|
+
* `loadURL`/`loadFile` invalidates readiness at initiation, so handlers
|
|
235
|
+
* registered in that window wait for the NEW document's handshake.
|
|
236
|
+
* `dispose()` detaches (idempotent); `disposeAll` sweeps everything.
|
|
237
|
+
*/
|
|
238
|
+
onReady(handler: () => void): HostToolbarMessageSubscription;
|
|
227
239
|
/**
|
|
228
240
|
* Post `{ channel, payload }` to the toolbar page (received via
|
|
229
241
|
* `window.diminaHostToolbar.onMessage(channel, handler)`). Gated and
|
|
@@ -306,6 +306,13 @@ export function createViewManager(ctx) {
|
|
|
306
306
|
hostToolbarPreloadOverride = path;
|
|
307
307
|
},
|
|
308
308
|
setHeightMode(mode) {
|
|
309
|
+
// Validate BEFORE touching any state: a poisoned `{ fixed }` (NaN /
|
|
310
|
+
// ±Infinity / negative) must neither reach the renderer placeholder
|
|
311
|
+
// (`height: NaNpx` corrupts the strip with no error anywhere) nor
|
|
312
|
+
// clobber the standing mode — fail-closed, not fail-corrupt.
|
|
313
|
+
if (mode !== 'auto' && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
|
|
314
|
+
throw new TypeError(`hostToolbar.setHeightMode: fixed height must be a finite, non-negative number (got ${mode.fixed})`);
|
|
315
|
+
}
|
|
309
316
|
hostToolbarHeightMode = mode;
|
|
310
317
|
if (mode !== 'auto') {
|
|
311
318
|
// Pin immediately: a preload-less/static toolbar never advertises, so
|
|
@@ -319,6 +326,9 @@ export function createViewManager(ctx) {
|
|
|
319
326
|
onMessage(channel, handler) {
|
|
320
327
|
return hostToolbarPort.onMessage(channel, handler);
|
|
321
328
|
},
|
|
329
|
+
onReady(handler) {
|
|
330
|
+
return hostToolbarPort.onReady(handler);
|
|
331
|
+
},
|
|
322
332
|
send(channel, payload) {
|
|
323
333
|
return hostToolbarPort.send(channel, payload);
|
|
324
334
|
},
|
|
@@ -41,6 +41,13 @@ export interface WorkbenchContext {
|
|
|
41
41
|
windows: WindowService;
|
|
42
42
|
/** Unified main → renderer event dispatcher */
|
|
43
43
|
notify: RendererNotifier;
|
|
44
|
+
/**
|
|
45
|
+
* Open (or re-focus) the standalone workbench-settings window. First-class
|
|
46
|
+
* member so contract holders (`MiniappRuntime` / `MenuContext`) can open
|
|
47
|
+
* settings without reaching into `windows`/`notify` plumbing. Wired by
|
|
48
|
+
* `createWorkbenchContext` to the real `openSettingsWindow` path.
|
|
49
|
+
*/
|
|
50
|
+
openSettings: () => Promise<void>;
|
|
44
51
|
/** Single source of truth for project + session + per-project settings */
|
|
45
52
|
workspace: WorkspaceService;
|
|
46
53
|
/**
|
|
@@ -4,6 +4,7 @@ import { defaultAdapter } from './default-adapter.js';
|
|
|
4
4
|
import { createRendererNotifier, } from './notifications/renderer-notifier.js';
|
|
5
5
|
import { createViewManager } from './views/view-manager.js';
|
|
6
6
|
import { createWindowService } from './window-service.js';
|
|
7
|
+
import { openSettingsWindow } from '../windows/settings-window/index.js';
|
|
7
8
|
import { createWorkspaceService, } from './workspace/workspace-service.js';
|
|
8
9
|
import { createLocalProjectsProvider } from './projects/local-provider.js';
|
|
9
10
|
import { createSimulatorApiRegistry, } from './simulator/custom-apis.js';
|
|
@@ -29,6 +30,10 @@ export function createWorkbenchContext(opts) {
|
|
|
29
30
|
ctx.windows = createWindowService(opts.mainWindow);
|
|
30
31
|
ctx.views = createViewManager(ctx);
|
|
31
32
|
ctx.notify = createRendererNotifier(ctx);
|
|
33
|
+
// Lazy closure (not a bound snapshot): reads ctx.windows/notify/rendererDir
|
|
34
|
+
// at call time through the live context, which structurally satisfies the
|
|
35
|
+
// helper's narrow OpenSettingsWindowDeps.
|
|
36
|
+
ctx.openSettings = () => openSettingsWindow(ctx);
|
|
32
37
|
ctx.projectsProvider = opts.projectsProvider ?? createLocalProjectsProvider();
|
|
33
38
|
ctx.projectTemplates = resolveTemplates(BUILTIN_TEMPLATES, opts.projectTemplates ?? [], opts.builtinTemplates ?? 'all');
|
|
34
39
|
ctx.customCreateProjectDialog = opts.customCreateProjectDialog;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompileConfig } from '../../../shared/types.js';
|
|
1
|
+
import type { AppInfo, CompileConfig, ProjectSession } from '../../../shared/types.js';
|
|
2
2
|
import type { WorkbenchContext } from '../workbench-context.js';
|
|
3
3
|
import type { Project, ProjectPages, ProjectSettings } from '../projects/project-repository.js';
|
|
4
4
|
/**
|
|
@@ -9,7 +9,7 @@ import type { Project, ProjectPages, ProjectSettings } from '../projects/project
|
|
|
9
9
|
export interface OpenProjectResult {
|
|
10
10
|
success: boolean;
|
|
11
11
|
port?: number;
|
|
12
|
-
appInfo?:
|
|
12
|
+
appInfo?: AppInfo;
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
@@ -32,11 +32,7 @@ export interface WorkspaceService {
|
|
|
32
32
|
validateProjectDir(dirPath: string): Promise<string | null>;
|
|
33
33
|
openProject(projectPath: string): Promise<OpenProjectResult>;
|
|
34
34
|
closeProject(): Promise<void>;
|
|
35
|
-
getSession():
|
|
36
|
-
close: () => Promise<void>;
|
|
37
|
-
port: number;
|
|
38
|
-
appInfo: unknown;
|
|
39
|
-
} | null;
|
|
35
|
+
getSession(): ProjectSession | null;
|
|
40
36
|
getProjectPath(): string;
|
|
41
37
|
/**
|
|
42
38
|
* The project path most recently torn down by `closeProject`, or '' if none
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import * as repo from '../projects/project-repository.js';
|
|
2
3
|
import { DEFAULT_COMPILE_CONFIG } from '../projects/types.js';
|
|
3
4
|
import { clearSimulatorServicewechatReferer, setSimulatorServicewechatReferer, } from '../simulator/referer.js';
|
|
4
5
|
import { loadWorkbenchSettings } from '../settings/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Runtime guard for the adapter-return boundary: `session.appInfo` must be an
|
|
8
|
+
* object carrying a string `appId` (the renderer derives its IPC scoping from
|
|
9
|
+
* it; a session without one cannot be driven). Loose: extra fields pass
|
|
10
|
+
* through untouched — only the contract-critical `appId` is enforced.
|
|
11
|
+
*/
|
|
12
|
+
const SessionAppInfoSchema = z.looseObject({ appId: z.string() });
|
|
5
13
|
/** Build a workspace service bound to the given workbench context. */
|
|
6
14
|
export function createWorkspaceService(ctx) {
|
|
7
15
|
let currentSession = null;
|
|
@@ -99,6 +107,25 @@ export function createWorkspaceService(ctx) {
|
|
|
99
107
|
sendStatus('error', String(err));
|
|
100
108
|
return { success: false, error: String(err) };
|
|
101
109
|
}
|
|
110
|
+
// Adapter-return boundary: enforce the AppInfo producer contract the
|
|
111
|
+
// moment the adapter resolves, BEFORE the session is recorded. A
|
|
112
|
+
// session without a string appId cannot be driven by the renderer, so
|
|
113
|
+
// it must never become the active session. The adapter already spun up
|
|
114
|
+
// live resources (compile watcher, dev-server port) — close them
|
|
115
|
+
// best-effort before reporting; the validation report is the law and a
|
|
116
|
+
// failing close() must not mask it (or escape as a throw).
|
|
117
|
+
if (!SessionAppInfoSchema.safeParse(session.appInfo).success) {
|
|
118
|
+
try {
|
|
119
|
+
await session.close();
|
|
120
|
+
}
|
|
121
|
+
catch (closeErr) {
|
|
122
|
+
console.warn('[workspace] closing appId-less adapter session failed (non-fatal):', closeErr);
|
|
123
|
+
}
|
|
124
|
+
const error = 'adapter returned session.appInfo without a string appId — ' +
|
|
125
|
+
'the CompilationAdapter must supply appInfo.appId';
|
|
126
|
+
sendStatus('error', error);
|
|
127
|
+
return { success: false, error };
|
|
128
|
+
}
|
|
102
129
|
currentSession = session;
|
|
103
130
|
currentProjectPath = projectPath;
|
|
104
131
|
bestEffort('updateLastOpened', () => {
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
import type { BrowserWindow } from 'electron';
|
|
2
|
+
import { type WorkbenchSettings } from '../../services/settings/index.js';
|
|
1
3
|
export { createSettingsWindow } from './create.js';
|
|
2
4
|
export { wireSettingsWindowEvents } from './events.js';
|
|
5
|
+
/**
|
|
6
|
+
* Exactly what `openSettingsWindow` needs — its OWN narrow deps interface,
|
|
7
|
+
* not a `Pick<WorkbenchContext, …>`: picking from the full context couples
|
|
8
|
+
* this helper to the whole context type (and made the contract's old
|
|
9
|
+
* `windows: object` pass-through promise unfulfillable). A full
|
|
10
|
+
* `WorkbenchContext` satisfies this structurally, so assembly points pass
|
|
11
|
+
* the context straight through.
|
|
12
|
+
*/
|
|
13
|
+
export interface OpenSettingsWindowDeps {
|
|
14
|
+
/** Absolute path to the renderer dist directory (settings entry HTML). */
|
|
15
|
+
rendererDir: string;
|
|
16
|
+
windows: {
|
|
17
|
+
readonly mainWindow: BrowserWindow;
|
|
18
|
+
readonly settingsWindow: BrowserWindow | null;
|
|
19
|
+
setSettingsWindow(win: BrowserWindow | null): void;
|
|
20
|
+
};
|
|
21
|
+
notify: {
|
|
22
|
+
workbenchSettingsInit(window: BrowserWindow, payload: {
|
|
23
|
+
settings: WorkbenchSettings;
|
|
24
|
+
}): void;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open (or re-focus) the standalone workbench-settings window. Reuses the
|
|
29
|
+
* live window when one is already registered; otherwise creates it, registers
|
|
30
|
+
* it on the window service, and wires its `closed` cleanup. Concurrent calls
|
|
31
|
+
* share a single in-flight creation (exactly one window; every caller still
|
|
32
|
+
* gets the show/focus/snapshot semantics). Always shows + focuses, then
|
|
33
|
+
* pushes the current settings snapshot into it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openSettingsWindow(deps: OpenSettingsWindowDeps): Promise<void>;
|
|
3
36
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
import { createSettingsWindow } from './create.js';
|
|
2
|
+
import { wireSettingsWindowEvents } from './events.js';
|
|
3
|
+
import { loadWorkbenchSettings, } from '../../services/settings/index.js';
|
|
1
4
|
export { createSettingsWindow } from './create.js';
|
|
2
5
|
export { wireSettingsWindowEvents } from './events.js';
|
|
6
|
+
/**
|
|
7
|
+
* In-flight creation guard, keyed by the windows registry (the identity that
|
|
8
|
+
* owns the `settingsWindow` slot). Overlapping `openSettingsWindow` calls
|
|
9
|
+
* before the first creation lands must share ONE creation — without this,
|
|
10
|
+
* both observe `settingsWindow === null` across the async construction
|
|
11
|
+
* boundary, build two BrowserWindows, and the loser is orphaned (alive,
|
|
12
|
+
* unreachable, never reused).
|
|
13
|
+
*/
|
|
14
|
+
const inFlightCreations = new WeakMap();
|
|
15
|
+
async function getOrCreateSettingsWindow(deps) {
|
|
16
|
+
const existing = deps.windows.settingsWindow;
|
|
17
|
+
if (existing && !existing.isDestroyed())
|
|
18
|
+
return existing;
|
|
19
|
+
const inFlight = inFlightCreations.get(deps.windows);
|
|
20
|
+
if (inFlight)
|
|
21
|
+
return inFlight;
|
|
22
|
+
const creation = (async () => {
|
|
23
|
+
const win = await createSettingsWindow(deps.windows.mainWindow, deps.rendererDir);
|
|
24
|
+
deps.windows.setSettingsWindow(win);
|
|
25
|
+
wireSettingsWindowEvents(win, () => {
|
|
26
|
+
// Electron delivers 'closed' asynchronously: a just-destroyed window's
|
|
27
|
+
// late callback may arrive AFTER a successor was registered. Only the
|
|
28
|
+
// CURRENT registration's own close may clear the slot — a stale
|
|
29
|
+
// window's close must not drop a live successor.
|
|
30
|
+
if (deps.windows.settingsWindow === win) {
|
|
31
|
+
deps.windows.setSettingsWindow(null);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return win;
|
|
35
|
+
})();
|
|
36
|
+
inFlightCreations.set(deps.windows, creation);
|
|
37
|
+
try {
|
|
38
|
+
return await creation;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (inFlightCreations.get(deps.windows) === creation) {
|
|
42
|
+
inFlightCreations.delete(deps.windows);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Open (or re-focus) the standalone workbench-settings window. Reuses the
|
|
48
|
+
* live window when one is already registered; otherwise creates it, registers
|
|
49
|
+
* it on the window service, and wires its `closed` cleanup. Concurrent calls
|
|
50
|
+
* share a single in-flight creation (exactly one window; every caller still
|
|
51
|
+
* gets the show/focus/snapshot semantics). Always shows + focuses, then
|
|
52
|
+
* pushes the current settings snapshot into it.
|
|
53
|
+
*/
|
|
54
|
+
export async function openSettingsWindow(deps) {
|
|
55
|
+
const win = await getOrCreateSettingsWindow(deps);
|
|
56
|
+
win.show();
|
|
57
|
+
win.focus();
|
|
58
|
+
deps.notify.workbenchSettingsInit(win, {
|
|
59
|
+
settings: loadWorkbenchSettings(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
3
62
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Preload side of the host-toolbar gated narrow channel.
|
|
3
3
|
*
|
|
4
|
+
* ── PUBLIC SURFACE EVOLUTION RULE ───────────────────────────────────────────
|
|
5
|
+
* `window.diminaHostToolbar` is a published page-facing API, typed by the
|
|
6
|
+
* exported `DiminaHostToolbarPageBridge` (src/main/runtime/miniapp-runtime.ts)
|
|
7
|
+
* — arbitrary host toolbar pages compile against it. Members may only be
|
|
8
|
+
* ADDED, never changed: any semantic change to an existing member (signature,
|
|
9
|
+
* return shape, queueing/throw behavior) ships under a NEW name instead.
|
|
10
|
+
* Exception: security fixes / compliance corrections may change existing
|
|
11
|
+
* member semantics, and MUST be called out in the release's version notes.
|
|
12
|
+
* Keep the exported bridge type in lockstep with what is exposed here.
|
|
13
|
+
*
|
|
4
14
|
* Receives the per-load MessagePort main transfers on `did-finish-load`
|
|
5
15
|
* (`ViewChannel.HostToolbarPort`, `event.ports[0]`) and bridges it to the page
|
|
6
16
|
* as `window.diminaHostToolbar` — EXACTLY `{ send, onMessage }`, functions
|
|
@@ -35,10 +45,5 @@
|
|
|
35
45
|
* envelope (queued first-comers survive), one console.warn per load.
|
|
36
46
|
*/
|
|
37
47
|
export declare const HOST_TOOLBAR_PENDING_LIMIT = 128;
|
|
38
|
-
/**
|
|
39
|
-
* Subscribe the handshake channel and expose the page bridge. Call ONLY from
|
|
40
|
-
* a passing toolbar-runtime guard (`activateHostToolbarRuntime`) — a failing
|
|
41
|
-
* guard must leave zero footprint (no bridge key, no IPC listener).
|
|
42
|
-
*/
|
|
43
48
|
export declare function installHostToolbarPortBridge(): void;
|
|
44
49
|
//# sourceMappingURL=host-toolbar-port.d.ts.map
|