@dimina-kit/devtools 0.4.0-dev.20260703051110 → 0.4.0-dev.20260706064107
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 +6 -0
- package/dist/main/index.bundle.js +445 -49
- package/dist/main/ipc/bridge-router.d.ts +8 -0
- package/dist/main/ipc/bridge-router.js +352 -38
- package/dist/main/services/compile-standby.d.ts +26 -0
- package/dist/main/services/compile-standby.js +57 -0
- package/dist/main/services/console-forward/index.d.ts +12 -1
- package/dist/main/services/console-forward/index.js +99 -1
- package/dist/main/services/diagnostics/index.d.ts +49 -0
- package/dist/main/services/diagnostics/index.js +61 -0
- package/dist/main/services/network-forward/index.js +32 -0
- package/dist/main/services/notifications/renderer-notifier.d.ts +39 -0
- package/dist/main/services/notifications/renderer-notifier.js +4 -1
- package/dist/main/services/projects/project-repository.js +2 -1
- package/dist/main/services/views/simulator-session-policy.js +17 -0
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/services/workspace/workspace-service.js +12 -3
- package/dist/main/windows/service-host-window/create.d.ts +15 -1
- package/dist/main/windows/service-host-window/create.js +11 -3
- package/dist/preload/shared/api-compat.js +18 -55
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs.map +1 -1
- package/dist/preload/windows/simulator.cjs +117 -44
- package/dist/preload/windows/simulator.cjs.map +3 -3
- package/dist/preload/windows/simulator.js +117 -44
- package/dist/renderer/assets/index-BwzPwCmM.js +49 -0
- package/dist/renderer/assets/{input-Cjk0wSEh.js → input-CTb_le7F.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BPWIV5hA.css +1 -0
- package/dist/renderer/assets/{ipc-transport-OZS-KmOW.js → ipc-transport-Ck5Xa8Tu.js} +2 -2
- package/dist/renderer/assets/popover-B1Zu2UQu.js +2 -0
- package/dist/renderer/assets/{select-BQDXi5ih.js → select-ozSk5Pt6.js} +2 -2
- package/dist/renderer/assets/{settings-tP3Px2KR.js → settings-CNwGOI22.js} +2 -2
- package/dist/renderer/assets/settings-api-DMbMp1Eq.js +2 -0
- package/dist/renderer/assets/{workbenchSettings-COhuFF-i.js → workbenchSettings-Cr4EHo5w.js} +2 -2
- 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/bridge-channels.d.ts +22 -0
- package/dist/shared/ipc-channels.d.ts +3 -0
- package/dist/shared/ipc-channels.js +9 -0
- package/dist/shared/request-core.d.ts +84 -0
- package/dist/shared/request-core.js +168 -0
- package/dist/shared/simulator-api-metadata.d.ts +22 -17
- package/dist/shared/simulator-api-metadata.js +36 -0
- package/dist/shared/types.d.ts +7 -4
- package/dist/simulator/assets/{device-shell-CiLAPa0I.js → device-shell-C-wcFq3Z.js} +2 -2
- package/dist/simulator/assets/{jsx-runtime-CDK-o-S0.js → jsx-runtime-BDTY6fEq.js} +2 -2
- package/dist/simulator/assets/simulator-DU3-fS3v.js +10 -0
- package/dist/simulator/assets/simulator-mini-app-DzfMfnVM.js +2 -0
- package/dist/simulator/simulator.html +2 -2
- package/package.json +5 -5
- package/dist/renderer/assets/index-D-ksyN1p.js +0 -49
- package/dist/renderer/assets/ipc-transport-D5dKIti1.css +0 -1
- package/dist/renderer/assets/popover-CQVvOe90.js +0 -2
- package/dist/renderer/assets/settings-api-B6x2mhaD.js +0 -2
- package/dist/simulator/assets/simulator-Dvnxry3y.js +0 -10
- package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires devkit's warm-standby compile worker into the devtools runtime.
|
|
3
|
+
*
|
|
4
|
+
* While no project is open, devkit keeps one project-agnostic compile worker
|
|
5
|
+
* forked + prewarmed; every openProject adopts it so the first compile skips
|
|
6
|
+
* fork + compiler import (~1.6s). This module owns exactly the glue devtools
|
|
7
|
+
* needs on top: turn the accelerator on at boot, mirror its lifecycle events
|
|
8
|
+
* onto the diagnostics bus (a user-machine bug report then carries the whole
|
|
9
|
+
* standby story), and shut it down with the context.
|
|
10
|
+
*
|
|
11
|
+
* Failure-transparent by design: if devkit is missing or predates the standby
|
|
12
|
+
* API, the accelerator silently stays off — compilation itself is untouched
|
|
13
|
+
* (the adapter falls back to devkit's normal cold-fork path, or the host runs
|
|
14
|
+
* its own adapter entirely).
|
|
15
|
+
*/
|
|
16
|
+
import type { DiagnosticsBus } from './diagnostics/index.js';
|
|
17
|
+
export interface CompileStandbyDeps {
|
|
18
|
+
/** Test hook: replaces the dynamic import('@dimina-kit/devkit'). */
|
|
19
|
+
loadDevkit?: () => Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
export declare function setupCompileWorkerStandby(ctx: {
|
|
22
|
+
diagnostics?: DiagnosticsBus;
|
|
23
|
+
}, deps?: CompileStandbyDeps): {
|
|
24
|
+
dispose: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=compile-standby.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const EVENT_SEVERITY = {
|
|
2
|
+
'spawned': 'info',
|
|
3
|
+
'prewarmed': 'info',
|
|
4
|
+
'adopted': 'info',
|
|
5
|
+
'died': 'warn',
|
|
6
|
+
'health-check-failed': 'warn',
|
|
7
|
+
'degraded': 'error',
|
|
8
|
+
};
|
|
9
|
+
export function setupCompileWorkerStandby(ctx, deps = {}) {
|
|
10
|
+
const loadDevkit = deps.loadDevkit ?? (() => import('@dimina-kit/devkit'));
|
|
11
|
+
let manager = null;
|
|
12
|
+
let disposed = false;
|
|
13
|
+
const forward = (ev) => {
|
|
14
|
+
try {
|
|
15
|
+
const detail = [
|
|
16
|
+
`compile standby ${ev.type}`,
|
|
17
|
+
typeof ev.pid === 'number' ? `pid=${ev.pid}` : null,
|
|
18
|
+
ev.reason ? `— ${ev.reason}` : null,
|
|
19
|
+
].filter(Boolean).join(' ');
|
|
20
|
+
ctx.diagnostics?.report({
|
|
21
|
+
severity: EVENT_SEVERITY[ev.type] ?? 'info',
|
|
22
|
+
code: 'compile-standby',
|
|
23
|
+
message: detail,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// telemetry must never break the standby
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
void (async () => {
|
|
31
|
+
let enable;
|
|
32
|
+
try {
|
|
33
|
+
const devkit = await loadDevkit();
|
|
34
|
+
enable = devkit.enableCompileWorkerStandby;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// devkit unavailable — the accelerator quietly stays off.
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (typeof enable !== 'function')
|
|
41
|
+
return;
|
|
42
|
+
manager = enable({ onEvent: forward });
|
|
43
|
+
// A teardown that raced the async load wins: the manager is disposed the
|
|
44
|
+
// moment it exists, so a quitting app can never leak a fresh spare.
|
|
45
|
+
if (disposed)
|
|
46
|
+
void manager.dispose();
|
|
47
|
+
})();
|
|
48
|
+
return {
|
|
49
|
+
dispose: async () => {
|
|
50
|
+
if (disposed)
|
|
51
|
+
return;
|
|
52
|
+
disposed = true;
|
|
53
|
+
await manager?.dispose();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=compile-standby.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Disposable } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
|
|
3
|
+
import type { DiagnosticsBus } from '../diagnostics/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* One console entry posted by a guest preload. Shape mirrors
|
|
5
6
|
* render-host/preload.cjs + service-host/preload.cjs `emitConsoleLog`:
|
|
@@ -31,6 +32,16 @@ export interface ConsoleForwarder extends Disposable {
|
|
|
31
32
|
* render→service forward is internal and not exposed as a sink.
|
|
32
33
|
*/
|
|
33
34
|
subscribe(sink: ConsoleSink): Disposable;
|
|
35
|
+
/**
|
|
36
|
+
* Flush any diagnostic queued for `appSessionId` (plus the global,
|
|
37
|
+
* session-less queue) into that session's now-ready service-host wc.
|
|
38
|
+
* Idempotent: entries are removed from their bucket as they flush, so a
|
|
39
|
+
* repeat call for the same session injects nothing further. Call once the
|
|
40
|
+
* session's service-host webContents has actually navigated/loaded (e.g.
|
|
41
|
+
* from `bootServiceHost`, after `did-finish-load`) — calling it earlier just
|
|
42
|
+
* finds nothing resolvable and leaves the queue intact for a later call.
|
|
43
|
+
*/
|
|
44
|
+
notifyServiceHostReady(appSessionId: string): void;
|
|
34
45
|
}
|
|
35
|
-
export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'
|
|
46
|
+
export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'>, diagnostics?: DiagnosticsBus): ConsoleForwarder;
|
|
36
47
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -25,9 +25,81 @@ function buildForwardScript(level, args) {
|
|
|
25
25
|
// `consoleAPICalled` whose top frame URL === RENDER_FORWARD_SOURCE_URL.
|
|
26
26
|
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/** `Diagnostic.severity` → the literal `console.<method>` call to emit (dot form, not computed — kept greppable/CDP-attributable like a hand-written call site). */
|
|
29
|
+
const DIAGNOSTIC_CONSOLE_CALL = {
|
|
30
|
+
error: 'console.error',
|
|
31
|
+
warn: 'console.warn',
|
|
32
|
+
info: 'console.info',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build the `executeJavaScript` source that injects one diagnostic into the
|
|
36
|
+
* service host's console, prefixed `[dimina-kit]`. Args ride as a JSON string
|
|
37
|
+
* (same anti-injection shape as `buildForwardScript`) and the sourceURL is the
|
|
38
|
+
* SAME sentinel the render mirror uses, so this line is exempt from
|
|
39
|
+
* service-console's loop-safety skip too.
|
|
40
|
+
*/
|
|
41
|
+
function buildDiagnosticScript(severity, message) {
|
|
42
|
+
const call = DIAGNOSTIC_CONSOLE_CALL[severity];
|
|
43
|
+
const argsJson = JSON.stringify([`[dimina-kit] ${message}`]);
|
|
44
|
+
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});${call}(...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
|
|
45
|
+
}
|
|
46
|
+
export function createConsoleForwarder(bridge, diagnostics) {
|
|
29
47
|
const sinks = new Set();
|
|
30
48
|
const registry = new DisposableRegistry();
|
|
49
|
+
// Diagnostics queued because no live service-host wc could be resolved yet,
|
|
50
|
+
// bucketed by the owning appSessionId; diagnostics with no appSessionId land
|
|
51
|
+
// in `pendingGlobal` instead. `notifyServiceHostReady` drains both into the
|
|
52
|
+
// session that just became ready.
|
|
53
|
+
const pendingBySession = new Map();
|
|
54
|
+
const pendingGlobal = [];
|
|
55
|
+
// Sessions whose service host has finished loading service.html (the caller
|
|
56
|
+
// signals this via `notifyServiceHostReady`). A resolvable wc alone is NOT
|
|
57
|
+
// readiness: the window exists (and is bound) before its spawn navigation
|
|
58
|
+
// lands, and anything injected into that pre-load document is wiped by the
|
|
59
|
+
// navigation — the diagnostic would silently vanish from the Console panel.
|
|
60
|
+
const readySessions = new Set();
|
|
61
|
+
function injectDiagnostic(wc, d) {
|
|
62
|
+
wc.executeJavaScript(buildDiagnosticScript(d.severity, d.message), true).catch(() => { });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Inject a diagnostic into its owning session's service-host console, or
|
|
66
|
+
* queue it until that host is READY (not merely constructed — see
|
|
67
|
+
* `readySessions`). Ownership is strict: a session-owned diagnostic NEVER
|
|
68
|
+
* falls back to the currently-active host — while `handleSpawn` is still
|
|
69
|
+
* running, the new session's host doesn't exist yet and the "active" host
|
|
70
|
+
* is the OUTGOING session's window, so a fallback would land the message in
|
|
71
|
+
* a console about to be destroyed instead of the session it explains. Only
|
|
72
|
+
* session-less diagnostics may use the active host. Never throws — a
|
|
73
|
+
* missing/destroyed/not-yet-ready host is a normal state, not an error.
|
|
74
|
+
*/
|
|
75
|
+
function handleDiagnostic(d) {
|
|
76
|
+
if (d.appSessionId) {
|
|
77
|
+
const wc = readySessions.has(d.appSessionId) && bridge.getServiceWcForBridge
|
|
78
|
+
? bridge.getServiceWcForBridge(d.appSessionId)
|
|
79
|
+
: null;
|
|
80
|
+
if (wc && !wc.isDestroyed()) {
|
|
81
|
+
injectDiagnostic(wc, d);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const bucket = pendingBySession.get(d.appSessionId);
|
|
85
|
+
if (bucket)
|
|
86
|
+
bucket.push(d);
|
|
87
|
+
else
|
|
88
|
+
pendingBySession.set(d.appSessionId, [d]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const wc = bridge.getServiceWc();
|
|
92
|
+
if (wc && !wc.isDestroyed()) {
|
|
93
|
+
injectDiagnostic(wc, d);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
pendingGlobal.push(d);
|
|
97
|
+
}
|
|
98
|
+
if (diagnostics) {
|
|
99
|
+
// replay:true so diagnostics reported before this forwarder existed (early
|
|
100
|
+
// boot, buffered on the bus) are queued/injected here too.
|
|
101
|
+
registry.add(diagnostics.subscribe(handleDiagnostic, { replay: true }));
|
|
102
|
+
}
|
|
31
103
|
/**
|
|
32
104
|
* Render→service forward. Resolves the service host for the entry's owning
|
|
33
105
|
* page (multi-app safe), falling back to the active app's service host when
|
|
@@ -79,8 +151,34 @@ export function createConsoleForwarder(bridge) {
|
|
|
79
151
|
sinks.add(sink);
|
|
80
152
|
return registry.add(toDisposable(() => { sinks.delete(sink); }));
|
|
81
153
|
},
|
|
154
|
+
notifyServiceHostReady(appSessionId) {
|
|
155
|
+
// Readiness is sticky per session: from here on, session-owned
|
|
156
|
+
// diagnostics inject directly instead of queueing.
|
|
157
|
+
readySessions.add(appSessionId);
|
|
158
|
+
// Re-resolve fresh rather than trusting whatever `handleDiagnostic` saw at
|
|
159
|
+
// report time — the caller invokes this exactly when it knows the wc just
|
|
160
|
+
// became live, so that resolution wins even if an earlier snapshot was
|
|
161
|
+
// destroyed/missing.
|
|
162
|
+
const wc = bridge.getServiceWcForBridge ? bridge.getServiceWcForBridge(appSessionId) : null;
|
|
163
|
+
if (!wc)
|
|
164
|
+
return;
|
|
165
|
+
const sessionEntries = pendingBySession.get(appSessionId);
|
|
166
|
+
if (sessionEntries) {
|
|
167
|
+
for (const d of sessionEntries)
|
|
168
|
+
injectDiagnostic(wc, d);
|
|
169
|
+
pendingBySession.delete(appSessionId);
|
|
170
|
+
}
|
|
171
|
+
if (pendingGlobal.length) {
|
|
172
|
+
for (const d of pendingGlobal)
|
|
173
|
+
injectDiagnostic(wc, d);
|
|
174
|
+
pendingGlobal.length = 0;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
82
177
|
dispose() {
|
|
83
178
|
sinks.clear();
|
|
179
|
+
pendingBySession.clear();
|
|
180
|
+
pendingGlobal.length = 0;
|
|
181
|
+
readySessions.clear();
|
|
84
182
|
return registry.disposeAll();
|
|
85
183
|
},
|
|
86
184
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics bus: the single authoritative channel for developer-facing
|
|
3
|
+
* diagnostics synthesized by main (e.g. "page not found", "logic bundle
|
|
4
|
+
* unreachable") that do not originate from a real `console.*` call the
|
|
5
|
+
* embedded service-host DevTools' CDP capture could observe on its own.
|
|
6
|
+
*
|
|
7
|
+
* Main-synthesized diagnostics used to reach only `ctx.guestConsole.emit`,
|
|
8
|
+
* which fans out to automation subscribers but — for `source:'service'`
|
|
9
|
+
* entries — is never injected into the service host's own console (see
|
|
10
|
+
* `console-forward`'s render-only forward invariant), so they never appeared
|
|
11
|
+
* in the Console panel. This bus is the fix: `report()` is the one place a
|
|
12
|
+
* diagnostic is born, and `console-forward` subscribes to inject each one
|
|
13
|
+
* into the owning session's service-host console (queuing until that
|
|
14
|
+
* session's window is ready — see `notifyServiceHostReady`).
|
|
15
|
+
*/
|
|
16
|
+
export type DiagnosticSeverity = 'error' | 'warn' | 'info';
|
|
17
|
+
export interface Diagnostic {
|
|
18
|
+
severity: DiagnosticSeverity;
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
appSessionId?: string;
|
|
22
|
+
ts: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DiagnosticsBus {
|
|
25
|
+
/** Record one diagnostic: buffers it, mirrors it to the main-process console, and synchronously notifies every live subscriber. No-op after `dispose()`. */
|
|
26
|
+
report(d: {
|
|
27
|
+
severity: DiagnosticSeverity;
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
appSessionId?: string;
|
|
31
|
+
}): void;
|
|
32
|
+
/**
|
|
33
|
+
* Register a sink. With `replay` (default true) the sink is first called,
|
|
34
|
+
* in order, for every buffered diagnostic still held, then for every
|
|
35
|
+
* diagnostic reported from now on; `replay: false` skips the backlog and
|
|
36
|
+
* only delivers diagnostics reported after this call.
|
|
37
|
+
*/
|
|
38
|
+
subscribe(sink: (d: Diagnostic) => void, opts?: {
|
|
39
|
+
replay?: boolean;
|
|
40
|
+
}): {
|
|
41
|
+
dispose(): void;
|
|
42
|
+
};
|
|
43
|
+
/** Clears subscribers and the buffer. `report()` becomes a no-op afterward (including the console mirror — dispose means fully off, not "log-only"). */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function createDiagnosticsBus(opts?: {
|
|
47
|
+
bufferCap?: number;
|
|
48
|
+
}): DiagnosticsBus;
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_BUFFER_CAP = 200;
|
|
2
|
+
const CONSOLE_METHOD = {
|
|
3
|
+
error: 'error',
|
|
4
|
+
warn: 'warn',
|
|
5
|
+
info: 'info',
|
|
6
|
+
};
|
|
7
|
+
export function createDiagnosticsBus(opts) {
|
|
8
|
+
const bufferCap = opts?.bufferCap && opts.bufferCap > 0 ? opts.bufferCap : DEFAULT_BUFFER_CAP;
|
|
9
|
+
const buffer = [];
|
|
10
|
+
const sinks = new Set();
|
|
11
|
+
let disposed = false;
|
|
12
|
+
return {
|
|
13
|
+
report(d) {
|
|
14
|
+
if (disposed)
|
|
15
|
+
return;
|
|
16
|
+
const entry = { ...d, ts: Date.now() };
|
|
17
|
+
buffer.push(entry);
|
|
18
|
+
// Ring buffer: drop the oldest once over cap. Bounded at `bufferCap` so a
|
|
19
|
+
// burst of synthesized diagnostics can never grow this without limit.
|
|
20
|
+
if (buffer.length > bufferCap)
|
|
21
|
+
buffer.shift();
|
|
22
|
+
// The one guaranteed-visible sink: unlike Console-panel injection (which
|
|
23
|
+
// needs a live, ready service-host wc), the main-process terminal is
|
|
24
|
+
// always there.
|
|
25
|
+
console[CONSOLE_METHOD[entry.severity]](`[dimina-kit:${entry.code}] ${entry.message}`);
|
|
26
|
+
for (const sink of sinks) {
|
|
27
|
+
try {
|
|
28
|
+
sink(entry);
|
|
29
|
+
}
|
|
30
|
+
catch { /* a sink must never break the others */ }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
subscribe(sink, subOpts) {
|
|
34
|
+
const replay = subOpts?.replay ?? true;
|
|
35
|
+
if (replay) {
|
|
36
|
+
for (const entry of buffer) {
|
|
37
|
+
try {
|
|
38
|
+
sink(entry);
|
|
39
|
+
}
|
|
40
|
+
catch { /* isolate a replay throw same as a live one */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
sinks.add(sink);
|
|
44
|
+
let released = false;
|
|
45
|
+
return {
|
|
46
|
+
dispose() {
|
|
47
|
+
if (released)
|
|
48
|
+
return;
|
|
49
|
+
released = true;
|
|
50
|
+
sinks.delete(sink);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
dispose() {
|
|
55
|
+
disposed = true;
|
|
56
|
+
sinks.clear();
|
|
57
|
+
buffer.length = 0;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -253,6 +253,16 @@ export function createNetworkForwarder(bridge) {
|
|
|
253
253
|
// degrade, dropped if we go ready (so a request shows in exactly one sink).
|
|
254
254
|
let probeConsoleBuffer = [];
|
|
255
255
|
let readyTimeoutTimer = null;
|
|
256
|
+
// Wall-clock deadline for the CURRENT probe, set once when 'probing' begins.
|
|
257
|
+
// scheduleReadyRetry() re-checks this on every retry so the retry chain is
|
|
258
|
+
// itself authoritative on giving up — it does not depend on winning a race
|
|
259
|
+
// against readyTimeoutTimer firing first (two timers due at the same virtual
|
|
260
|
+
// instant have no guaranteed firing order under fake timers, which let a
|
|
261
|
+
// never-ready host's retry chain outlive the nominal timeout in CI: an
|
|
262
|
+
// "Aborting after running 10000 timers" abort in `network-forward`'s
|
|
263
|
+
// ready-timeout test). readyTimeoutTimer stays as a backstop for hosts that
|
|
264
|
+
// never retry at all (e.g. the queue drains before the deadline).
|
|
265
|
+
let probeDeadline = null;
|
|
256
266
|
// ── Batched native dispatch into the DevTools front-end ───────────────────
|
|
257
267
|
// Queued raw CDP messages (already namespaced + JSON-stringified) awaiting a
|
|
258
268
|
// microtask flush — coalescing many events into one executeJavaScript avoids
|
|
@@ -269,6 +279,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
269
279
|
if (sink === 'probing')
|
|
270
280
|
return;
|
|
271
281
|
sink = 'probing';
|
|
282
|
+
probeDeadline = Date.now() + DEVTOOLS_READY_TIMEOUT_MS;
|
|
272
283
|
if (readyTimeoutTimer)
|
|
273
284
|
clearTimeout(readyTimeoutTimer);
|
|
274
285
|
readyTimeoutTimer = setTimeout(() => {
|
|
@@ -281,6 +292,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
281
292
|
/** Native path confirmed live: console buffer is moot, drop it. */
|
|
282
293
|
function markReady() {
|
|
283
294
|
sink = 'ready';
|
|
295
|
+
probeDeadline = null;
|
|
284
296
|
if (readyTimeoutTimer) {
|
|
285
297
|
clearTimeout(readyTimeoutTimer);
|
|
286
298
|
readyTimeoutTimer = null;
|
|
@@ -299,6 +311,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
299
311
|
*/
|
|
300
312
|
function degradeToConsole() {
|
|
301
313
|
sink = 'degraded';
|
|
314
|
+
probeDeadline = null;
|
|
302
315
|
dispatchQueue = [];
|
|
303
316
|
if (readyTimeoutTimer) {
|
|
304
317
|
clearTimeout(readyTimeoutTimer);
|
|
@@ -444,10 +457,21 @@ export function createNetworkForwarder(bridge) {
|
|
|
444
457
|
function scheduleReadyRetry() {
|
|
445
458
|
if (readyRetryTimer || sink === 'ready' || sink === 'degraded')
|
|
446
459
|
return;
|
|
460
|
+
// Self-terminate on the same deadline readyTimeoutTimer enforces, instead of
|
|
461
|
+
// trusting that timer to win a same-instant race against this one (see the
|
|
462
|
+
// comment on `probeDeadline`'s declaration).
|
|
463
|
+
if (probeDeadline !== null && Date.now() >= probeDeadline) {
|
|
464
|
+
degradeToConsole();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
447
467
|
readyRetryTimer = setTimeout(() => {
|
|
448
468
|
readyRetryTimer = null;
|
|
449
469
|
if (sink === 'degraded')
|
|
450
470
|
return;
|
|
471
|
+
if (probeDeadline !== null && Date.now() >= probeDeadline) {
|
|
472
|
+
degradeToConsole();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
451
475
|
if (dispatchQueue.length > 0)
|
|
452
476
|
scheduleFlush();
|
|
453
477
|
}, READY_RETRY_MS);
|
|
@@ -504,6 +528,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
504
528
|
// A new simulator attach restarts the native sink: 'idle' until the next
|
|
505
529
|
// event re-probes the (possibly already-set) host.
|
|
506
530
|
sink = 'idle';
|
|
531
|
+
probeDeadline = null;
|
|
507
532
|
if (readyRetryTimer) {
|
|
508
533
|
clearTimeout(readyRetryTimer);
|
|
509
534
|
readyRetryTimer = null;
|
|
@@ -692,6 +717,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
692
717
|
clearTimeout(readyRetryTimer);
|
|
693
718
|
readyRetryTimer = null;
|
|
694
719
|
}
|
|
720
|
+
probeDeadline = null;
|
|
695
721
|
// Records buffered while probing the OLD host are stale — drop, don't flush
|
|
696
722
|
// (their native copies were already queued; on a host swap we restart clean).
|
|
697
723
|
probeConsoleBuffer = [];
|
|
@@ -732,6 +758,12 @@ export function createNetworkForwarder(bridge) {
|
|
|
732
758
|
catch { /* gone */ }
|
|
733
759
|
});
|
|
734
760
|
}
|
|
761
|
+
// A host swap while still 'probing' the OLD host must not inherit its
|
|
762
|
+
// window: beginProbing()'s `sink === 'probing'` guard would otherwise skip
|
|
763
|
+
// (re-)arming readyTimeoutTimer/probeDeadline for the NEW host, leaving it
|
|
764
|
+
// probing forever with no timeout. Force through 'idle' so beginProbing()
|
|
765
|
+
// always arms a fresh window for whichever host is now current.
|
|
766
|
+
sink = 'idle';
|
|
735
767
|
beginProbing();
|
|
736
768
|
if (dispatchQueue.length > 0)
|
|
737
769
|
scheduleFlush();
|
|
@@ -11,6 +11,43 @@ export interface ProjectStatusPayload {
|
|
|
11
11
|
message: string;
|
|
12
12
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
13
13
|
hotReload?: boolean;
|
|
14
|
+
/** Freshly-read page list, carried on a hot-reload status so the launch dropdown stays current. Absent when the read failed or wasn't attempted. */
|
|
15
|
+
pages?: string[];
|
|
16
|
+
/** Present (`'dead'`) once the project's file watcher has stopped — saves no longer trigger an automatic rebuild. */
|
|
17
|
+
watcher?: 'dead';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Payload for the `session:runtimeStatus` push — the post-compile SESSION
|
|
21
|
+
* lifecycle main tracks per `appSessionId` in bridge-router (`AppSession`
|
|
22
|
+
* itself is main-private; this is the public projection). A successful
|
|
23
|
+
* compile only means the resource tree exists; it says nothing about whether
|
|
24
|
+
* the simulator actually booted — this closes that gap.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionRuntimeStatusPayload {
|
|
27
|
+
appId: string;
|
|
28
|
+
phase: 'launching' | 'running' | 'launch-failed' | 'crashed';
|
|
29
|
+
/**
|
|
30
|
+
* Machine-readable cause, present on `'launch-failed'` and `'crashed'`:
|
|
31
|
+
* `'timeout'` — the launch watchdog (`LAUNCH_TIMEOUT_MS`) expired before the
|
|
32
|
+
* root page reported `domReady`; `'logic-bundle-unreachable'` — the
|
|
33
|
+
* compiled `logic.js` could not be fetched/executed;
|
|
34
|
+
* `'service-host-navigation-failed'` — the service-host window failed to
|
|
35
|
+
* navigate to its spawn URL; `'service-host-crashed'` — the service host's
|
|
36
|
+
* renderer process was gone. Plain `string` (not a literal union) so an
|
|
37
|
+
* unrecognized future code degrades gracefully instead of a type error.
|
|
38
|
+
*/
|
|
39
|
+
code?: string;
|
|
40
|
+
/** Human-readable detail, mirroring the short form of the matching `ctx.diagnostics` report. */
|
|
41
|
+
reason?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Present on `'launching'` when `handleSpawn`'s page-mount gate
|
|
44
|
+
* (`resolveRootPagePath`) substituted the resolved page for the request —
|
|
45
|
+
* i.e. `SpawnResult.pageFallbackApplied` was true for this spawn.
|
|
46
|
+
*/
|
|
47
|
+
pageFallback?: {
|
|
48
|
+
requested: string;
|
|
49
|
+
resolved: string;
|
|
50
|
+
};
|
|
14
51
|
}
|
|
15
52
|
/**
|
|
16
53
|
* Payload for the `project:compileLog` push — one filtered dmcc log line.
|
|
@@ -51,6 +88,8 @@ export interface WorkbenchSettingsInitPayload {
|
|
|
51
88
|
export interface RendererNotifier {
|
|
52
89
|
/** Broadcast project compile status transitions to the main renderer. */
|
|
53
90
|
projectStatus(payload: ProjectStatusPayload): void;
|
|
91
|
+
/** Broadcast a session's post-compile runtime lifecycle to the main renderer. */
|
|
92
|
+
sessionRuntimeStatus(payload: SessionRuntimeStatusPayload): void;
|
|
54
93
|
/** Push one per-line dmcc compile-log entry to the main renderer. */
|
|
55
94
|
compileLog(payload: CompileLogPayload): void;
|
|
56
95
|
/** Ask the main renderer to navigate back to its landing screen. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProjectChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
|
|
1
|
+
import { ProjectChannel, SessionChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
|
|
2
2
|
/** Safely resolve a WebContents, skipping destroyed / missing targets. */
|
|
3
3
|
function liveWebContents(wc) {
|
|
4
4
|
if (!wc)
|
|
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
|
|
|
24
24
|
projectStatus(payload) {
|
|
25
25
|
sendToMain(ProjectChannel.Status, payload);
|
|
26
26
|
},
|
|
27
|
+
sessionRuntimeStatus(payload) {
|
|
28
|
+
sendToMain(SessionChannel.RuntimeStatus, payload);
|
|
29
|
+
},
|
|
27
30
|
compileLog(payload) {
|
|
28
31
|
sendToMain(ProjectChannel.CompileLog, payload);
|
|
29
32
|
},
|
|
@@ -103,7 +103,8 @@ export function getProjectPages(dirPath) {
|
|
|
103
103
|
entryPagePath: appJson.entryPagePath || appJson.pages?.[0] || '',
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
-
catch {
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn(`Failed to read project pages from ${appJsonPath}`, err);
|
|
107
108
|
return { pages: [], entryPagePath: '' };
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -19,6 +19,12 @@ import { session } from 'electron';
|
|
|
19
19
|
import { toDisposable } from '@dimina-kit/electron-deck/main';
|
|
20
20
|
import { getSimulatorServicewechatReferer } from '../simulator/referer.js';
|
|
21
21
|
import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from './miniapp-partition.js';
|
|
22
|
+
/** The response headers this policy owns outright (compared lowercase). */
|
|
23
|
+
const CORS_OVERRIDE_HEADERS = new Set([
|
|
24
|
+
'access-control-allow-origin',
|
|
25
|
+
'access-control-allow-headers',
|
|
26
|
+
'access-control-allow-methods',
|
|
27
|
+
]);
|
|
22
28
|
/** Apply the simulator runtime's referer + CORS webRequest policy to one
|
|
23
29
|
* session. Each session installs its own listeners (a webRequest listener is
|
|
24
30
|
* per-session), so this runs once per partition. */
|
|
@@ -35,6 +41,17 @@ function applySimulatorWebRequestPolicy(simulatorSession) {
|
|
|
35
41
|
// CORS for the native render/service hosts to fetch compiled app resources
|
|
36
42
|
// cross-origin. (The COOP/COEP cross-origin-isolation headers were only for
|
|
37
43
|
// the removed default-path SharedArrayBuffer sync Worker — dropped.)
|
|
44
|
+
//
|
|
45
|
+
// This policy is the single authority for these three headers: any value
|
|
46
|
+
// the server itself sent must be REPLACED, not shadowed. responseHeaders
|
|
47
|
+
// keys keep the server's original casing, so assigning the lowercase key
|
|
48
|
+
// beside an existing `Access-Control-Allow-Origin` puts BOTH on the wire —
|
|
49
|
+
// Chromium then rejects the response for carrying multiple ACAO values
|
|
50
|
+
// (`*, *`), breaking every fetch against a CORS-compliant backend.
|
|
51
|
+
for (const key of Object.keys(headers)) {
|
|
52
|
+
if (CORS_OVERRIDE_HEADERS.has(key.toLowerCase()))
|
|
53
|
+
delete headers[key];
|
|
54
|
+
}
|
|
38
55
|
headers['access-control-allow-origin'] = ['*'];
|
|
39
56
|
headers['access-control-allow-headers'] = ['*'];
|
|
40
57
|
headers['access-control-allow-methods'] = ['*'];
|
|
@@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron';
|
|
|
2
2
|
import type { CompilationAdapter, CustomFileTypes, WorkbenchConfig } from '../../shared/types.js';
|
|
3
3
|
import type { BridgeRouterHandle } from '../ipc/bridge-router.js';
|
|
4
4
|
import type { ConsoleForwarder } from './console-forward/index.js';
|
|
5
|
+
import type { DiagnosticsBus } from './diagnostics/index.js';
|
|
5
6
|
import type { NetworkForwarder } from './network-forward/index.js';
|
|
6
7
|
import type { AppDataTap } from './simulator-appdata/index.js';
|
|
7
8
|
import type { StorageApi } from './simulator-storage/index.js';
|
|
@@ -167,6 +168,15 @@ export interface WorkbenchContext {
|
|
|
167
168
|
* installed.
|
|
168
169
|
*/
|
|
169
170
|
consoleForwarder?: ConsoleForwarder;
|
|
171
|
+
/**
|
|
172
|
+
* Authoritative diagnostics bus for main-synthesized developer diagnostics
|
|
173
|
+
* (page-not-found, logic-bundle-unreachable, service-host-navigation-failed,
|
|
174
|
+
* …), set by `installBridgeRouter`. `consoleForwarder` subscribes to it so
|
|
175
|
+
* every diagnostic also surfaces in the owning session's embedded DevTools
|
|
176
|
+
* Console panel, not just the main-process log. Undefined until the bridge
|
|
177
|
+
* router is installed.
|
|
178
|
+
*/
|
|
179
|
+
diagnostics?: DiagnosticsBus;
|
|
170
180
|
/**
|
|
171
181
|
* Native-host network forwarder, set in app bootstrap. Attaches the CDP
|
|
172
182
|
* debugger to the simulator WCV (where `wx.request`/`downloadFile`/`uploadFile`
|
|
@@ -38,8 +38,8 @@ export function createWorkspaceService(ctx) {
|
|
|
38
38
|
// wins request tokens, so a close mid-open can't disposeAll() the views the open
|
|
39
39
|
// is building or desync currentSession vs the bridge appSessions. See op-lock.ts.
|
|
40
40
|
const opLock = createOpLock();
|
|
41
|
-
function sendStatus(status, message, hotReload) {
|
|
42
|
-
ctx.notify.projectStatus(
|
|
41
|
+
function sendStatus(status, message, hotReload, pages, watcherDead) {
|
|
42
|
+
ctx.notify.projectStatus({ status, message, ...(hotReload ? { hotReload: true } : {}), ...(pages ? { pages } : {}), ...(watcherDead ? { watcher: 'dead' } : {}) });
|
|
43
43
|
}
|
|
44
44
|
function bestEffort(label, fn) {
|
|
45
45
|
try {
|
|
@@ -127,8 +127,17 @@ export function createWorkspaceService(ctx) {
|
|
|
127
127
|
sourcemap: true,
|
|
128
128
|
fileTypes: ctx.fileTypes,
|
|
129
129
|
watch: compile.watch,
|
|
130
|
-
onRebuild: () =>
|
|
130
|
+
onRebuild: () => {
|
|
131
|
+
// getProjectPages never throws — failure yields the empty-pages
|
|
132
|
+
// shape. Empty means "read failed or degenerate project" (a valid
|
|
133
|
+
// app.json names ≥1 page) and is withheld so the renderer keeps its
|
|
134
|
+
// previous dropdown instead of blanking; the push is never blocked.
|
|
135
|
+
const { pages } = repo.getProjectPages(projectPath);
|
|
136
|
+
sendStatus('ready', '编译完成,已重启', true, pages.length ? pages : undefined);
|
|
137
|
+
},
|
|
131
138
|
onBuildError: (err) => sendStatus('error', String(err)),
|
|
139
|
+
// Watcher died mid-session (EMFILE, permission loss, …): non-fatal, so 'ready' stays but `watcher: 'dead'` flags that saves no longer auto-rebuild.
|
|
140
|
+
onWatcherError: () => sendStatus('ready', '文件监听已停止,保存不再触发自动编译', false, undefined, true),
|
|
132
141
|
onLog: (entry) => {
|
|
133
142
|
if (sessionGeneration !== logGeneration)
|
|
134
143
|
return;
|
|
@@ -28,6 +28,13 @@ export interface ServiceHostWindowOptions {
|
|
|
28
28
|
* which page logic referencing `qd.*` throws `ReferenceError: qd is not defined`.
|
|
29
29
|
*/
|
|
30
30
|
apiNamespaces?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Observes a spawn-navigation `loadURL` rejection (NOT encoded into the
|
|
33
|
+
* spawn URL). Without it the fresh-window path swallows the failure entirely
|
|
34
|
+
* — `did-finish-load` never fires, the session hangs, and the only signal is
|
|
35
|
+
* the launch watchdog's late timeout instead of the real cause.
|
|
36
|
+
*/
|
|
37
|
+
onLoadFailed?: (err: unknown) => void;
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Construct an (un-navigated) service-host BrowserWindow. Split out from
|
|
@@ -52,8 +59,15 @@ export declare function buildServiceHostSpawnUrl(opts: ServiceHostWindowOptions)
|
|
|
52
59
|
* preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
|
|
53
60
|
* Returns the load promise (resolves on did-finish-load). Attaches the dev-only
|
|
54
61
|
* detached-DevTools hook, matching the original behavior.
|
|
62
|
+
*
|
|
63
|
+
* A `loadURL` rejection is swallowed — the pooled-window recycle path expects
|
|
64
|
+
* a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
|
|
65
|
+
* given, still observes the original rejection reason (routed by the caller
|
|
66
|
+
* into the diagnostics bus) before this resolves.
|
|
55
67
|
*/
|
|
56
|
-
export declare function navigateServiceHost(win: BrowserWindow, url: string
|
|
68
|
+
export declare function navigateServiceHost(win: BrowserWindow, url: string, opts?: {
|
|
69
|
+
onLoadFailed?: (err: unknown) => void;
|
|
70
|
+
}): Promise<void>;
|
|
57
71
|
/**
|
|
58
72
|
* The spec a `ServiceHostPool` uses to pre-warm service-host windows. Keeps the
|
|
59
73
|
* pooled window byte-equivalent to `constructServiceHostWindow`'s output, and
|
|
@@ -72,9 +72,17 @@ export function buildServiceHostSpawnUrl(opts) {
|
|
|
72
72
|
* preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
|
|
73
73
|
* Returns the load promise (resolves on did-finish-load). Attaches the dev-only
|
|
74
74
|
* detached-DevTools hook, matching the original behavior.
|
|
75
|
+
*
|
|
76
|
+
* A `loadURL` rejection is swallowed — the pooled-window recycle path expects
|
|
77
|
+
* a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
|
|
78
|
+
* given, still observes the original rejection reason (routed by the caller
|
|
79
|
+
* into the diagnostics bus) before this resolves.
|
|
75
80
|
*/
|
|
76
|
-
export function navigateServiceHost(win, url) {
|
|
77
|
-
const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, () =>
|
|
81
|
+
export function navigateServiceHost(win, url, opts) {
|
|
82
|
+
const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, (err) => {
|
|
83
|
+
opts?.onLoadFailed?.(err);
|
|
84
|
+
return undefined;
|
|
85
|
+
});
|
|
78
86
|
if (!app.isPackaged && process.env.NODE_ENV !== 'test') {
|
|
79
87
|
// Recycled pooled windows get navigated repeatedly. A `once` hook that never
|
|
80
88
|
// fires (the spawn was disposed before its service.html settled) stays on the
|
|
@@ -141,7 +149,7 @@ export function createServiceHostWindow(opts) {
|
|
|
141
149
|
// so its logic-layer localStorage/cookies are shared with the project's render
|
|
142
150
|
// side only, never with other projects.
|
|
143
151
|
const win = constructServiceHostWindow({ appId: opts.appId, partition: miniappPartition(opts.appId, opts.projectPath) });
|
|
144
|
-
void navigateServiceHost(win, buildServiceHostSpawnUrl(opts));
|
|
152
|
+
void navigateServiceHost(win, buildServiceHostSpawnUrl(opts), { onLoadFailed: opts.onLoadFailed });
|
|
145
153
|
return win;
|
|
146
154
|
}
|
|
147
155
|
//# sourceMappingURL=create.js.map
|